mirror of
https://github.com/BradNut/weddingsite
synced 2025-09-08 17:40:36 +00:00
Adding wedding website files.
This commit is contained in:
parent
8dcf63ff9d
commit
7400a5b50d
118 changed files with 19355 additions and 0 deletions
12
.vscode/launch.json
vendored
Normal file
12
.vscode/launch.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"name": "Launch Program",
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"port": 9229
|
||||
}
|
||||
]
|
||||
}
|
||||
17
README.md
Normal file
17
README.md
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# Wedding Website
|
||||
|
||||
## This is a skeleton template of the wedding website I created
|
||||
## Names, Dates, Locations are all hardcoded to a value
|
||||
|
||||
The site implements a basic auth to protect access without knowing the password to the site.
|
||||
|
||||
## Tech
|
||||
Overall a typical NextJS Application
|
||||
### Frontend
|
||||
- ReactJS
|
||||
- Styled Components
|
||||
- Next Iron Session for Login
|
||||
### Backend
|
||||
- NextJS APIs
|
||||
- Mongoose DB for MongoDB
|
||||
- Used to store RSVPs and default login
|
||||
81
components/CustomNextCloudinaryImage.js
Normal file
81
components/CustomNextCloudinaryImage.js
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { buildUrl } from 'cloudinary-build-url';
|
||||
// import wait from 'waait';
|
||||
import styled, { keyframes } from 'styled-components';
|
||||
|
||||
const blurImageTransition = keyframes`
|
||||
from {
|
||||
filter: blur(0);
|
||||
}
|
||||
to {
|
||||
|
||||
filter: blur(5px)
|
||||
}
|
||||
`;
|
||||
|
||||
const unblurImageTransition = keyframes`
|
||||
from {
|
||||
filter: blur(5px)
|
||||
}
|
||||
to {
|
||||
filter: blur(0);
|
||||
}
|
||||
`;
|
||||
|
||||
const CustomStylesContainer = styled.div`
|
||||
.unblur {
|
||||
animation: ${unblurImageTransition} 1s linear;
|
||||
}
|
||||
.blur {
|
||||
animation: ${blurImageTransition} 1s linear;
|
||||
}
|
||||
`;
|
||||
|
||||
const CustomNextCloudinaryImage = (props) => {
|
||||
const { height, width, src, onLoad, blur, resize, ...other } = props;
|
||||
const [onLoadCount, setOnloadCount] = useState(0);
|
||||
const [imageLoaded, setImageLoaded] = useState(false);
|
||||
|
||||
const transformations = {};
|
||||
|
||||
if (resize) {
|
||||
transformations.resize = {
|
||||
type: 'scale',
|
||||
width,
|
||||
height,
|
||||
};
|
||||
}
|
||||
|
||||
const imageUrl = buildUrl(`${process.env.NEXT_PUBLIC_FOLDER_NAME}/${src}`, {
|
||||
cloud: {
|
||||
cloudName: process.env.NEXT_PUBLIC_CLOUD_NAME,
|
||||
},
|
||||
transformations,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (onLoadCount > 1) {
|
||||
setImageLoaded(true);
|
||||
}
|
||||
}, [onLoadCount]);
|
||||
|
||||
return (
|
||||
<CustomStylesContainer>
|
||||
<Image
|
||||
className={`${imageLoaded ? 'unblur' : 'blur'}`}
|
||||
onLoad={(e) => {
|
||||
setOnloadCount((prev) => prev + 1);
|
||||
if (onLoad) onLoad(e);
|
||||
}}
|
||||
src={imageUrl}
|
||||
objectFit="cover"
|
||||
width={width}
|
||||
height={height}
|
||||
{...other}
|
||||
/>
|
||||
</CustomStylesContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomNextCloudinaryImage;
|
||||
63
components/CustomNextImage.js
Normal file
63
components/CustomNextImage.js
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
import styled, { keyframes } from 'styled-components';
|
||||
|
||||
const blurImageTransition = keyframes`
|
||||
from {
|
||||
filter: blur(0);
|
||||
}
|
||||
to {
|
||||
|
||||
filter: blur(5px)
|
||||
}
|
||||
`;
|
||||
|
||||
const unblurImageTransition = keyframes`
|
||||
from {
|
||||
filter: blur(5px)
|
||||
}
|
||||
to {
|
||||
filter: blur(0);
|
||||
}
|
||||
`;
|
||||
|
||||
const CustomStylesContainer = styled.div`
|
||||
.unblur {
|
||||
animation: ${unblurImageTransition} 1s linear;
|
||||
}
|
||||
.blur {
|
||||
animation: ${blurImageTransition} 1s linear;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
const CustomNextImage = (props) => {
|
||||
const { height, width, src, onLoad, ...other } = props;
|
||||
const [onLoadCount, setOnloadCount] = useState(0);
|
||||
const [imageLoaded, setImageLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (onLoadCount > 1) {
|
||||
setImageLoaded(true);
|
||||
}
|
||||
}, [onLoadCount]);
|
||||
|
||||
return (
|
||||
<CustomStylesContainer>
|
||||
<Image
|
||||
className={`${imageLoaded ? 'unblur' : 'blur'}`}
|
||||
onLoad={(e) => {
|
||||
setOnloadCount((prev) => prev + 1);
|
||||
if (onLoad) onLoad(e);
|
||||
}}
|
||||
src={imageUrl}
|
||||
objectFit="cover"
|
||||
width={width}
|
||||
height={height}
|
||||
{...other}
|
||||
/>
|
||||
</CustomStylesContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomNextImage;
|
||||
69
components/Event.js
Normal file
69
components/Event.js
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import styled from 'styled-components';
|
||||
|
||||
const EventStyles = styled.article`
|
||||
display: grid;
|
||||
gap: 2rem;
|
||||
justify-content: center;
|
||||
margin-top: 3.5rem;
|
||||
.schedule-event {
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 2px solid var(--lightGrey);
|
||||
}
|
||||
`;
|
||||
|
||||
const ScheduleStyle = styled.article`
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
flex-direction: row;
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
export default function Event({ event }) {
|
||||
const {
|
||||
name,
|
||||
date,
|
||||
start,
|
||||
end,
|
||||
venueName,
|
||||
attire,
|
||||
description,
|
||||
openToAll,
|
||||
showSchedule,
|
||||
scheduleEvents,
|
||||
} = event;
|
||||
return (
|
||||
<EventStyles>
|
||||
<div className="center">
|
||||
<h2>{name}</h2>
|
||||
<h3>{date}</h3>
|
||||
<h3>
|
||||
{start}
|
||||
{end && ` - ${end}`}
|
||||
</h3>
|
||||
{venueName && <div dangerouslySetInnerHTML={{ __html: venueName }} />}
|
||||
{attire && <h4>{attire}</h4>}
|
||||
{description && <h4>{description}</h4>}
|
||||
</div>
|
||||
{showSchedule &&
|
||||
scheduleEvents &&
|
||||
scheduleEvents.map(({ name, start, end, venueName }) => (
|
||||
<ScheduleStyle key={name} className="schedule-event">
|
||||
<div>
|
||||
{start && (
|
||||
<h3>
|
||||
{start}
|
||||
{end && ` - {end}`}
|
||||
</h3>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{name && <h3>{name}</h3>}
|
||||
{venueName && (
|
||||
<div dangerouslySetInnerHTML={{ __html: venueName }} />
|
||||
)}
|
||||
</div>
|
||||
</ScheduleStyle>
|
||||
))}
|
||||
</EventStyles>
|
||||
);
|
||||
}
|
||||
75
components/Footer.js
Normal file
75
components/Footer.js
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import styled from 'styled-components';
|
||||
import Link from 'next/link';
|
||||
import useUser from '../lib/useUser';
|
||||
|
||||
const FooterStyles = styled.footer`
|
||||
display: grid;
|
||||
grid-template-rows: repeat(2, 1fr);
|
||||
gap: 1.5rem;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
gap: 1rem;
|
||||
text-align: center;
|
||||
|
||||
margin-top: 6rem;
|
||||
|
||||
@media (max-width: 800px) {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 4rem;
|
||||
}
|
||||
|
||||
hr {
|
||||
display: block;
|
||||
max-width: 50%;
|
||||
height: 0;
|
||||
max-height: 0;
|
||||
border: solid;
|
||||
width: 100%;
|
||||
border-width: thin 0 0 0;
|
||||
transition: inherit;
|
||||
border-color: var(--lightShade);
|
||||
color: var(--lightShade);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
padding: 0.2rem;
|
||||
font-weight: 600;
|
||||
color: var(--lightShade);
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0.5rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export default function Footer() {
|
||||
const { user } = useUser();
|
||||
|
||||
return (
|
||||
<FooterStyles>
|
||||
<div>
|
||||
<h2>
|
||||
<Link href="/">
|
||||
<a>N & N</a>
|
||||
</Link>
|
||||
</h2>
|
||||
{user && user.isLoggedIn === true ? (
|
||||
<>
|
||||
<hr />
|
||||
<h3>06.03.2030</h3>
|
||||
</>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<p>Created by Bradley</p>
|
||||
</div>
|
||||
</FooterStyles>
|
||||
);
|
||||
}
|
||||
43
components/Form.js
Normal file
43
components/Form.js
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const Form = ({ errorMessage, onSubmit }) => (
|
||||
<form onSubmit={onSubmit}>
|
||||
<label htmlFor="password">
|
||||
<span>Please enter the password to view this page</span>
|
||||
<input type="text" id="password" name="password" required />
|
||||
</label>
|
||||
|
||||
<button type="submit">Login</button>
|
||||
|
||||
{errorMessage && <p className="error">{errorMessage}</p>}
|
||||
|
||||
<style jsx>{`
|
||||
form,
|
||||
label {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
}
|
||||
label > span {
|
||||
font-weight: 600;
|
||||
}
|
||||
input {
|
||||
padding: 8px;
|
||||
margin: 0.3rem 0 1rem;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.error {
|
||||
color: brown;
|
||||
margin: 1rem 0 0;
|
||||
}
|
||||
`}</style>
|
||||
</form>
|
||||
);
|
||||
|
||||
export default Form;
|
||||
|
||||
Form.propTypes = {
|
||||
errorMessage: PropTypes.string,
|
||||
onSubmit: PropTypes.func,
|
||||
};
|
||||
118
components/GuestRSVP.js
Normal file
118
components/GuestRSVP.js
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
import { useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import PlusOneRSVP from './PlusOneRSVP';
|
||||
|
||||
export const GuestStyles = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
// flex-direction: row;
|
||||
justify-content: center;
|
||||
justify-items: center;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
margin: 1rem 0;
|
||||
h3 {
|
||||
margin-right: 2rem;
|
||||
}
|
||||
|
||||
input[type='radio'] {
|
||||
opacity: 0;
|
||||
position: fixed;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
/* input[type='checkbox'] {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
} */
|
||||
|
||||
label {
|
||||
background: none;
|
||||
color: var(--lightViolet);
|
||||
border: 1px solid var(--lightViolet);
|
||||
padding: 1rem 1.5rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
--cast: 2px;
|
||||
box-shadow: var(--cast) var(--cast) 0 var(--lightAccent);
|
||||
text-shadow: 0.5px 0.5px 0 rgba(0, 0, 0, 0.2);
|
||||
transition: all 0.2s;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
--cast: 4px;
|
||||
}
|
||||
&:active {
|
||||
--cast: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
label:hover {
|
||||
background-color: var(--lightViolet);
|
||||
color: var(--black);
|
||||
}
|
||||
|
||||
input[type='radio']:checked + label {
|
||||
background: var(--lightViolet);
|
||||
color: var(--black);
|
||||
border: 0;
|
||||
}
|
||||
|
||||
input[type='radio']:focus + label {
|
||||
background: var(--primary);
|
||||
color: var(--black);
|
||||
border: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export default function GuestRSVP({ guest, inputs, handleChange }) {
|
||||
if (!guest) {
|
||||
return <p>Loading...</p>;
|
||||
}
|
||||
|
||||
// const [plusOne, setPlusOne] = useState(
|
||||
// guest?.rsvpStatus === 'accepted' && guest?.plusOne
|
||||
// );
|
||||
|
||||
return (
|
||||
<>
|
||||
<GuestStyles key={guest.id}>
|
||||
<h3>
|
||||
{guest.firstName} {guest.lastName}
|
||||
</h3>
|
||||
<input
|
||||
type="radio"
|
||||
id={`${guest.id}-accepted`}
|
||||
name={`${guest.id}-rsvpStatus`}
|
||||
value="accepted"
|
||||
checked={inputs[guest.id]?.rsvpStatus === 'accepted'}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<label htmlFor={`${guest.id}-accepted`}>
|
||||
{inputs[guest.id]?.rsvpStatus === 'accepted' ? 'Accepted' : 'Accept'}
|
||||
</label>
|
||||
<input
|
||||
type="radio"
|
||||
id={`${guest.id}-declined`}
|
||||
name={`${guest.id}-rsvpStatus`}
|
||||
value="declined"
|
||||
checked={inputs[guest.id]?.rsvpStatus === 'declined'}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<label htmlFor={`${guest.id}-declined`}>
|
||||
{inputs[guest.id]?.rsvpStatus === 'declined' ? 'Declined' : 'Decline'}
|
||||
</label>
|
||||
</GuestStyles>
|
||||
{guest?.hasPlusOne && inputs[guest?.id]?.rsvpStatus === 'accepted' ? (
|
||||
<PlusOneRSVP
|
||||
guest={guest}
|
||||
inputs={inputs}
|
||||
handleChange={handleChange}
|
||||
/>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
51
components/Header.js
Normal file
51
components/Header.js
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import Link from 'next/link';
|
||||
import useUser from '../lib/useUser';
|
||||
import WeddingStart from './WeddingStart';
|
||||
import Nav from './Nav';
|
||||
|
||||
const HeaderStyles = styled.header`
|
||||
display: grid;
|
||||
gap: 1.8rem;
|
||||
margin: 2rem 1.5rem 1rem 1.5rem;
|
||||
nav {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 650px) {
|
||||
h2 {
|
||||
font-size: var(--h3);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const Header = () => {
|
||||
const { user, mutateUser } = useUser();
|
||||
return (
|
||||
<HeaderStyles>
|
||||
<div>
|
||||
<Link href="/">
|
||||
<a>
|
||||
<h1 className="center">Name & Name</h1>
|
||||
</a>
|
||||
</Link>
|
||||
{user && user.isLoggedIn === true ? (
|
||||
<>
|
||||
<h2 className="center">
|
||||
June 3rd, 2030 • New York, New York
|
||||
</h2>
|
||||
<h3 className="center">
|
||||
Countdown: <WeddingStart /> days!
|
||||
</h3>
|
||||
</>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</div>
|
||||
{user && user.isLoggedIn === true ? <Nav /> : ''}
|
||||
</HeaderStyles>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
23
components/HomeContent.js
Normal file
23
components/HomeContent.js
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import styled from 'styled-components';
|
||||
import CustomNextImage from './CustomNextImage';
|
||||
import Timeline from './Timeline';
|
||||
|
||||
const HomeStyles = styled.div`
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
`;
|
||||
|
||||
export default function HomeContent() {
|
||||
return (
|
||||
<HomeStyles>
|
||||
<CustomNextImage
|
||||
src="https://via.placeholder.com/800X1307.png"
|
||||
height={880}
|
||||
width={1307}
|
||||
alt="Picture of Name and Name"
|
||||
blur
|
||||
/>
|
||||
<Timeline />
|
||||
</HomeStyles>
|
||||
);
|
||||
}
|
||||
23
components/Layout.js
Normal file
23
components/Layout.js
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import React from 'react';
|
||||
import Head from 'next/head';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const Layout = ({ children }) => (
|
||||
<>
|
||||
<Head>
|
||||
<title>NN By the Sea</title>
|
||||
</Head>
|
||||
<noscript>
|
||||
<h1>🐧🐧🐧 Please enable JavaScript to view our site. 🐧🐧🐧</h1>
|
||||
</noscript>
|
||||
<main>
|
||||
<div className="container">{children}</div>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
|
||||
export default Layout;
|
||||
|
||||
Layout.propTypes = {
|
||||
children: PropTypes.node,
|
||||
};
|
||||
176
components/Login.js
Normal file
176
components/Login.js
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import styled, { keyframes } from 'styled-components';
|
||||
import useUser from '../lib/useUser';
|
||||
import fetchJson from '../lib/fetchJson';
|
||||
import useForm from '../lib/useForm';
|
||||
|
||||
const loadingFrame = keyframes`
|
||||
from {
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
to {
|
||||
background-position: 100% 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
const FormStyles = styled.form`
|
||||
display: grid;
|
||||
|
||||
box-shadow: var(--level-2);
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
border: 1px solid var(--primary);
|
||||
padding: 20px;
|
||||
font-size: 1.5rem;
|
||||
line-height: 1.5;
|
||||
font-weight: 600;
|
||||
label {
|
||||
margin-top: 1.5rem;
|
||||
display: block;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
input,
|
||||
textarea,
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--primary);
|
||||
&:focus {
|
||||
outline: 0;
|
||||
border-color: var(--lightViolet);
|
||||
}
|
||||
}
|
||||
button,
|
||||
input[type='submit'] {
|
||||
width: 100%;
|
||||
margin-top: 0.5rem;
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
padding: 0.5rem 1.2rem;
|
||||
}
|
||||
fieldset {
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
padding: 0;
|
||||
|
||||
&[disabled] {
|
||||
opacity: 0.5;
|
||||
}
|
||||
&::before {
|
||||
height: 10px;
|
||||
content: '';
|
||||
display: block;
|
||||
border-radius: 4px;
|
||||
background-image: linear-gradient(
|
||||
to right,
|
||||
var(--primary) 0%,
|
||||
var(--lightViolet) 50%,
|
||||
var(--primary) 100%
|
||||
);
|
||||
}
|
||||
&[aria-busy='true']::before {
|
||||
background-size: 50% auto;
|
||||
animation: ${loadingFrame} 0.5s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
.penguin {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const Login = () => {
|
||||
const router = useRouter();
|
||||
|
||||
const { inputs, handleChange, clearForm, resetForm } = useForm({
|
||||
username: 'ibwedding',
|
||||
password: '',
|
||||
penguin: 'penguin',
|
||||
});
|
||||
|
||||
const { mutateUser } = useUser({
|
||||
redirectTo: '/',
|
||||
redirectIfFound: true,
|
||||
});
|
||||
|
||||
const [errorMsg, setErrorMsg] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
async function handleSubmit({ username, password, penguin }) {
|
||||
const body = {
|
||||
username,
|
||||
password,
|
||||
penguin,
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await mutateUser(
|
||||
fetchJson('/api/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('An unexpected error happened:', error);
|
||||
setErrorMsg('Unable to login');
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* <Form isLogin errorMessage={errorMsg} onSubmit={handleSubmit} /> */}
|
||||
<FormStyles
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
await handleSubmit(inputs);
|
||||
if (!errorMsg && errorMsg?.length !== 0) {
|
||||
router.push({
|
||||
pathname: `/rsvp`,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{errorMsg && <p className="error">Error: {errorMsg}</p>}
|
||||
<fieldset aria-busy={loading} disabled={loading}>
|
||||
<label htmlFor="username">
|
||||
<span>Username</span>
|
||||
<input
|
||||
required
|
||||
type="text"
|
||||
id="username"
|
||||
name="username"
|
||||
value={inputs.username}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</label>
|
||||
<label htmlFor="password">
|
||||
<span>Please enter the password to view this page</span>
|
||||
<input
|
||||
required
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
placeholder="Password"
|
||||
value={inputs.password}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</label>
|
||||
<input
|
||||
type="penguin"
|
||||
name="penguin"
|
||||
value={inputs.penguin}
|
||||
onChange={handleChange}
|
||||
className="penguin"
|
||||
/>
|
||||
<button type="submit">Login</button>
|
||||
</fieldset>
|
||||
</FormStyles>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
95
components/Modal.js
Normal file
95
components/Modal.js
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
import { createPortal } from 'react-dom';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const ModalOverlayStyles = styled.div`
|
||||
background-color: #999999;
|
||||
height: 100vh;
|
||||
left: 0;
|
||||
opacity: 0.5;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 100vw;
|
||||
z-index: 500;
|
||||
`;
|
||||
|
||||
const ModalWrapperStyles = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
left: 0;
|
||||
outline: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
position: fixed;
|
||||
top: 25%;
|
||||
width: 100%;
|
||||
z-index: 1000;
|
||||
`;
|
||||
|
||||
const ModalStyles = styled.div`
|
||||
align-items: center;
|
||||
background: var(--background);
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 1.8rem;
|
||||
max-width: 500px;
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
`;
|
||||
|
||||
const ModalHeaderStyles = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1.8rem 0.95rem;
|
||||
`;
|
||||
|
||||
const ModalTitleStyles = styled.h2`
|
||||
margin-bottom: 0.4rem;
|
||||
`;
|
||||
|
||||
const ModalButtonStyles = styled.button`
|
||||
// border-top: 1px solid var(--primary);
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
padding: 2rem;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const ModalDescriptionStyles = styled.span`
|
||||
padding: 2rem;
|
||||
font-size: 2rem;
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const Modal = ({ isVisible, hideModal, title, message, children }) =>
|
||||
isVisible
|
||||
? createPortal(
|
||||
<>
|
||||
<ModalOverlayStyles />
|
||||
<ModalWrapperStyles
|
||||
aria-modal
|
||||
aria-hidden={!isVisible}
|
||||
tabIndex={-1}
|
||||
role="dialog"
|
||||
aria-label={title}
|
||||
>
|
||||
<ModalStyles>
|
||||
<ModalHeaderStyles>
|
||||
<ModalTitleStyles>{title}</ModalTitleStyles>
|
||||
<ModalDescriptionStyles>
|
||||
{message}
|
||||
{children}
|
||||
</ModalDescriptionStyles>
|
||||
</ModalHeaderStyles>
|
||||
<ModalButtonStyles type="button" onClick={hideModal}>
|
||||
Close
|
||||
</ModalButtonStyles>
|
||||
</ModalStyles>
|
||||
</ModalWrapperStyles>
|
||||
</>,
|
||||
document.body
|
||||
)
|
||||
: null;
|
||||
|
||||
export default Modal;
|
||||
30
components/Nav.js
Normal file
30
components/Nav.js
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { NavLink } from './NavLink';
|
||||
import NavStyles from './styles/NavStyles';
|
||||
|
||||
export default function Nav() {
|
||||
return (
|
||||
<NavStyles>
|
||||
<NavLink href="/">
|
||||
<a>Home</a>
|
||||
</NavLink>
|
||||
<NavLink href="/cats">
|
||||
<a>Our Cats</a>
|
||||
</NavLink>
|
||||
<NavLink href="/party">
|
||||
<a>Wedding Party</a>
|
||||
</NavLink>
|
||||
<NavLink href="/photos">
|
||||
<a>Photos</a>
|
||||
</NavLink>
|
||||
<NavLink href="/travelstay">
|
||||
<a>Travel & Stay</a>
|
||||
</NavLink>
|
||||
<NavLink href="/qanda">
|
||||
<a>Q + A</a>
|
||||
</NavLink>
|
||||
<NavLink href="/rsvp">
|
||||
<a>RSVP</a>
|
||||
</NavLink>
|
||||
</NavStyles>
|
||||
);
|
||||
}
|
||||
19
components/NavLink.js
Normal file
19
components/NavLink.js
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
export const NavLink = ({ children, href }) => {
|
||||
const child = React.Children.only(children);
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<Link href={href}>
|
||||
{React.cloneElement(child, {
|
||||
'aria-current':
|
||||
router.pathname === href || router.pathname.includes(`${href}/`)
|
||||
? 'page'
|
||||
: null,
|
||||
})}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
339
components/Page.js
Normal file
339
components/Page.js
Normal file
|
|
@ -0,0 +1,339 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import styled, { createGlobalStyle } from 'styled-components';
|
||||
import 'normalize.css';
|
||||
import Header from './Header';
|
||||
import Typography from './Typography';
|
||||
import Footer from './Footer';
|
||||
import LayoutStyles from './styles/LayoutStyles';
|
||||
|
||||
const GlobalStyles = createGlobalStyle`
|
||||
:root {
|
||||
/* Define Colors as colors */
|
||||
--red: #990000;
|
||||
--coral: #e64c44;
|
||||
--blue: #336699;
|
||||
--seaFoamBlue: #466b72;
|
||||
--purpleBlue: #2E2B5E;
|
||||
--white: #fffffe;
|
||||
--greyWhite: #E6E3E0;
|
||||
--grey: #efefef;
|
||||
--yellow: #ffc600;
|
||||
--light: #ffffff;
|
||||
--black: #1B2D45;
|
||||
--dark: #000000;
|
||||
--seaGreen: #83C6A4;
|
||||
--lighterDark: #131415;
|
||||
--shellYellow: #ffc850;
|
||||
--lightGrey: #C5C5C5;
|
||||
--lightGray: var(--lightGrey);
|
||||
--lightShade: #f8f7f5;
|
||||
--darkGrey: #272727;
|
||||
--coralTan: #ffddb7;
|
||||
// --coralTan: #fccfb9;
|
||||
// --coralTan: #ffddb7;
|
||||
// --darkTan: #dfb28e;
|
||||
--blueGreen: #1d384e;
|
||||
--lightViolet: #C298F7;
|
||||
--darkerViolet: #7551a9;
|
||||
|
||||
/* Define Colors intentions */
|
||||
--primary: var(--coralTan);
|
||||
--secondary: var(--coralTan);
|
||||
--danger: var(--grey);
|
||||
--background: var(--seaFoamBlue);
|
||||
--textColor: var(--black);
|
||||
--buttonTextColor: var(--black);
|
||||
--textAccent: var(--purpleBlue);
|
||||
--lineColor: var(--grey);
|
||||
--cardBg: var(--darkGrey);
|
||||
--headerBackground: var(--darkGrey);
|
||||
--footerBackground: var(--darkGrey);
|
||||
--linkHover: var(--lightViolet);
|
||||
--lightHairLine: var(--lightGrey);
|
||||
|
||||
/* Styles */
|
||||
--line: solid 1px var(--lineColor);
|
||||
|
||||
/* Type */
|
||||
--headingFont: 'Istok Web';
|
||||
--bodyFont: 'Kanit';
|
||||
--baseFontSize: 100%;
|
||||
--h1: 4.209rem;
|
||||
--h2: 3.157rem;
|
||||
--h3: 2.369rem;
|
||||
--h4: 1.777rem;
|
||||
--h5: 1.333em;
|
||||
--h6: 1rem;
|
||||
--bodyTextSize: 1.777rem;
|
||||
--smallText: 1.333rem;
|
||||
--lineHeight: 1.75;
|
||||
|
||||
/* Elevation */
|
||||
--level-0: none;
|
||||
--level-1: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||
--level-2: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
--level-3: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
||||
0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
--level-4: 0 20px 25px -5px rgba(0, 0, 0, 0.1),
|
||||
0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
--level-1-primary: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||
--level-2-primary: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||
0 2px 4px -1px #C298F7;
|
||||
--level-3-primary: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
||||
0 4px 6px -2px #C298F7;
|
||||
--level-4-primary: 0 20px 25px -5px rgba(0, 0, 0, 0.1),
|
||||
0 10px 10px -5px #C298F7;
|
||||
|
||||
/* Positioning */
|
||||
--containerPadding: 2.5%;
|
||||
--headerHeight: 8rem;
|
||||
--borderRadius: 10px;
|
||||
--maxWidth: 850px;
|
||||
|
||||
/* Media Queryies - Not yet supported in CSS */
|
||||
/*
|
||||
--xsmall: 340px;
|
||||
--small: 500px;
|
||||
--large: 960px;
|
||||
--wide: 1200px;
|
||||
*/
|
||||
}
|
||||
|
||||
html {
|
||||
width: 100%;
|
||||
background-image: url('https://res.cloudinary.com/royvalentinedev/image/upload/v1621792514/wedding/Background_u0cgyd.png');
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
background-color: var(--seaFoamBlue);
|
||||
font-size: 62.5%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
*, *::before, *::after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
body {
|
||||
line-height: var(--lineHeight);
|
||||
color: var(--primary);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-size: var(--bodyTextSize);
|
||||
}
|
||||
|
||||
body::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
}
|
||||
html {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--primary) var(--background);
|
||||
}
|
||||
body::-webkit-scrollbar-track {
|
||||
background: var(--background);
|
||||
}
|
||||
body::-webkit-scrollbar-thumb {
|
||||
background-color: var(--primary) ;
|
||||
border-radius: 6px;
|
||||
border: 3px solid var(--background);
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: var(--primary);
|
||||
color: var(--background);
|
||||
}
|
||||
|
||||
a.button,
|
||||
button {
|
||||
background: var(--lightViolet);
|
||||
color: var(--black);
|
||||
border: 0;
|
||||
padding: 1rem 1.5rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
--cast: 2px;
|
||||
box-shadow: var(--level-1-primary);
|
||||
text-shadow: 0.5px 0.5px 0 rgba(0,0,0,0.2);
|
||||
transition: all 0.2s;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
/* --cast: 4px; */
|
||||
box-shadow: var(--level-2-primary)
|
||||
}
|
||||
&:active {
|
||||
/* --cast: 2px; */
|
||||
box-shadow: var(--level-0)
|
||||
}
|
||||
}
|
||||
|
||||
a.button.ghost,
|
||||
button.ghost {
|
||||
background: none;
|
||||
color: var(--lightViolet);
|
||||
border: 1px solid var(--lightViolet);
|
||||
|
||||
&:disabled {
|
||||
color: hsla(266, 86%, 78%, 0.53);
|
||||
border: 1px solid hsla(266, 86%, 78%, 0.53);
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--linkHover);
|
||||
// text-decoration: underline;
|
||||
}
|
||||
|
||||
input,textarea {
|
||||
font-size: 2rem;
|
||||
padding: 0.4rem;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--primary);
|
||||
background: inherit;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 0;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
nav {
|
||||
a::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 0.3rem;
|
||||
bottom: -0.4rem;
|
||||
left: 0px;
|
||||
background: var(--secondary);
|
||||
transition: transform 0.3s ease 0s;
|
||||
transition-timing-function: cubic-bezier(1, -0.65, 0, 2.31);
|
||||
transform: scale(0);
|
||||
}
|
||||
|
||||
@media (max-width: 650px) {
|
||||
a {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
position: relative;
|
||||
font-size: 2rem;
|
||||
text-decoration: none;
|
||||
margin: 0.4rem 0;
|
||||
|
||||
&[aria-current='page'],
|
||||
&.current-parent {
|
||||
&:after {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--secondary);
|
||||
&:after {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
img,
|
||||
figure {
|
||||
border-radius: 4px;
|
||||
box-shadow: var(--level-2);
|
||||
}
|
||||
|
||||
.emoji:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 1.5rem;
|
||||
background: var(--cardBg);
|
||||
box-shadow: var(--level-3);
|
||||
border-radius: var(--borderRadius);
|
||||
}
|
||||
|
||||
/* First item will never have margin top */
|
||||
.card > *:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* Last item will never have margin bottom */
|
||||
.card > *:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.modal {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.toast {
|
||||
color: white;
|
||||
background: var(--black);
|
||||
border-radius: var(--borderRadius);
|
||||
padding: 20px;
|
||||
box-shadow: var(--level-2);
|
||||
position: fixed;
|
||||
bottom: 10px;
|
||||
left: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
.toast p {
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
const ContentStyles = styled.main`
|
||||
max-width: var(--maxWidth);
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
|
||||
p {
|
||||
margin: 0 auto;
|
||||
}
|
||||
`;
|
||||
|
||||
export default function Page({ children }) {
|
||||
return (
|
||||
<div>
|
||||
<GlobalStyles />
|
||||
<Typography />
|
||||
<LayoutStyles>
|
||||
<Header />
|
||||
<ContentStyles>{children}</ContentStyles>
|
||||
<Footer />
|
||||
</LayoutStyles>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Page.propTypes = {
|
||||
children: PropTypes.any,
|
||||
};
|
||||
122
components/PlusOneRSVP.js
Normal file
122
components/PlusOneRSVP.js
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
import { useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const PlusOneStyles = styled.div`
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
|
||||
.plusone__names {
|
||||
display: grid;
|
||||
gap: 1.2rem;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
display: grid;
|
||||
grid-template-columns: min-content auto;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 2rem;
|
||||
|
||||
.checkbox__input {
|
||||
display: grid;
|
||||
grid-template-areas: 'checkbox';
|
||||
|
||||
> * {
|
||||
grid-area: checkbox;
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox__input input:checked + .checkbox__control svg {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.checkbox__input input:focus + .checkbox__control {
|
||||
box-shadow: var(--level-2-primary);
|
||||
}
|
||||
|
||||
.checkbox__control {
|
||||
display: inline-grid;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
border-radius: 0.1em;
|
||||
border: 0.1em solid var(--lightViolet);
|
||||
|
||||
svg {
|
||||
transition: transform 0.1s ease-in 25ms;
|
||||
transform: scale(0);
|
||||
transform-origin: bottom left;
|
||||
}
|
||||
}
|
||||
|
||||
input[type='checkbox'] {
|
||||
opacity: 0;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default function PlusOneRSVP({ guest, inputs, handleChange }) {
|
||||
function onChangePlusOne(e) {
|
||||
handleChange({
|
||||
target: {
|
||||
value: !inputs[`${guest.id}`].plusOne,
|
||||
name: `${guest.id}-plusOne`,
|
||||
type: 'text',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<PlusOneStyles key={`${guest.id}`}>
|
||||
<label htmlFor={`${guest.id}`} className="checkbox">
|
||||
<span className="checkbox__input">
|
||||
<input
|
||||
id={`${guest.id}`}
|
||||
name={`${guest.id}`}
|
||||
checked={inputs[`${guest.id}`].plusOne}
|
||||
onChange={onChangePlusOne}
|
||||
type="checkbox"
|
||||
/>
|
||||
<span className="checkbox__control">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="3"
|
||||
d="M1.73 12.91l6.37 6.37L22.79 4.59"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
<span className="checkbox__label">Plus one? </span>
|
||||
</label>
|
||||
{inputs[`${guest.id}`].plusOne && (
|
||||
<div className="plusone__names">
|
||||
<input
|
||||
type="text"
|
||||
id={`${guest.id}-plusOneFirstName`}
|
||||
name={`${guest.id}-plusOneFirstName`}
|
||||
placeholder="First Name"
|
||||
value={inputs[`${guest.id}`]?.plusOneFirstName}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
id={`${guest.id}-plusOneLastName`}
|
||||
name={`${guest.id}-plusOneLastName`}
|
||||
placeholder="Last Name"
|
||||
value={inputs[`${guest.id}`]?.plusOneLastName}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</PlusOneStyles>
|
||||
);
|
||||
}
|
||||
11
components/Timeline.js
Normal file
11
components/Timeline.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import timelineData from '../lib/events.json';
|
||||
import Event from './Event';
|
||||
|
||||
export default function Timeline() {
|
||||
return timelineData.map(
|
||||
(timelineEvent) =>
|
||||
timelineEvent?.openToAll && (
|
||||
<Event key={timelineEvent?.name} event={timelineEvent} />
|
||||
)
|
||||
);
|
||||
}
|
||||
77
components/Typography.js
Normal file
77
components/Typography.js
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import { createGlobalStyle } from 'styled-components';
|
||||
|
||||
const Typography = createGlobalStyle`
|
||||
@font-face {
|
||||
font-family: 'Josefin_Sans';
|
||||
src: url('/fonts/Josefin_Sans/static/JosefinSans-Regular.ttf') format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Josefin_Sans_Bold';
|
||||
src: url('/fonts/Josefin_Sans/static/JosefinSans-Bold.ttf') format('truetype');
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
display: swap;
|
||||
}
|
||||
|
||||
html {
|
||||
font-family: 'Josefin_Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
/* TODO: Change to theme value of light or dark */
|
||||
color: var(--lightGrey);
|
||||
}
|
||||
|
||||
p, li {
|
||||
letter-spacing: 0.5px;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
h1,h2,h3,h4,h5,h6 {
|
||||
font-family: 'Josefin_Sans_Bold', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
/* TODO: Change to theme value of light or dark */
|
||||
color: var(--primary);
|
||||
font-weight: normal;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
font-size: var(--h1);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: var(--h2);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: var(--h3);
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: var(--h4);
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: var(--h5);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Josefin_Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--lightGrey);
|
||||
}
|
||||
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tilt {
|
||||
transform: rotate(-2deg);
|
||||
}
|
||||
`;
|
||||
|
||||
export default Typography;
|
||||
22
components/WeddingStart.js
Normal file
22
components/WeddingStart.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { useState } from 'react';
|
||||
import useInterval from '../utils/useInterval';
|
||||
|
||||
function useWeddingStart({ update = 60000 }) {
|
||||
const weddingDate = 1906736400000;
|
||||
const [timeToWedding, setTime] = useState(weddingDate - Date.now());
|
||||
useInterval(() => {
|
||||
setTime(weddingDate - Date.now());
|
||||
}, update);
|
||||
return {
|
||||
timeToWedding,
|
||||
timeAsDays: Math.ceil(timeToWedding / 1000 / 60 / 60 / 24),
|
||||
};
|
||||
}
|
||||
|
||||
export default function WeddingStart() {
|
||||
const { timeToWedding, timeAsDays } = useWeddingStart({
|
||||
update: 60000,
|
||||
});
|
||||
|
||||
return <>{timeAsDays}</>;
|
||||
}
|
||||
75
components/styles/Form.js
Normal file
75
components/styles/Form.js
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import styled, { keyframes } from 'styled-components';
|
||||
|
||||
const loading = keyframes`
|
||||
from {
|
||||
background-position: 0 0;
|
||||
/* rotate: 0; */
|
||||
}
|
||||
|
||||
to {
|
||||
background-position: 100% 100%;
|
||||
/* rotate: 360deg; */
|
||||
}
|
||||
`;
|
||||
|
||||
const Form = styled.form`
|
||||
display: grid;
|
||||
|
||||
box-shadow: var(--level-2);
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
border: 1px solid var(--primary);
|
||||
padding: 20px;
|
||||
font-size: 1.5rem;
|
||||
line-height: 1.5;
|
||||
font-weight: 600;
|
||||
label {
|
||||
margin-top: 1.5rem;
|
||||
display: block;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
input,
|
||||
textarea,
|
||||
select {
|
||||
width: 100%;
|
||||
border: 1px solid var(--primary);
|
||||
&:focus {
|
||||
outline: 0;
|
||||
border-color: var(--lightViolet);
|
||||
}
|
||||
}
|
||||
button,
|
||||
input[type='submit'] {
|
||||
width: 100%;
|
||||
margin-top: 0.5rem;
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
padding: 0.5rem 1.2rem;
|
||||
}
|
||||
fieldset {
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
padding: 0;
|
||||
|
||||
&[disabled] {
|
||||
opacity: 0.5;
|
||||
}
|
||||
&::before {
|
||||
height: 10px;
|
||||
content: '';
|
||||
display: block;
|
||||
border-radius: 4px;
|
||||
background-image: linear-gradient(
|
||||
to right,
|
||||
var(--primary) 0%,
|
||||
var(--lightViolet) 50%,
|
||||
var(--primary) 100%
|
||||
);
|
||||
}
|
||||
&[aria-busy='true']::before {
|
||||
background-size: 50% auto;
|
||||
animation: ${loading} 0.5s linear infinite;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Form;
|
||||
16
components/styles/LayoutStyles.js
Normal file
16
components/styles/LayoutStyles.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import styled from 'styled-components';
|
||||
|
||||
const LayoutStyles = styled.div`
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
|
||||
p,
|
||||
li {
|
||||
word-wrap: normal;
|
||||
font-size: var(--bodyTextSize);
|
||||
color: var(--primary);
|
||||
}
|
||||
`;
|
||||
|
||||
export default LayoutStyles;
|
||||
38
components/styles/NavStyles.js
Normal file
38
components/styles/NavStyles.js
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import styled from 'styled-components';
|
||||
|
||||
const NavStyles = styled.nav`
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
font-size: 2rem;
|
||||
gap: 1.5rem;
|
||||
|
||||
a,
|
||||
button {
|
||||
padding: 1rem 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
text-transform: uppercase;
|
||||
font-weight: 900;
|
||||
font-size: 1.8rem;
|
||||
background: none;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
|
||||
@media (max-width: 700px) {
|
||||
font-size: 1.5rem;
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 1300px) {
|
||||
// border-top: 1px solid var(--lightGray);
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export default NavStyles;
|
||||
80
components/styles/nprogress.css
Executable file
80
components/styles/nprogress.css
Executable file
|
|
@ -0,0 +1,80 @@
|
|||
/* Make clicks pass-through */
|
||||
#nprogress {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#nprogress .bar {
|
||||
background: var(--secondary);
|
||||
position: fixed;
|
||||
z-index: 1031;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
width: 100%;
|
||||
height: 5px;
|
||||
}
|
||||
|
||||
/* Fancy blur effect */
|
||||
#nprogress .peg {
|
||||
display: block;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
width: 100px;
|
||||
height: 100%;
|
||||
box-shadow: 0 0 10px var(--secondary), 0 0 5px var(--secondary);
|
||||
opacity: 1;
|
||||
|
||||
-webkit-transform: rotate(3deg) translate(0px, -4px);
|
||||
-ms-transform: rotate(3deg) translate(0px, -4px);
|
||||
transform: rotate(3deg) translate(0px, -4px);
|
||||
}
|
||||
|
||||
/* Remove these to get rid of the spinner */
|
||||
#nprogress .spinner {
|
||||
display: block;
|
||||
position: fixed;
|
||||
z-index: 1031;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
}
|
||||
|
||||
#nprogress .spinner-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
box-sizing: border-box;
|
||||
|
||||
border: solid 2px transparent;
|
||||
border-top-color: var(--secondary);
|
||||
border-left-color: var(--secondary);
|
||||
border-radius: 50%;
|
||||
|
||||
-webkit-animation: nprogress-spinner 400ms linear infinite;
|
||||
animation: nprogress-spinner 400ms linear infinite;
|
||||
}
|
||||
|
||||
.nprogress-custom-parent {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nprogress-custom-parent #nprogress .spinner,
|
||||
.nprogress-custom-parent #nprogress .bar {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
@-webkit-keyframes nprogress-spinner {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@keyframes nprogress-spinner {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
69
lib/events.json
Normal file
69
lib/events.json
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
[
|
||||
{
|
||||
"name": "Wedding Practicing",
|
||||
"date": "Sunday, June 2nd, 2030",
|
||||
"start": "",
|
||||
"end": "",
|
||||
"venueName": "",
|
||||
"attire": "",
|
||||
"description": "Rehearsal & Rehearsal Dinner",
|
||||
"openToAll": false,
|
||||
"showSchedule": false,
|
||||
"scheduleEvents": []
|
||||
},
|
||||
{
|
||||
"name": "Wedding Saying Hello",
|
||||
"date": "Sunday, June 2nd, 2030",
|
||||
"start": "9:00 PM",
|
||||
"end": "",
|
||||
"venueName": "",
|
||||
"attire": "",
|
||||
"description": "Come hang out with us and say hi! Location TBD",
|
||||
"openToAll": true,
|
||||
"showSchedule": false,
|
||||
"scheduleEvents": []
|
||||
},
|
||||
{
|
||||
"name": "Wedding Saying I Do",
|
||||
"date": "Monday, June 3rd, 2030",
|
||||
"start": "5:00 PM",
|
||||
"end": "11:00 PM",
|
||||
"venueName": "<div><h4>Central Park New York, NY, USA</h4></div>",
|
||||
"attire": "",
|
||||
"description": "",
|
||||
"openToAll": true,
|
||||
"showSchedule": false,
|
||||
"scheduleEvents": [
|
||||
{
|
||||
"name": "Ceremony",
|
||||
"start": "5:00 PM",
|
||||
"end": "",
|
||||
"venueName": "<div><h4>Central Park New York, NY, USA</h4></div>"
|
||||
},
|
||||
{
|
||||
"name": "Cocktail Hour",
|
||||
"start": "5:30 PM",
|
||||
"end": "",
|
||||
"venueName": "<div><h4>Central Park New York, NY, USA</h4></div>"
|
||||
},
|
||||
{
|
||||
"name": "Dinner, Dancing, Dessert",
|
||||
"start": "7:00 PM",
|
||||
"end": "",
|
||||
"venueName": "<div><h4>Central Park New York, NY, USA</h4></div>"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Wedding Saying Goodbye",
|
||||
"date": "Tuesday, June 4th, 2030",
|
||||
"start": "",
|
||||
"end": "",
|
||||
"venueName": "",
|
||||
"attire": "",
|
||||
"description": "Farewell Brunch",
|
||||
"openToAll": false,
|
||||
"showSchedule": false,
|
||||
"scheduleEvents": []
|
||||
}
|
||||
]
|
||||
24
lib/fetchJson.js
Normal file
24
lib/fetchJson.js
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
export default async function fetcher(...args) {
|
||||
try {
|
||||
const response = await fetch(...args);
|
||||
|
||||
// if the server replies, there's always some data in json
|
||||
// if there's a network error, it will throw at the previous line
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
return data;
|
||||
}
|
||||
|
||||
// console.log(JSON.stringify(data));
|
||||
const error = new Error(response?.statusText || data?.message);
|
||||
error.response = response;
|
||||
error.data = data;
|
||||
throw error;
|
||||
} catch (error) {
|
||||
if (!error.data) {
|
||||
error.data = { message: error.message };
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
14
lib/session.js
Normal file
14
lib/session.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
// this file is a wrapper with defaults to be used in both API routes and `getServerSideProps` functions
|
||||
import { withIronSession } from 'next-iron-session';
|
||||
|
||||
export default function withSession(handler) {
|
||||
return withIronSession(handler, {
|
||||
password: process.env.SECRET_COOKIE_PASSWORD,
|
||||
cookieName: 'weddingwebsitesession',
|
||||
cookieOptions: {
|
||||
// the next line allows to use the session in non-https environments like
|
||||
// Next.js dev mode (http://localhost:3000)
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
},
|
||||
});
|
||||
}
|
||||
35
lib/svgs.js
Normal file
35
lib/svgs.js
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
export const CalendarIcon = () => (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M11 6.5C11 6.36739 11.0527 6.24021 11.1464 6.14645C11.2402 6.05268 11.3674 6 11.5 6H12.5C12.6326 6 12.7598 6.05268 12.8536 6.14645C12.9473 6.24021 13 6.36739 13 6.5V7.5C13 7.63261 12.9473 7.75979 12.8536 7.85355C12.7598 7.94732 12.6326 8 12.5 8H11.5C11.3674 8 11.2402 7.94732 11.1464 7.85355C11.0527 7.75979 11 7.63261 11 7.5V6.5Z"
|
||||
fill="#FCCFB9"
|
||||
/>
|
||||
<path
|
||||
d="M3.5 0C3.63261 0 3.75979 0.0526784 3.85355 0.146447C3.94732 0.240215 4 0.367392 4 0.5V1H12V0.5C12 0.367392 12.0527 0.240215 12.1464 0.146447C12.2402 0.0526784 12.3674 0 12.5 0C12.6326 0 12.7598 0.0526784 12.8536 0.146447C12.9473 0.240215 13 0.367392 13 0.5V1H14C14.5304 1 15.0391 1.21071 15.4142 1.58579C15.7893 1.96086 16 2.46957 16 3V14C16 14.5304 15.7893 15.0391 15.4142 15.4142C15.0391 15.7893 14.5304 16 14 16H2C1.46957 16 0.960859 15.7893 0.585786 15.4142C0.210714 15.0391 0 14.5304 0 14V3C0 2.46957 0.210714 1.96086 0.585786 1.58579C0.960859 1.21071 1.46957 1 2 1H3V0.5C3 0.367392 3.05268 0.240215 3.14645 0.146447C3.24021 0.0526784 3.36739 0 3.5 0V0ZM1 4V14C1 14.2652 1.10536 14.5196 1.29289 14.7071C1.48043 14.8946 1.73478 15 2 15H14C14.2652 15 14.5196 14.8946 14.7071 14.7071C14.8946 14.5196 15 14.2652 15 14V4H1Z"
|
||||
fill="#FCCFB9"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const MapIcon = () => (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M15.817 0.112823C15.8743 0.159759 15.9204 0.218822 15.952 0.285748C15.9837 0.352674 16 0.425792 16 0.499823V14.4998C15.9999 14.6154 15.9598 14.7273 15.8866 14.8167C15.8133 14.906 15.7113 14.9672 15.598 14.9898L10.598 15.9898C10.5333 16.0027 10.4667 16.0027 10.402 15.9898L5.5 15.0098L0.598 15.9898C0.525489 16.0043 0.450665 16.0025 0.378921 15.9846C0.307176 15.9667 0.240296 15.9331 0.183099 15.8863C0.125903 15.8394 0.0798134 15.7804 0.0481518 15.7136C0.0164902 15.6468 4.46527e-05 15.5738 0 15.4998L0 1.49982C6.9782e-05 1.38428 0.0401561 1.27232 0.113443 1.18299C0.186731 1.09366 0.288695 1.03247 0.402 1.00982L5.402 0.00982311C5.46669 -0.00310763 5.53331 -0.00310763 5.598 0.00982311L10.5 0.989823L15.402 0.00982311C15.4745 -0.00476108 15.5493 -0.00308756 15.6211 0.0147231C15.6928 0.0325338 15.7597 0.0660382 15.817 0.112823V0.112823ZM10 1.90982L6 1.10982V14.0898L10 14.8898V1.90982ZM11 14.8898L15 14.0898V1.10982L11 1.90982V14.8898ZM5 14.0898V1.10982L1 1.90982V14.8898L5 14.0898Z"
|
||||
fill="#FCCFB9"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
71
lib/useForm.js
Normal file
71
lib/useForm.js
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
|
||||
export default function useForm(initial = {}) {
|
||||
// Create a state object for our inputs
|
||||
const [inputs, setInputs] = useState(initial);
|
||||
const initialValues = Object.values(initial).join('');
|
||||
|
||||
useEffect(() => {
|
||||
// This function runs when the things we are watching change
|
||||
setInputs(initial);
|
||||
}, [initialValues]);
|
||||
|
||||
function handleChange(e) {
|
||||
let { value, name, type } = e.target;
|
||||
// console.log(value, name, type);
|
||||
if (name.includes('-')) {
|
||||
const values = name.split('-');
|
||||
// console.log(`Values: ${JSON.stringify(values)}`);
|
||||
const [id, property] = values;
|
||||
const data = inputs[id];
|
||||
// console.log(`Data: ${JSON.stringify(data)}`);
|
||||
// console.log(`Value: ${JSON.stringify(value)}`);
|
||||
// console.log(`Property: ${JSON.stringify(property)}`);
|
||||
data[property] = value;
|
||||
|
||||
if (property === 'rsvpStatus' && value !== 'accepted') {
|
||||
// console.log('Setting plus one to false');
|
||||
data.plusOne = false;
|
||||
}
|
||||
|
||||
setInputs({
|
||||
...inputs,
|
||||
[id]: data,
|
||||
});
|
||||
} else {
|
||||
if (type === 'number') {
|
||||
value = parseInt(value);
|
||||
}
|
||||
if (type === 'file') {
|
||||
value[0] = e.target.files;
|
||||
}
|
||||
|
||||
setInputs({
|
||||
// Copy the existing state
|
||||
...inputs,
|
||||
[name]: value,
|
||||
});
|
||||
}
|
||||
|
||||
// console.log(`Inputs after: ${JSON.stringify(inputs)}`);
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
setInputs(initial);
|
||||
}
|
||||
|
||||
function clearForm() {
|
||||
const blankState = Object.fromEntries(
|
||||
Object.entries(inputs).map(([key, value]) => [key, ''])
|
||||
);
|
||||
setInputs(blankState);
|
||||
}
|
||||
|
||||
// Return the things we want to surface from this custom hook
|
||||
return {
|
||||
inputs,
|
||||
handleChange,
|
||||
resetForm,
|
||||
clearForm,
|
||||
};
|
||||
}
|
||||
16
lib/useModal.js
Normal file
16
lib/useModal.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { useState } from 'react';
|
||||
|
||||
const useModal = () => {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
function toggleModal() {
|
||||
setIsVisible(!isVisible);
|
||||
}
|
||||
|
||||
return {
|
||||
isVisible,
|
||||
toggleModal,
|
||||
};
|
||||
};
|
||||
|
||||
export default useModal;
|
||||
27
lib/useUser.js
Normal file
27
lib/useUser.js
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { useEffect } from 'react';
|
||||
import Router from 'next/router';
|
||||
import useSWR from 'swr';
|
||||
|
||||
export default function useUser({
|
||||
redirectTo = false,
|
||||
redirectIfFound = false,
|
||||
} = {}) {
|
||||
const { data: user, mutate: mutateUser } = useSWR('/api/user');
|
||||
|
||||
useEffect(() => {
|
||||
// if no redirect needed, just return (example: already on /dashboard)
|
||||
// if user data not yet there (fetch in progress, logged in or not) then don't do anything yet
|
||||
if (!redirectTo || !user) return;
|
||||
|
||||
if (
|
||||
// If redirectTo is set, redirect if the user was not found.
|
||||
(redirectTo && !redirectIfFound && !user?.isLoggedIn) ||
|
||||
// If redirectIfFound is also set, redirect if the user was found
|
||||
(redirectIfFound && user?.isLoggedIn)
|
||||
) {
|
||||
Router.push(redirectTo);
|
||||
}
|
||||
}, [user, redirectIfFound, redirectTo]);
|
||||
|
||||
return { user, mutateUser };
|
||||
}
|
||||
7
lib/utils.js
Normal file
7
lib/utils.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export async function copyToClipboard(textToCopy) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(textToCopy);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
20
models/Group.js
Normal file
20
models/Group.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import mongoose from 'mongoose';
|
||||
|
||||
const { Schema } = mongoose;
|
||||
|
||||
const groupSchema = new Schema({
|
||||
name: String,
|
||||
note: String,
|
||||
rsvpSubmitted: {
|
||||
type: Boolean,
|
||||
deafult: false,
|
||||
},
|
||||
guests: [
|
||||
{
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Guest',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export default mongoose.models.Group || mongoose.model('Group', groupSchema);
|
||||
26
models/Guest.js
Normal file
26
models/Guest.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import mongoose from 'mongoose';
|
||||
|
||||
const { Schema } = mongoose;
|
||||
|
||||
const guestSchema = new Schema({
|
||||
firstName: { type: String, required: true },
|
||||
lastName: { type: String, required: true },
|
||||
role: String,
|
||||
rsvpStatus: {
|
||||
type: String,
|
||||
enum: ['invited', 'accepted', 'declined'],
|
||||
default: 'invited',
|
||||
},
|
||||
dietaryNotes: String,
|
||||
songRequests: String,
|
||||
hasPlusOne: { type: Boolean, default: false },
|
||||
plusOne: { type: Boolean, default: false },
|
||||
plusOneFirstName: String,
|
||||
plusOneLastName: String,
|
||||
group: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Group.guests',
|
||||
},
|
||||
});
|
||||
|
||||
export default mongoose.models.Guest || mongoose.model('Guest', guestSchema);
|
||||
15
models/User.js
Normal file
15
models/User.js
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import mongoose from 'mongoose';
|
||||
|
||||
const { Schema } = mongoose;
|
||||
|
||||
const UserSchema = new Schema({
|
||||
username: String,
|
||||
password: String,
|
||||
role: {
|
||||
type: String,
|
||||
enum: ['admin', 'guest'],
|
||||
default: 'guest',
|
||||
},
|
||||
});
|
||||
|
||||
export default mongoose.models.User || mongoose.model('User', UserSchema);
|
||||
5
next.config.js
Normal file
5
next.config.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
module.exports = {
|
||||
images: {
|
||||
domains: ['res.cloudinary.com', 'via.placeholder.com'],
|
||||
},
|
||||
};
|
||||
14745
package-lock.json
generated
Normal file
14745
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
111
package.json
Normal file
111
package.json
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
{
|
||||
"name": "nextjs",
|
||||
"version": "0.1.0",
|
||||
"description": "Wedding Website",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "NODE_OPTIONS='--inspect' next dev",
|
||||
"build": "next build",
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"babel-core": "^6.26.3",
|
||||
"babel-plugin-styled-components": "^1.12.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"cloudinary-build-url": "^0.2.1",
|
||||
"dotenv": "^10.0.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"mongodb": "^3.6.8",
|
||||
"mongoose": "^5.12.11",
|
||||
"next": "^10.2.3",
|
||||
"next-iron-session": "^4.1.13",
|
||||
"next-with-apollo": "^5.1.1",
|
||||
"normalize.css": "^8.0.1",
|
||||
"nprogress": "^0.2.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-icons": "^4.2.0",
|
||||
"styled-components": "^5.3.0",
|
||||
"swr": "^0.5.6",
|
||||
"waait": "^1.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.13.13",
|
||||
"@babel/preset-env": "^7.14.2",
|
||||
"@typescript-eslint/eslint-plugin": "^4.25.0",
|
||||
"@typescript-eslint/parser": "^4.25.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"eslint": "^7.27.0",
|
||||
"eslint-config-airbnb": "^18.2.1",
|
||||
"eslint-config-prettier": "^7.1.0",
|
||||
"eslint-config-wesbos": "^2.0.0-beta.5",
|
||||
"eslint-plugin-html": "^6.1.2",
|
||||
"eslint-plugin-import": "^2.23.3",
|
||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
||||
"eslint-plugin-prettier": "^3.4.0",
|
||||
"eslint-plugin-react": "^7.23.2",
|
||||
"eslint-plugin-react-hooks": "^4.2.0",
|
||||
"prettier": "^2.3.0",
|
||||
"typescript": "^4.2.4"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"wesbos"
|
||||
]
|
||||
},
|
||||
"//": "This is our babel config, I prefer this over a .babelrc file",
|
||||
"babel": {
|
||||
"env": {
|
||||
"development": {
|
||||
"presets": [
|
||||
"next/babel"
|
||||
],
|
||||
"plugins": [
|
||||
[
|
||||
"styled-components",
|
||||
{
|
||||
"ssr": true,
|
||||
"displayName": true
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"production": {
|
||||
"presets": [
|
||||
"next/babel"
|
||||
],
|
||||
"plugins": [
|
||||
[
|
||||
"styled-components",
|
||||
{
|
||||
"ssr": true,
|
||||
"displayName": true
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"test": {
|
||||
"presets": [
|
||||
[
|
||||
"next/babel",
|
||||
{
|
||||
"preset-env": {
|
||||
"modules": "commonjs"
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"plugins": [
|
||||
[
|
||||
"styled-components",
|
||||
{
|
||||
"ssr": true,
|
||||
"displayName": true
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
14
pages/404.js
Normal file
14
pages/404.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import Head from 'next/head';
|
||||
|
||||
export default function FourOhFourPage() {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>404 - Page not found</title>
|
||||
</Head>
|
||||
<h1>Sorry page not found!</h1>
|
||||
<p>404.</p>
|
||||
<p>You just hit a route that doesn't exist.</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
51
pages/_app.js
Normal file
51
pages/_app.js
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import Head from 'next/head';
|
||||
import NProgress from 'nprogress';
|
||||
import { Router } from 'next/router';
|
||||
import { SWRConfig } from 'swr';
|
||||
import Page from '../components/Page';
|
||||
import '../components/styles/nprogress.css';
|
||||
import fetch from '../lib/fetchJson';
|
||||
|
||||
Router.events.on('routeChangeStart', () => NProgress.start());
|
||||
Router.events.on('routeChangeComplete', () => NProgress.done());
|
||||
Router.events.on('routeChangeError', () => NProgress.done());
|
||||
|
||||
function MyApp({ Component, pageProps }) {
|
||||
return (
|
||||
<SWRConfig
|
||||
value={{
|
||||
fetcher: fetch,
|
||||
onError: (err) => {
|
||||
console.error(err);
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Page>
|
||||
<Head>
|
||||
<link rel="icon" type="image/svg" href="/penguin.svg" />
|
||||
{/* meta tags */}
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="description" content="Wedding Website" />
|
||||
<meta name="theme-color" content="#FCCFB9" />
|
||||
<meta name="robots" content="noindex" />
|
||||
<meta name="googlebot" content="noindex" />
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/Josefin_Sans/static/JosefinSans-Regular.ttf"
|
||||
as="font"
|
||||
crossOrigin=""
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/Josefin_Sans/static/JosefinSans-Bold.ttf"
|
||||
as="font"
|
||||
crossOrigin=""
|
||||
/>
|
||||
</Head>
|
||||
<Component {...pageProps} />
|
||||
</Page>
|
||||
</SWRConfig>
|
||||
);
|
||||
}
|
||||
|
||||
export default MyApp;
|
||||
25
pages/_document.js
Normal file
25
pages/_document.js
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import Document, { Html, Head, NextScript, Main } from 'next/document';
|
||||
import { ServerStyleSheet } from 'styled-components';
|
||||
|
||||
export default class MyDocument extends Document {
|
||||
static getInitialProps({ renderPage }) {
|
||||
const sheet = new ServerStyleSheet();
|
||||
const page = renderPage((App) => (props) =>
|
||||
sheet.collectStyles(<App {...props} />)
|
||||
);
|
||||
const styleTags = sheet.getStyleElement();
|
||||
return { ...page, styleTags };
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Html lang="en-US">
|
||||
<Head />
|
||||
<body>
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
}
|
||||
93
pages/api/group.js
Normal file
93
pages/api/group.js
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import withSession from '../../lib/session';
|
||||
import Group from '../../models/Group';
|
||||
import Guest from '../../models/Guest';
|
||||
import connectDb from '../../utils/db';
|
||||
|
||||
export default withSession(async (req, res) => {
|
||||
const {
|
||||
query: { id },
|
||||
method,
|
||||
body,
|
||||
session,
|
||||
} = req;
|
||||
|
||||
const user = session.get('user');
|
||||
|
||||
if (!user?.isLoggedIn) {
|
||||
res.status(401).end();
|
||||
return;
|
||||
}
|
||||
|
||||
await connectDb();
|
||||
|
||||
// const { id: groupId } = await req.body;
|
||||
// console.log(`groupId: ${groupId}`);
|
||||
|
||||
const response = {};
|
||||
|
||||
switch (method) {
|
||||
case 'GET':
|
||||
try {
|
||||
const group = await Group.findById(id);
|
||||
// console.log('group', group);
|
||||
response.id = id;
|
||||
const guestList = [];
|
||||
for (const guestId of group?.guests) {
|
||||
// console.log(JSON.stringify(guestId));
|
||||
const guestData = await Guest.findById(guestId);
|
||||
const guest = {
|
||||
id: guestData.id,
|
||||
firstName: guestData.firstName,
|
||||
lastName: guestData.lastName,
|
||||
role: guestData.role,
|
||||
rsvpStatus: guestData.rsvpStatus || '',
|
||||
dietaryNotes: guestData.dietaryNotes || '',
|
||||
songRequests: guestData.songRequests || '',
|
||||
};
|
||||
guestList.push(guest);
|
||||
}
|
||||
response.guests = guestList;
|
||||
response.note = group?.note || '';
|
||||
// console.log('response', response);
|
||||
res.status(200).json(JSON.stringify(response));
|
||||
} catch (error) {
|
||||
const { response: fetchResponse } = error;
|
||||
res.status(fetchResponse?.status || 500).json(error.data);
|
||||
}
|
||||
break;
|
||||
case 'POST':
|
||||
try {
|
||||
const { groupId, guests, note } = body;
|
||||
for (const guest of guests) {
|
||||
// console.log(`Updating ${guest.id} with status ${guest.rsvpStatus}`);
|
||||
const guestData = await Guest.findById(guest.id);
|
||||
const accepted = guest?.rsvpStatus === 'accepted';
|
||||
guestData.rsvpStatus =
|
||||
guest?.rsvpStatus !== 'invited' ? guest?.rsvpStatus : 'invited';
|
||||
guestData.dietaryNotes = guest?.dietaryNotes;
|
||||
guestData.songRequests = guest?.songRequests;
|
||||
guestData.plusOne =
|
||||
(guestData?.hasPlusOne && guest?.plusOne && accepted) || false;
|
||||
guestData.plusOneFirstName =
|
||||
(guestData?.hasPlusOne && guest?.plusOneFirstName) || '';
|
||||
guestData.plusOneLastName =
|
||||
(guestData?.hasPlusOne && guest?.plusOneLastName) || '';
|
||||
guestData.save();
|
||||
}
|
||||
await Group.findByIdAndUpdate(groupId, {
|
||||
note,
|
||||
});
|
||||
res.status(200).json(JSON.stringify({ message: 'SUCCESS' }));
|
||||
} catch (error) {
|
||||
const { response: fetchResponse } = error;
|
||||
console.error('error', error);
|
||||
res
|
||||
.status(fetchResponse?.status || 500)
|
||||
.json({ message: 'Unable to RSVP Your Group' });
|
||||
}
|
||||
break;
|
||||
default:
|
||||
res.status(400).json({ message: 'Unable to RSVP Your Group' });
|
||||
break;
|
||||
}
|
||||
});
|
||||
38
pages/api/guest.js
Normal file
38
pages/api/guest.js
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import withSession from '../../lib/session';
|
||||
import Guest from '../../models/Guest';
|
||||
import connectDb from '../../utils/db';
|
||||
|
||||
export default withSession(async (req, res) => {
|
||||
const {
|
||||
query: { id },
|
||||
method,
|
||||
session,
|
||||
} = req;
|
||||
|
||||
const user = session.get('user');
|
||||
|
||||
if (!user?.isLoggedIn) {
|
||||
res.status(401).end();
|
||||
return;
|
||||
}
|
||||
|
||||
await connectDb();
|
||||
|
||||
switch (method) {
|
||||
case 'GET':
|
||||
try {
|
||||
const guest = await Guest.findById(id);
|
||||
if (!guest) {
|
||||
return res.status(400).json({ success: false });
|
||||
}
|
||||
res.status(200).json({ success: true, data: guest });
|
||||
} catch (error) {
|
||||
const { response: fetchResponse } = error;
|
||||
res.status(fetchResponse?.status || 500).json(error.data);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
res.status(400).json({ success: false });
|
||||
break;
|
||||
}
|
||||
});
|
||||
36
pages/api/login.js
Normal file
36
pages/api/login.js
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import bcrypt from 'bcryptjs';
|
||||
import withSession from '../../lib/session';
|
||||
import connectDb from '../../utils/db';
|
||||
import User from '../../models/User';
|
||||
|
||||
const { compare } = bcrypt;
|
||||
|
||||
export default withSession(async (req, res) => {
|
||||
const { username, password, penguin } = await req.body;
|
||||
await connectDb();
|
||||
|
||||
try {
|
||||
if (username && password && penguin && penguin === 'penguin') {
|
||||
let isAuthorized = false;
|
||||
const userData = await User.findOne({ username });
|
||||
const savedPassword = userData?.password || '';
|
||||
isAuthorized = await compare(password, savedPassword);
|
||||
if (isAuthorized) {
|
||||
const user = { isLoggedIn: isAuthorized, id: userData._id };
|
||||
req.session.set('user', user);
|
||||
await req.session.save();
|
||||
res.json(user);
|
||||
} else {
|
||||
res.status(400).json({ message: 'Unable to login' });
|
||||
}
|
||||
} else {
|
||||
res.status(400).json({ message: 'Unable to login' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
const { response: fetchResponse } = error;
|
||||
res
|
||||
.status(fetchResponse?.status || 500)
|
||||
.json({ message: 'Unable to login' });
|
||||
}
|
||||
});
|
||||
6
pages/api/logout.js
Normal file
6
pages/api/logout.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import withSession from '../../lib/session';
|
||||
|
||||
export default withSession(async (req, res) => {
|
||||
req.session.destroy();
|
||||
res.json({ isLoggedIn: false });
|
||||
});
|
||||
38
pages/api/permissions.js
Normal file
38
pages/api/permissions.js
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import withSession from '../../lib/session';
|
||||
import User from '../../models/User';
|
||||
|
||||
const rootDomain = process.env.ROOT_DOMAIN;
|
||||
const protectedRoutes = [`${rootDomain}/register`, `${rootDomain}/createguest`];
|
||||
|
||||
export default async function permissions(req, res) {
|
||||
const { method, session } = req;
|
||||
|
||||
const user = session.get('user');
|
||||
|
||||
if (!user?.isLoggedIn) {
|
||||
res.status(401).end();
|
||||
return;
|
||||
}
|
||||
|
||||
// const user = req.session.get('user');
|
||||
// console.log(JSON.stringify(req.body));
|
||||
res.status(200).json({});
|
||||
// if (user) {
|
||||
// const dbUser = await User.findOne({ _id: user.id });
|
||||
// const { role } = dbUser;
|
||||
// const referrer = req?.headers?.referrer;
|
||||
// let permitted = false;
|
||||
// if (protectedRoutes.includes(referrer)) {
|
||||
// if (role === 'admin') {
|
||||
// permitted = true;
|
||||
// }
|
||||
// }
|
||||
// res.json({
|
||||
// permitted,
|
||||
// });
|
||||
// } else {
|
||||
// res.json({
|
||||
// permissted: false,
|
||||
// });
|
||||
// }
|
||||
}
|
||||
27
pages/api/register.js
Normal file
27
pages/api/register.js
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { genSalt, hash } from 'bcryptjs';
|
||||
import withSession from '../../lib/session';
|
||||
import connectDb from '../../utils/db.js';
|
||||
import User from '../../models/User';
|
||||
|
||||
export default withSession(async (req, res) => {
|
||||
const { username, password } = await req.body;
|
||||
await connectDb();
|
||||
|
||||
const salt = await genSalt(10);
|
||||
const hashedPassword = await hash(password, salt);
|
||||
|
||||
try {
|
||||
const result = await User.create({
|
||||
username,
|
||||
password: hashedPassword,
|
||||
role: 'guest',
|
||||
});
|
||||
const user = { isLoggedIn: true, id: result?._id };
|
||||
req.session.set('user', user);
|
||||
await req.session.save();
|
||||
res.status(201).json({ success: true });
|
||||
} catch (error) {
|
||||
const { response: fetchResponse } = error;
|
||||
res.status(fetchResponse?.status || 500).json(error.data);
|
||||
}
|
||||
});
|
||||
34
pages/api/rsvp.js
Normal file
34
pages/api/rsvp.js
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import withSession from '../../lib/session';
|
||||
import connectDb from '../../utils/db.js';
|
||||
import Guest from '../../models/Guest';
|
||||
|
||||
export default withSession(async (req, res) => {
|
||||
const {
|
||||
query: { id },
|
||||
method,
|
||||
session,
|
||||
} = req;
|
||||
|
||||
const user = session.get('user');
|
||||
|
||||
if (!user?.isLoggedIn) {
|
||||
res.status(401).end();
|
||||
return;
|
||||
}
|
||||
|
||||
// const { method } = req;
|
||||
await connectDb();
|
||||
const { firstName, lastName } = await req.body;
|
||||
|
||||
try {
|
||||
const result = await Guest.findOne({
|
||||
firstName: { $regex: new RegExp(firstName.trim(), 'i') },
|
||||
lastName: { $regex: new RegExp(lastName.trim(), 'i') },
|
||||
});
|
||||
// console.log(JSON.stringify(result));
|
||||
res.status(200).json({ status: 'SUCCESS', groupId: result.group });
|
||||
} catch (error) {
|
||||
const { response: fetchResponse } = error;
|
||||
res.status(fetchResponse?.status || 500).json({ status: 'FAILURE' });
|
||||
}
|
||||
});
|
||||
18
pages/api/user.js
Normal file
18
pages/api/user.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import withSession from '../../lib/session';
|
||||
|
||||
export default withSession(async (req, res) => {
|
||||
const user = req.session.get('user');
|
||||
|
||||
if (user) {
|
||||
// in a real world application you might read the user id from the session and then do a database request
|
||||
// to get more information on the user if needed
|
||||
res.json({
|
||||
isLoggedIn: true,
|
||||
...user,
|
||||
});
|
||||
} else {
|
||||
res.json({
|
||||
isLoggedIn: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
80
pages/cats.js
Normal file
80
pages/cats.js
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import Head from 'next/head';
|
||||
import { RiExternalLinkLine } from 'react-icons/ri';
|
||||
import CustomNextImage from '../components/CustomNextImage';
|
||||
import Layout from '../components/Layout';
|
||||
import useUser from '../lib/useUser';
|
||||
import { PhotoPageStyles, PhotosStyles } from './photos';
|
||||
|
||||
export default function CatsPage() {
|
||||
const { user } = useUser({ redirectTo: '/login' });
|
||||
|
||||
if (!user || user.isLoggedIn === false) {
|
||||
return <Layout>Loading...</Layout>;
|
||||
}
|
||||
|
||||
const cats = [
|
||||
{
|
||||
url: 'https://via.placeholder.com/500x500.png',
|
||||
alt: 'Cat 1',
|
||||
},
|
||||
{
|
||||
url: 'https://via.placeholder.com/500x500.png',
|
||||
alt: 'Cat 2',
|
||||
},
|
||||
{
|
||||
url: 'https://via.placeholder.com/500x500.png',
|
||||
alt: 'Cat 3',
|
||||
},
|
||||
{
|
||||
url: 'https://via.placeholder.com/500x500.png',
|
||||
alt: 'Cat 4',
|
||||
},
|
||||
{
|
||||
url: 'https://via.placeholder.com/500x500.png',
|
||||
alt: 'Cat 5',
|
||||
},
|
||||
{
|
||||
url: 'https://via.placeholder.com/500x500.png',
|
||||
alt: 'Cat 6',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Head>
|
||||
<title key="title">N & N | Our Cats</title>
|
||||
</Head>
|
||||
<PhotoPageStyles className="center">
|
||||
<h1>Photos</h1>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
aria-label="Link to Photo Gallery"
|
||||
href="https://example.com"
|
||||
>
|
||||
Link to full photo gallery <RiExternalLinkLine />
|
||||
</a>
|
||||
<PhotosStyles>
|
||||
{cats.map((cat) => (
|
||||
<CustomNextImage
|
||||
key={cat.url}
|
||||
src={cat.url}
|
||||
alt={cat.alt}
|
||||
height={500}
|
||||
width={500}
|
||||
blur
|
||||
/>
|
||||
))}
|
||||
</PhotosStyles>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
aria-label="Link to Photo Gallery"
|
||||
href="https://example.com"
|
||||
>
|
||||
Link to full photo gallery <RiExternalLinkLine />
|
||||
</a>
|
||||
</PhotoPageStyles>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
28
pages/home.js
Normal file
28
pages/home.js
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import Head from 'next/head';
|
||||
import styled from 'styled-components';
|
||||
import Layout from '../components/Layout';
|
||||
import useUser from '../lib/useUser';
|
||||
|
||||
const HomeStyles = styled.div`
|
||||
display: grid;
|
||||
gap: 2rem;
|
||||
`;
|
||||
|
||||
export default function HomePage() {
|
||||
const { user } = useUser({ redirectTo: '/login' });
|
||||
|
||||
if (!user || user.isLoggedIn === false) {
|
||||
return <Layout>Loading...</Layout>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>N & N - Wedding</title>
|
||||
</Head>
|
||||
<HomeStyles>
|
||||
<h1>Welcome!</h1>
|
||||
</HomeStyles>
|
||||
</>
|
||||
);
|
||||
}
|
||||
36
pages/index.js
Normal file
36
pages/index.js
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import styled from 'styled-components';
|
||||
import Head from 'next/head';
|
||||
import useUser from '../lib/useUser';
|
||||
import HomeContent from '../components/HomeContent';
|
||||
import connectDb from '../utils/db';
|
||||
import Login from '../components/Login';
|
||||
import Layout from '../components/Layout';
|
||||
|
||||
const LandingStyles = styled.div`
|
||||
display: grid;
|
||||
`;
|
||||
|
||||
export default function Home() {
|
||||
const { user, mutateUser } = useUser();
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<Layout>
|
||||
<h1>Loading...</h1>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<LandingStyles>
|
||||
<Head>
|
||||
<title key="title">N & N | Wedding</title>
|
||||
</Head>
|
||||
{user && user.isLoggedIn === true ? <HomeContent /> : <Login />}
|
||||
</LandingStyles>
|
||||
);
|
||||
}
|
||||
|
||||
export async function getServerSideProps() {
|
||||
return { props: {} };
|
||||
}
|
||||
5
pages/login.js
Normal file
5
pages/login.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import Login from '../components/Login';
|
||||
|
||||
export default function LoginPage() {
|
||||
return <Login />;
|
||||
}
|
||||
35
pages/logout.js
Normal file
35
pages/logout.js
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { Router, useRouter } from 'next/router';
|
||||
import Layout from '../components/Layout';
|
||||
import fetchJson from '../lib/fetchJson';
|
||||
import useUser from '../lib/useUser';
|
||||
|
||||
async function logout(router) {
|
||||
try {
|
||||
fetchJson('/api/logout', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({}),
|
||||
}).then((e) => {
|
||||
router.push({
|
||||
pathname: `/`,
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
console.error();
|
||||
}
|
||||
}
|
||||
|
||||
export default function LogoutPage() {
|
||||
const router = useRouter();
|
||||
const { user } = useUser({ redirectTo: '/login' });
|
||||
|
||||
if (!user || user.isLoggedIn === false) {
|
||||
return <Layout>loading...</Layout>;
|
||||
}
|
||||
|
||||
return (
|
||||
<button type="button" onClick={() => logout(router)}>
|
||||
LOGOUT
|
||||
</button>
|
||||
);
|
||||
}
|
||||
145
pages/party.js
Normal file
145
pages/party.js
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
import Head from 'next/head';
|
||||
import styled from 'styled-components';
|
||||
import Layout from '../components/Layout';
|
||||
import useUser from '../lib/useUser';
|
||||
import CustomNextImage from '../components/CustomNextImage';
|
||||
|
||||
const PartyPageStyles = styled.div`
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
gap: 1.5rem;
|
||||
|
||||
img,
|
||||
figure {
|
||||
border-radius: 4px;
|
||||
}
|
||||
`;
|
||||
|
||||
const PartyStyles = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1.5rem;
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
@media (max-width: 650px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
`;
|
||||
|
||||
const PartyCard = styled.div`
|
||||
display: grid;
|
||||
h3 {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const weddingParty = [
|
||||
{
|
||||
name: 'Best Man',
|
||||
title: 'Best Man',
|
||||
imageUrl: '',
|
||||
},
|
||||
{
|
||||
name: 'Man/Maid of Honor',
|
||||
title: 'Man/Maid of Honor',
|
||||
imageUrl: '',
|
||||
},
|
||||
{
|
||||
name: 'Groomsman',
|
||||
title: 'Groomsman',
|
||||
imageUrl: '',
|
||||
},
|
||||
{
|
||||
name: 'Bridesmaid',
|
||||
title: 'Bridesmaid',
|
||||
imageUrl: '',
|
||||
},
|
||||
{
|
||||
name: 'Groomsman',
|
||||
title: 'Groomsman',
|
||||
imageUrl: '',
|
||||
},
|
||||
{
|
||||
name: 'Bridesmaid',
|
||||
title: 'Bridesmaid',
|
||||
imageUrl: '',
|
||||
},
|
||||
{
|
||||
name: 'Groomsman',
|
||||
title: 'Groomsman',
|
||||
imageUrl: '',
|
||||
},
|
||||
{
|
||||
name: 'Bridesmaid',
|
||||
title: 'Bridesmaid',
|
||||
imageUrl: '',
|
||||
},
|
||||
];
|
||||
|
||||
export default function PartyPage() {
|
||||
const { user } = useUser({ redirectTo: '/login' });
|
||||
|
||||
if (!user || user.isLoggedIn === false) {
|
||||
return <Layout>Loading...</Layout>;
|
||||
}
|
||||
|
||||
return (
|
||||
<PartyPageStyles>
|
||||
<Head>
|
||||
<title key="title">N & N | Wedding Party</title>
|
||||
</Head>
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
}}
|
||||
>
|
||||
<h1 className="center">Meet our Wedding Party</h1>
|
||||
<CustomNextImage
|
||||
src="https://via.placeholder.com/450X800.png"
|
||||
alt="Wedding Part"
|
||||
height={450}
|
||||
width={800}
|
||||
/>
|
||||
</div>
|
||||
<h2 className="center">The Party</h2>
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'minmax(300px, 400px)',
|
||||
justifyContent: 'center',
|
||||
margin: '1rem 0',
|
||||
}}
|
||||
>
|
||||
<PartyCard className="card">
|
||||
<h2 className="center">Officiant</h2>
|
||||
<CustomNextImage
|
||||
src="https://via.placeholder.com/1200x1600.png"
|
||||
alt="Wedding Officiant"
|
||||
objectFit="cover"
|
||||
width="1200"
|
||||
height="1600"
|
||||
/>
|
||||
<h3 className="center">Wedding Officiant</h3>
|
||||
</PartyCard>
|
||||
</div>
|
||||
<PartyStyles>
|
||||
{weddingParty.map((party, index) => (
|
||||
<PartyCard className="card" key={index}>
|
||||
<h2 className="center">{party.name}</h2>
|
||||
<CustomNextImage
|
||||
src="https://via.placeholder.com/1200x1600.png"
|
||||
alt={`${party.name} - ${party.title}`}
|
||||
objectFit="cover"
|
||||
width="1200"
|
||||
height="1600"
|
||||
/>
|
||||
<h3 className="center">{party.title}</h3>
|
||||
</PartyCard>
|
||||
))}
|
||||
</PartyStyles>
|
||||
</PartyPageStyles>
|
||||
);
|
||||
}
|
||||
113
pages/photos.js
Normal file
113
pages/photos.js
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
import Head from 'next/head';
|
||||
import { RiExternalLinkLine } from 'react-icons/ri';
|
||||
import styled from 'styled-components';
|
||||
import CustomNextCloudinaryImage from '../components/CustomNextCloudinaryImage';
|
||||
import Layout from '../components/Layout';
|
||||
import useUser from '../lib/useUser';
|
||||
|
||||
export const PhotoPageStyles = styled.div`
|
||||
display: grid;
|
||||
gap: 1.5rem;
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
`;
|
||||
|
||||
export const PhotosStyles = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-gap: 1.5rem;
|
||||
margin: 0.5rem auto;
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
@media (max-width: 650px) {
|
||||
grid-template-columns: repeat(1, minmax(150px, 2000px));
|
||||
grid-gap: 1.2rem;
|
||||
margin: 0.2rem 0;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
img,
|
||||
figure {
|
||||
border-radius: 4px;
|
||||
box-shadow: var(--level-1);
|
||||
}
|
||||
`;
|
||||
|
||||
export default function PhotosPage() {
|
||||
const { user } = useUser({ redirectTo: '/login' });
|
||||
|
||||
if (!user || user.isLoggedIn === false) {
|
||||
return <Layout>Loading...</Layout>;
|
||||
}
|
||||
|
||||
const photos = [
|
||||
{
|
||||
url: 'https://via.placeholder.com/1000x1000.png',
|
||||
alt: 'Photo 1',
|
||||
},
|
||||
{
|
||||
url: 'https://via.placeholder.com/1000x1000.png',
|
||||
alt: 'Photo 2',
|
||||
},
|
||||
{
|
||||
url: 'https://via.placeholder.com/1000x1000.png',
|
||||
alt: 'Photo 3',
|
||||
},
|
||||
{
|
||||
url: 'https://via.placeholder.com/1000x1000.png',
|
||||
alt: 'Photo 4',
|
||||
},
|
||||
{
|
||||
url: 'https://via.placeholder.com/1000x1000.png',
|
||||
alt: 'Photo 5',
|
||||
},
|
||||
{
|
||||
url: 'https://via.placeholder.com/1000x1000.png',
|
||||
alt: 'Photo 6',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Head>
|
||||
<title key="title">N & N | Photos</title>
|
||||
</Head>
|
||||
<PhotoPageStyles className="center">
|
||||
<h1>Photos</h1>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
aria-label="Link to Photo Gallery"
|
||||
href="https://example.com"
|
||||
>
|
||||
Link to full photo gallery <RiExternalLinkLine />
|
||||
</a>
|
||||
<PhotosStyles>
|
||||
{photos.map((photo, index) => (
|
||||
<CustomNextCloudinaryImage
|
||||
key={index}
|
||||
src={photo.url}
|
||||
alt={photo.alt}
|
||||
height={1000}
|
||||
width={1000}
|
||||
blur
|
||||
/>
|
||||
))}
|
||||
</PhotosStyles>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
aria-label="Link to Photo Gallery"
|
||||
href="https://example.com"
|
||||
>
|
||||
Link to full photo gallery <RiExternalLinkLine />
|
||||
</a>
|
||||
</PhotoPageStyles>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
37
pages/profile-sg.js
Normal file
37
pages/profile-sg.js
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import useUser from '../lib/useUser'
|
||||
import Layout from '../components/Layout'
|
||||
|
||||
const SgProfile = () => {
|
||||
const { user } = useUser({ redirectTo: '/login' })
|
||||
|
||||
if (!user || user.isLoggedIn === false) {
|
||||
return <Layout>loading...</Layout>
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<h1>Your GitHub profile</h1>
|
||||
<h2>
|
||||
This page uses{' '}
|
||||
<a href="https://nextjs.org/docs/basic-features/pages#static-generation-recommended">
|
||||
Static Generation (SG)
|
||||
</a>{' '}
|
||||
and the <a href="/api/user">/api/user</a> route (using{' '}
|
||||
<a href="https://github.com/zeit/swr">SWR</a>)
|
||||
</h2>
|
||||
|
||||
<p style={{ fontStyle: 'italic' }}>
|
||||
Public data, from{' '}
|
||||
<a href={githubUrl(user.login)}>{githubUrl(user.login)}</a>, reduced to
|
||||
`login` and `avatar_url`.
|
||||
</p>
|
||||
<pre>{JSON.stringify(user, null, 2)}</pre>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
function githubUrl(login) {
|
||||
return `https://api.github.com/users/${login}`
|
||||
}
|
||||
|
||||
export default SgProfile
|
||||
61
pages/profile-ssr.js
Normal file
61
pages/profile-ssr.js
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import Layout from '../components/Layout';
|
||||
import withSession from '../lib/session';
|
||||
|
||||
const SsrProfile = ({ user }) => (
|
||||
<Layout>
|
||||
<h1>Your GitHub profile</h1>
|
||||
<h2>
|
||||
This page uses{' '}
|
||||
<a href="https://nextjs.org/docs/basic-features/pages#server-side-rendering">
|
||||
Server-side Rendering (SSR)
|
||||
</a>{' '}
|
||||
and{' '}
|
||||
<a href="https://nextjs.org/docs/basic-features/data-fetching#getserversideprops-server-side-rendering">
|
||||
getServerSideProps
|
||||
</a>
|
||||
</h2>
|
||||
|
||||
{user?.isLoggedIn && (
|
||||
<>
|
||||
<p style={{ fontStyle: 'italic' }}>
|
||||
Public data, from{' '}
|
||||
<a href={githubUrl(user.login)}>{githubUrl(user.login)}</a>, reduced
|
||||
to `login` and `avatar_url`.
|
||||
</p>
|
||||
<pre>{JSON.stringify(user, null, 2)}</pre>
|
||||
</>
|
||||
)}
|
||||
</Layout>
|
||||
);
|
||||
|
||||
export const getServerSideProps = withSession(async ({ req, res }) => {
|
||||
const user = req.session.get('user');
|
||||
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: '/login',
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
props: { user: req.session.get('user') },
|
||||
};
|
||||
});
|
||||
|
||||
export default SsrProfile;
|
||||
|
||||
function githubUrl(login) {
|
||||
return `https://api.github.com/users/${login}`;
|
||||
}
|
||||
|
||||
SsrProfile.propTypes = {
|
||||
user: PropTypes.shape({
|
||||
isLoggedIn: PropTypes.bool,
|
||||
login: PropTypes.string,
|
||||
avatarUrl: PropTypes.string,
|
||||
}),
|
||||
};
|
||||
57
pages/qanda.js
Normal file
57
pages/qanda.js
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import Head from 'next/head';
|
||||
import Link from 'next/link';
|
||||
import styled from 'styled-components';
|
||||
import Layout from '../components/Layout';
|
||||
import useUser from '../lib/useUser';
|
||||
|
||||
const QAStyles = styled.div`
|
||||
ol > li {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
li {
|
||||
font-weight: bold;
|
||||
}
|
||||
p {
|
||||
color: var(--white);
|
||||
}
|
||||
`;
|
||||
|
||||
export default function QandAPage() {
|
||||
const { user } = useUser({ redirectTo: '/login' });
|
||||
|
||||
if (!user || user.isLoggedIn === false) {
|
||||
return <Layout>Loading...</Layout>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Head>
|
||||
<title key="title">N & N | QA</title>
|
||||
</Head>
|
||||
<h1 className="center">Q & A</h1>
|
||||
<QAStyles>
|
||||
<ol>
|
||||
<li>Question 1</li>
|
||||
<p>
|
||||
Answer 1
|
||||
</p>
|
||||
<li>How do I get to the venue?</li>
|
||||
<p>
|
||||
See more detailed info on our{' '}
|
||||
<Link href="/travelstay">Travel & Stay</Link> page.
|
||||
</p>
|
||||
<li>I still have questions, what is the best way to contact you?</li>
|
||||
<p>
|
||||
If you have any questions not answered by this Q&A feel free to
|
||||
contact Name and Name at{' '}
|
||||
<a href="mailto:name@example.com">name@example.com</a>.
|
||||
</p>
|
||||
</ol>
|
||||
</QAStyles>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
100
pages/register.js
Normal file
100
pages/register.js
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import { useRouter } from 'next/router';
|
||||
import { useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import useForm from '../lib/useForm';
|
||||
import useUser from '../lib/useUser';
|
||||
import fetchJson from '../lib/fetchJson';
|
||||
|
||||
const FormStyles = styled.form`
|
||||
display: grid;
|
||||
|
||||
label {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-top: 0.5rem;
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
export default function RegisterPage() {
|
||||
const router = useRouter();
|
||||
|
||||
const { inputs, handleChange, clearForm, resetForm } = useForm({
|
||||
username: '',
|
||||
password: '',
|
||||
});
|
||||
|
||||
const { mutateUser } = useUser({
|
||||
redirectTo: '/',
|
||||
redirectIfFound: true,
|
||||
});
|
||||
|
||||
const [errorMsg, setErrorMsg] = useState('');
|
||||
|
||||
async function handleSubmit(username, password) {
|
||||
const body = {
|
||||
username,
|
||||
password,
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetchJson('/api/register', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
console.log(res);
|
||||
} catch (error) {
|
||||
console.error('An unexpected error happened:', error);
|
||||
setErrorMsg(error.data.message);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<FormStyles
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
await handleSubmit(inputs.username, inputs.password);
|
||||
if (!errorMsg && errorMsg?.length !== 0) {
|
||||
router.push({
|
||||
pathname: `/`,
|
||||
});
|
||||
} else {
|
||||
console.log(errorMsg);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{errorMsg && <p className="error">Error: {errorMsg}</p>}
|
||||
<fieldset aria-busy={false} disabled={false}>
|
||||
<label htmlFor="username">
|
||||
<span>Username</span>
|
||||
<input
|
||||
required
|
||||
type="text"
|
||||
id="username"
|
||||
name="username"
|
||||
placeholder="Username"
|
||||
value={inputs.username}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</label>
|
||||
<label htmlFor="password">
|
||||
<span>Password</span>
|
||||
<input
|
||||
required
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
placeholder="Password"
|
||||
value={inputs.password}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</label>
|
||||
<button type="submit">Login</button>
|
||||
</fieldset>
|
||||
</FormStyles>
|
||||
);
|
||||
}
|
||||
418
pages/rsvp/[id].js
Normal file
418
pages/rsvp/[id].js
Normal file
|
|
@ -0,0 +1,418 @@
|
|||
import Head from 'next/head';
|
||||
import { useState } from 'react';
|
||||
import styled, { keyframes } from 'styled-components';
|
||||
import GuestRSVP from '../../components/GuestRSVP';
|
||||
import Layout from '../../components/Layout';
|
||||
import useForm from '../../lib/useForm';
|
||||
import useUser from '../../lib/useUser';
|
||||
import fetchJson from '../../lib/fetchJson';
|
||||
import Group from '../../models/Group';
|
||||
import Guest from '../../models/Guest';
|
||||
import connectDb from '../../utils/db';
|
||||
import { CalendarIcon, MapIcon } from '../../lib/svgs';
|
||||
import useModal from '../../lib/useModal';
|
||||
import Modal from '../../components/Modal';
|
||||
|
||||
const RSVPGroupStyles = styled.div`
|
||||
h2 {
|
||||
margin: 2rem 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const loadingFrame = keyframes`
|
||||
from {
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
to {
|
||||
background-position: 100% 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
const FormStyles = styled.form`
|
||||
display: grid;
|
||||
gap: 2rem;
|
||||
margin-top: 3rem;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
height: 10px;
|
||||
content: '';
|
||||
display: block;
|
||||
border-radius: 4px;
|
||||
background-image: linear-gradient(
|
||||
to right,
|
||||
var(--primary) 0%,
|
||||
var(--lightViolet) 50%,
|
||||
var(--primary) 100%
|
||||
);
|
||||
}
|
||||
&[aria-busy='true']::before,
|
||||
&[aria-busy='true']::after {
|
||||
background-size: 50% auto;
|
||||
animation: ${loadingFrame} 0.5s linear infinite;
|
||||
}
|
||||
|
||||
label {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea,
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--primary);
|
||||
&:focus {
|
||||
outline: 0;
|
||||
border-color: var(--lightViolet);
|
||||
}
|
||||
}
|
||||
|
||||
button[type='submit'],
|
||||
input[type='submit'] {
|
||||
width: 100%;
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
padding: 1.2rem 1.2rem;
|
||||
}
|
||||
|
||||
hr {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
height: 0;
|
||||
max-height: 0;
|
||||
border: solid;
|
||||
width: 100%;
|
||||
border-width: thin 0 0 0;
|
||||
transition: inherit;
|
||||
border-color: var(--lightShade);
|
||||
color: var(--lightShade);
|
||||
margin: 2.5rem 0;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
background: var(--lightGray);
|
||||
}
|
||||
|
||||
fieldset {
|
||||
border-radius: 4px;
|
||||
|
||||
&[disabled] {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const AddressStyles = styled.div`
|
||||
button {
|
||||
background: none;
|
||||
color: var(--primary);
|
||||
padding: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const QuestionStyles = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
|
||||
@media (max-width: 600px) {
|
||||
grid-template-columns: auto;
|
||||
}
|
||||
`;
|
||||
|
||||
const ErrorContactStyles = styled.p`
|
||||
font-weight: bold;
|
||||
a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
`;
|
||||
|
||||
export default function SingleGroupPage({ group }) {
|
||||
const { guests, note } = group;
|
||||
const { user } = useUser({ redirectTo: '/login' });
|
||||
const [errorMsg, setErrorMsg] = useState('');
|
||||
const [errorCount, setErrorCount] = useState(0);
|
||||
const [message, setMessage] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [groupHasPlusOne, setGroupHasPlusOne] = useState(false);
|
||||
const { isVisible, toggleModal } = useModal();
|
||||
const address = 'Central Park, New York, New York, USA';
|
||||
|
||||
function getInitialFormData() {
|
||||
const initial = {};
|
||||
for (const guest of guests) {
|
||||
const guestData = {
|
||||
rsvpStatus: guest?.rsvpStatus || '',
|
||||
dietaryNotes: guest?.dietaryNotes || '',
|
||||
songRequests: guest?.songRequests || '',
|
||||
hasPlusOne: guest?.hasPlusOne || false,
|
||||
plusOne: guest?.plusOne || false,
|
||||
plusOneFirstName: guest?.plusOneFirstName || '',
|
||||
plusOneLastName: guest?.plusOneLastName || '',
|
||||
};
|
||||
initial[guest.id] = guestData;
|
||||
if (guest.hasPlusOne) {
|
||||
setGroupHasPlusOne(true);
|
||||
}
|
||||
}
|
||||
initial.note = note || '';
|
||||
return initial;
|
||||
}
|
||||
|
||||
const { inputs, handleChange, clearForm, resetForm } = useForm(
|
||||
getInitialFormData
|
||||
);
|
||||
|
||||
if (!user || user.isLoggedIn === false) {
|
||||
return <Layout>Loading...</Layout>;
|
||||
}
|
||||
|
||||
if (group?.guests?.length === 0) return <p>Loading...</p>;
|
||||
|
||||
async function handleSubmit(groupId) {
|
||||
const keys = Object.keys(inputs);
|
||||
const guestData = [];
|
||||
// console.log(JSON.stringify(inputs));
|
||||
keys.forEach((key, index) => {
|
||||
if (key !== 'note') {
|
||||
guestData.push({
|
||||
id: key,
|
||||
...inputs[key],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const body = {
|
||||
groupId,
|
||||
note: inputs.note,
|
||||
guests: guestData,
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetchJson('/api/group', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (res.message === 'SUCCESS') {
|
||||
setMessage(
|
||||
`Successfully submited your RSVP${
|
||||
body.guests.length > 1 ? 's' : ''
|
||||
}. Don't forget to save the date!!`
|
||||
);
|
||||
toggleModal();
|
||||
} else {
|
||||
setErrorCount(errorCount + 1);
|
||||
setErrorMsg('Unable to RSVP Your Group');
|
||||
}
|
||||
} catch (error) {
|
||||
// console.error('An unexpected error happened:', error);
|
||||
setErrorCount(errorCount + 1);
|
||||
setErrorMsg(error.data.message);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Head>
|
||||
<title key="title">N & N | RSVP</title>
|
||||
</Head>
|
||||
<RSVPGroupStyles>
|
||||
<h2>Wedding SAYING I DO</h2>
|
||||
<div>
|
||||
<a href="/myevents.ics" aria-label="Add to calendar">
|
||||
<CalendarIcon />
|
||||
</a>{' '}
|
||||
Monday, June 3rd, 2030 at 5:00 PM
|
||||
</div>
|
||||
<AddressStyles>
|
||||
<p>
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://share.here.com/p/s-YmI9LTczLjk4MTYxJTJDNDAuNzY0MjclMkMtNzMuOTQ4ODIlMkM0MC44MDA0OTtjPWFkbWluaXN0cmF0aXZlLXJlZ2lvbjtsYXQ9NDAuNzgyMzg7bG9uPS03My45NjUyMTtuPUNlbnRyYWwrUGFyazt6PTE0O2g9MWM3MDIz"
|
||||
rel="noopener noreferrer nofollow"
|
||||
aria-label="Go to map"
|
||||
>
|
||||
<MapIcon />
|
||||
</a>{' '}
|
||||
Wedding Location
|
||||
</p>
|
||||
<p>{address}</p>
|
||||
</AddressStyles>
|
||||
</RSVPGroupStyles>
|
||||
<FormStyles
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
await handleSubmit(group.id);
|
||||
setLoading(false);
|
||||
}}
|
||||
aria-busy={loading}
|
||||
disabled={loading}
|
||||
>
|
||||
{errorMsg && <p className="error">Error: {errorMsg}</p>}
|
||||
{errorMsg && errorCount > 3 && (
|
||||
<ErrorContactStyles>
|
||||
Support contact:{' '}
|
||||
<a
|
||||
href={`mailto:name@example.com?subject=RSVP Group Failed: ${errorMsg} ${group.id}`}
|
||||
>
|
||||
name@example.com
|
||||
</a>
|
||||
</ErrorContactStyles>
|
||||
)}
|
||||
<fieldset aria-busy={loading} disabled={loading}>
|
||||
<legend>RSVP Invitation</legend>
|
||||
{group.guests.map((guest) => (
|
||||
<GuestRSVP
|
||||
key={guest.id}
|
||||
guest={guest}
|
||||
inputs={inputs}
|
||||
handleChange={handleChange}
|
||||
/>
|
||||
))}
|
||||
</fieldset>
|
||||
<fieldset aria-busy={loading} disabled={loading}>
|
||||
<legend>
|
||||
Do you {groupHasPlusOne ? 'or your plus one ' : ''}have any dietary
|
||||
restrictions?
|
||||
</legend>
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gap: '1rem',
|
||||
}}
|
||||
>
|
||||
{group.guests.map((guest) =>
|
||||
!guest.isPlusOne ? (
|
||||
<QuestionStyles key={`${guest?.id}-dietaryNotes`}>
|
||||
<p>
|
||||
{guest.firstName} {guest.lastName} :
|
||||
</p>
|
||||
<label htmlFor={`${guest.id}-dietaryNotes`}>
|
||||
<textarea
|
||||
name={`${guest.id}-dietaryNotes`}
|
||||
id={`${guest.id}-dietaryNotes`}
|
||||
cols="30"
|
||||
rows="2"
|
||||
placeholder="Example: Nut allergy, Fish, etc."
|
||||
value={inputs[guest.id]?.dietaryNotes}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</label>
|
||||
</QuestionStyles>
|
||||
) : (
|
||||
''
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset aria-busy={loading} disabled={loading}>
|
||||
<legend>Do you have any song requests?</legend>
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gap: '1rem',
|
||||
}}
|
||||
>
|
||||
{group.guests.map((guest) =>
|
||||
!guest?.isPlusOne ? (
|
||||
<QuestionStyles key={`${guest?.id}-songRequests`}>
|
||||
<p>
|
||||
{guest.firstName} {guest.lastName}:
|
||||
</p>
|
||||
<label htmlFor={`${guest.id}-songRequests`}>
|
||||
<textarea
|
||||
name={`${guest.id}-songRequests`}
|
||||
id={`${guest.id}-songRequests`}
|
||||
cols="30"
|
||||
rows="2"
|
||||
placeholder="Example: Paint It Black - Rolling Stones"
|
||||
value={inputs[guest.id]?.songRequests}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</label>
|
||||
</QuestionStyles>
|
||||
) : (
|
||||
''
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset aria-busy={loading} disabled={loading}>
|
||||
<legend>Additonal Notes?</legend>
|
||||
<textarea
|
||||
name="note"
|
||||
id="note"
|
||||
cols="30"
|
||||
rows="10"
|
||||
value={inputs.note}
|
||||
onChange={handleChange}
|
||||
placeholder="Anything you want to ask us?"
|
||||
/>
|
||||
</fieldset>
|
||||
<hr />
|
||||
{errorMsg && <p className="error">Error: {errorMsg}</p>}
|
||||
{errorMsg && errorCount > 3 && (
|
||||
<ErrorContactStyles>
|
||||
Support contact:{' '}
|
||||
<a
|
||||
href={`mailto:name@example.com?subject=RSVP Group Failed: ${errorMsg}`}
|
||||
>
|
||||
name@example.com
|
||||
</a>
|
||||
</ErrorContactStyles>
|
||||
)}
|
||||
<button type="submit">Submit RSVP</button>
|
||||
</FormStyles>
|
||||
<Modal isVisible={isVisible} hideModal={toggleModal} title="RSVP Success">
|
||||
<p>{message}</p>
|
||||
<div>
|
||||
<p>Monday, June 3, 2030 at 5:00 PM</p>
|
||||
<a href="/myevents.ics" aria-label="Click to add to calendar">
|
||||
<CalendarIcon /> Add to Calendar
|
||||
</a>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export async function getServerSideProps({ params }) {
|
||||
try {
|
||||
await connectDb();
|
||||
const groupData = await Group.findById(params.id);
|
||||
const group = {};
|
||||
|
||||
group.id = params.id;
|
||||
const guestList = [];
|
||||
for (const guestId of groupData?.guests) {
|
||||
const guestData = await Guest.findById(guestId);
|
||||
const guest = {
|
||||
id: guestData.id,
|
||||
firstName: guestData.firstName,
|
||||
lastName: guestData.lastName,
|
||||
rsvpStatus: guestData.rsvpStatus || '',
|
||||
dietaryNotes: guestData.dietaryNotes || '',
|
||||
songRequests: guestData.songRequests || '',
|
||||
hasPlusOne: guestData.hasPlusOne || false,
|
||||
plusOne: guestData.plusOne || false,
|
||||
plusOneFirstName: guestData.plusOneFirstName || '',
|
||||
plusOneLastName: guestData.plusOneLastName || '',
|
||||
};
|
||||
guestList.push(guest);
|
||||
}
|
||||
group.guests = guestList;
|
||||
group.note = groupData.note || '';
|
||||
|
||||
return { props: { group } };
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return { notFound: true };
|
||||
}
|
||||
}
|
||||
123
pages/rsvp/index.js
Normal file
123
pages/rsvp/index.js
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
import Head from 'next/head';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import Layout from '../../components/Layout';
|
||||
import Form from '../../components/styles/Form';
|
||||
import fetchJson from '../../lib/fetchJson';
|
||||
import useForm from '../../lib/useForm';
|
||||
import useUser from '../../lib/useUser';
|
||||
|
||||
const RSVPStyles = styled.div`
|
||||
display: grid;
|
||||
`;
|
||||
|
||||
const ErrorContactStyles = styled.p`
|
||||
font-weight: bold;
|
||||
a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
`;
|
||||
|
||||
export default function RsvpPage() {
|
||||
const router = useRouter();
|
||||
|
||||
const { inputs, handleChange, clearForm, resetForm } = useForm({
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
});
|
||||
|
||||
const [errorMsg, setErrorMsg] = useState('');
|
||||
const [errorCount, setErrorCount] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const { user, mutateUser } = useUser({ redirectTo: '/login' });
|
||||
|
||||
if (!user || user.isLoggedIn === false) {
|
||||
return <Layout>Loading...</Layout>;
|
||||
}
|
||||
|
||||
async function handleSubmit(firstName, lastName, groupId) {
|
||||
const body = {
|
||||
firstName,
|
||||
lastName,
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetchJson('/api/rsvp', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (res.status === 'SUCCESS') {
|
||||
router.push({
|
||||
pathname: `/rsvp/${res.groupId}`,
|
||||
});
|
||||
} else {
|
||||
setErrorCount(errorCount + 1);
|
||||
setErrorMsg('Unable to RSVP');
|
||||
}
|
||||
} catch (error) {
|
||||
// console.error('An unexpected error happened:', error);
|
||||
setErrorCount(errorCount + 1);
|
||||
setErrorMsg('Unable to RSVP');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<RSVPStyles>
|
||||
<Head>
|
||||
<title key="title">N & N | RSVP</title>
|
||||
</Head>
|
||||
<h1 className="center">RSVP to our wedding</h1>
|
||||
<Form
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
await handleSubmit(inputs.firstName, inputs.lastName);
|
||||
setLoading(false);
|
||||
}}
|
||||
>
|
||||
{errorMsg && <p className="error">Error: {errorMsg}</p>}
|
||||
{errorMsg && errorCount > 3 && (
|
||||
<ErrorContactStyles>
|
||||
Support contact:{' '}
|
||||
<a
|
||||
href={`mailto:name@example.com?subject=RSVP Failed for ${inputs.firstName} ${inputs.lastName}`}
|
||||
>
|
||||
name@example.com
|
||||
</a>
|
||||
</ErrorContactStyles>
|
||||
)}
|
||||
<fieldset aria-busy={loading} disabled={loading}>
|
||||
<label htmlFor="username">
|
||||
<span>First Name</span>
|
||||
<input
|
||||
required
|
||||
type="text"
|
||||
id="firstName"
|
||||
name="firstName"
|
||||
placeholder="First Name"
|
||||
value={inputs.firstName}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</label>
|
||||
<label htmlFor="password">
|
||||
<span>Last Name</span>
|
||||
<input
|
||||
required
|
||||
type="text"
|
||||
id="lastName"
|
||||
name="lastName"
|
||||
placeholder="Last Name"
|
||||
value={inputs.lastName}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</label>
|
||||
<button type="submit">Click to RSVP</button>
|
||||
</fieldset>
|
||||
</Form>
|
||||
</RSVPStyles>
|
||||
);
|
||||
}
|
||||
123
pages/travelstay.js
Normal file
123
pages/travelstay.js
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
import Head from 'next/head';
|
||||
import styled from 'styled-components';
|
||||
import { RiExternalLinkLine } from 'react-icons/ri';
|
||||
import Layout from '../components/Layout';
|
||||
import { MapIcon } from '../lib/svgs';
|
||||
import useUser from '../lib/useUser';
|
||||
|
||||
const TravelAndStayStyles = styled.div`
|
||||
display: grid;
|
||||
gap: 4rem;
|
||||
text-align: center;
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
a.link {
|
||||
text-decoration: underline;
|
||||
color: var(--primary);
|
||||
}
|
||||
`;
|
||||
|
||||
export default function TravelAndStayPage() {
|
||||
const { user } = useUser({ redirectTo: '/login' });
|
||||
|
||||
if (!user || user.isLoggedIn === false) {
|
||||
return <Layout>Loading...</Layout>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Wedding - Travel & Stay</title>
|
||||
</Head>
|
||||
<TravelAndStayStyles>
|
||||
<h1>Travel & Stay</h1>
|
||||
<div>
|
||||
<h2>Traveling to the wedding</h2>
|
||||
<p>
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://share.here.com/p/s-YmI9LTczLjk4MTYxJTJDNDAuNzY0MjclMkMtNzMuOTQ4ODIlMkM0MC44MDA0OTtjPWFkbWluaXN0cmF0aXZlLXJlZ2lvbjtsYXQ9NDAuNzgyMzg7bG9uPS03My45NjUyMTtuPUNlbnRyYWwrUGFyazt6PTE0O2g9MWM3MDIz"
|
||||
rel="noopener noreferrer nofollow"
|
||||
aria-label="Go to map"
|
||||
>
|
||||
<MapIcon />
|
||||
</a>{' '}
|
||||
Central Park
|
||||
</p>
|
||||
<p>Central Park, New York, NY, USA</p>
|
||||
<p>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Link to Example Hotel Website"
|
||||
href="https://hotelexample.com/contact/"
|
||||
>
|
||||
Hotel Website <RiExternalLinkLine />
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://share.here.com/p/s-YmI9LTczLjk4MTYxJTJDNDAuNzY0MjclMkMtNzMuOTQ4ODIlMkM0MC44MDA0OTtjPWFkbWluaXN0cmF0aXZlLXJlZ2lvbjtsYXQ9NDAuNzgyMzg7bG9uPS03My45NjUyMTtuPUNlbnRyYWwrUGFyazt6PTE0O2g9MWM3MDIz"
|
||||
rel="noopener noreferrer nofollow"
|
||||
aria-label="Go to map"
|
||||
className="link"
|
||||
>
|
||||
Get Directions to the hotel
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Hotel Information</h2>
|
||||
<h2>Do I need to book a hotel room?</h2>
|
||||
<p>
|
||||
Answer
|
||||
</p>
|
||||
<p>There are also hotels in the area such as:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<a
|
||||
href="https://www.hotelexample.com/"
|
||||
aria-label="Name of Hotel"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Example 1 <RiExternalLinkLine />
|
||||
</a>
|
||||
</li>
|
||||
<p>Travel Time: X minute drive (X miles)</p>
|
||||
<li>
|
||||
<a
|
||||
href="https://www.hotelexample.com/"
|
||||
aria-label="Name of Hotel"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Example 1 <RiExternalLinkLine />
|
||||
</a>
|
||||
</li>
|
||||
<p>Travel Time: X minute drive (X miles)</p><li>
|
||||
<a
|
||||
href="https://www.hotelexample.com/"
|
||||
aria-label="Name of Hotel"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Example 1 <RiExternalLinkLine />
|
||||
</a>
|
||||
</li>
|
||||
<p>Travel Time: X minute drive (X miles)</p>
|
||||
</ul>
|
||||
</div>
|
||||
</TravelAndStayStyles>
|
||||
</>
|
||||
);
|
||||
}
|
||||
BIN
public/assets/Background.jpeg
Normal file
BIN
public/assets/Background.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 174 KiB |
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
BIN
public/fonts/Istok_Web/IstokWeb-Bold.ttf
Normal file
BIN
public/fonts/Istok_Web/IstokWeb-Bold.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Istok_Web/IstokWeb-BoldItalic.ttf
Normal file
BIN
public/fonts/Istok_Web/IstokWeb-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Istok_Web/IstokWeb-Italic.ttf
Normal file
BIN
public/fonts/Istok_Web/IstokWeb-Italic.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Istok_Web/IstokWeb-Regular.ttf
Normal file
BIN
public/fonts/Istok_Web/IstokWeb-Regular.ttf
Normal file
Binary file not shown.
94
public/fonts/Istok_Web/OFL.txt
Normal file
94
public/fonts/Istok_Web/OFL.txt
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
Copyright (c) 2008-2012, 2014, Andrey V. Panov (panov@canopus.iacp.dvo.ru),
|
||||
with Reserved Font Name Istok.
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Binary file not shown.
BIN
public/fonts/Josefin_Sans/JosefinSans-VariableFont_wght.ttf
Normal file
BIN
public/fonts/Josefin_Sans/JosefinSans-VariableFont_wght.ttf
Normal file
Binary file not shown.
93
public/fonts/Josefin_Sans/OFL.txt
Normal file
93
public/fonts/Josefin_Sans/OFL.txt
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
Copyright (c) 2010, Santiago Orozco (hi@typemade.mx)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
77
public/fonts/Josefin_Sans/README.txt
Normal file
77
public/fonts/Josefin_Sans/README.txt
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
Josefin Sans Variable Font
|
||||
==========================
|
||||
|
||||
This download contains Josefin Sans as both variable fonts and static fonts.
|
||||
|
||||
Josefin Sans is a variable font with this axis:
|
||||
wght
|
||||
|
||||
This means all the styles are contained in these files:
|
||||
Josefin_Sans/JosefinSans-VariableFont_wght.ttf
|
||||
Josefin_Sans/JosefinSans-Italic-VariableFont_wght.ttf
|
||||
|
||||
If your app fully supports variable fonts, you can now pick intermediate styles
|
||||
that aren’t available as static fonts. Not all apps support variable fonts, and
|
||||
in those cases you can use the static font files for Josefin Sans:
|
||||
Josefin_Sans/static/JosefinSans-Thin.ttf
|
||||
Josefin_Sans/static/JosefinSans-ExtraLight.ttf
|
||||
Josefin_Sans/static/JosefinSans-Light.ttf
|
||||
Josefin_Sans/static/JosefinSans-Regular.ttf
|
||||
Josefin_Sans/static/JosefinSans-Medium.ttf
|
||||
Josefin_Sans/static/JosefinSans-SemiBold.ttf
|
||||
Josefin_Sans/static/JosefinSans-Bold.ttf
|
||||
Josefin_Sans/static/JosefinSans-ThinItalic.ttf
|
||||
Josefin_Sans/static/JosefinSans-ExtraLightItalic.ttf
|
||||
Josefin_Sans/static/JosefinSans-LightItalic.ttf
|
||||
Josefin_Sans/static/JosefinSans-Italic.ttf
|
||||
Josefin_Sans/static/JosefinSans-MediumItalic.ttf
|
||||
Josefin_Sans/static/JosefinSans-SemiBoldItalic.ttf
|
||||
Josefin_Sans/static/JosefinSans-BoldItalic.ttf
|
||||
|
||||
Get started
|
||||
-----------
|
||||
|
||||
1. Install the font files you want to use
|
||||
|
||||
2. Use your app's font picker to view the font family and all the
|
||||
available styles
|
||||
|
||||
Learn more about variable fonts
|
||||
-------------------------------
|
||||
|
||||
https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts
|
||||
https://variablefonts.typenetwork.com
|
||||
https://medium.com/variable-fonts
|
||||
|
||||
In desktop apps
|
||||
|
||||
https://theblog.adobe.com/can-variable-fonts-illustrator-cc
|
||||
https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts
|
||||
|
||||
Online
|
||||
|
||||
https://developers.google.com/fonts/docs/getting_started
|
||||
https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide
|
||||
https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts
|
||||
|
||||
Installing fonts
|
||||
|
||||
MacOS: https://support.apple.com/en-us/HT201749
|
||||
Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux
|
||||
Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows
|
||||
|
||||
Android Apps
|
||||
|
||||
https://developers.google.com/fonts/docs/android
|
||||
https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts
|
||||
|
||||
License
|
||||
-------
|
||||
Please read the full license text (OFL.txt) to understand the permissions,
|
||||
restrictions and requirements for usage, redistribution, and modification.
|
||||
|
||||
You can use them freely in your products & projects - print or digital,
|
||||
commercial or otherwise. However, you can't sell the fonts on their own.
|
||||
|
||||
This isn't legal advice, please consider consulting a lawyer and see the full
|
||||
license for all details.
|
||||
BIN
public/fonts/Josefin_Sans/static/JosefinSans-Bold.ttf
Normal file
BIN
public/fonts/Josefin_Sans/static/JosefinSans-Bold.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Josefin_Sans/static/JosefinSans-BoldItalic.ttf
Normal file
BIN
public/fonts/Josefin_Sans/static/JosefinSans-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Josefin_Sans/static/JosefinSans-ExtraLight.ttf
Normal file
BIN
public/fonts/Josefin_Sans/static/JosefinSans-ExtraLight.ttf
Normal file
Binary file not shown.
Binary file not shown.
BIN
public/fonts/Josefin_Sans/static/JosefinSans-Italic.ttf
Normal file
BIN
public/fonts/Josefin_Sans/static/JosefinSans-Italic.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Josefin_Sans/static/JosefinSans-Light.ttf
Normal file
BIN
public/fonts/Josefin_Sans/static/JosefinSans-Light.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Josefin_Sans/static/JosefinSans-LightItalic.ttf
Normal file
BIN
public/fonts/Josefin_Sans/static/JosefinSans-LightItalic.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Josefin_Sans/static/JosefinSans-Medium.ttf
Normal file
BIN
public/fonts/Josefin_Sans/static/JosefinSans-Medium.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Josefin_Sans/static/JosefinSans-MediumItalic.ttf
Normal file
BIN
public/fonts/Josefin_Sans/static/JosefinSans-MediumItalic.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Josefin_Sans/static/JosefinSans-Regular.ttf
Normal file
BIN
public/fonts/Josefin_Sans/static/JosefinSans-Regular.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Josefin_Sans/static/JosefinSans-SemiBold.ttf
Normal file
BIN
public/fonts/Josefin_Sans/static/JosefinSans-SemiBold.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Josefin_Sans/static/JosefinSans-SemiBoldItalic.ttf
Normal file
BIN
public/fonts/Josefin_Sans/static/JosefinSans-SemiBoldItalic.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Josefin_Sans/static/JosefinSans-Thin.ttf
Normal file
BIN
public/fonts/Josefin_Sans/static/JosefinSans-Thin.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Josefin_Sans/static/JosefinSans-ThinItalic.ttf
Normal file
BIN
public/fonts/Josefin_Sans/static/JosefinSans-ThinItalic.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Kanit/Kanit-Black.ttf
Normal file
BIN
public/fonts/Kanit/Kanit-Black.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Kanit/Kanit-BlackItalic.ttf
Normal file
BIN
public/fonts/Kanit/Kanit-BlackItalic.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Kanit/Kanit-Bold.ttf
Normal file
BIN
public/fonts/Kanit/Kanit-Bold.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Kanit/Kanit-BoldItalic.ttf
Normal file
BIN
public/fonts/Kanit/Kanit-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Kanit/Kanit-ExtraBold.ttf
Normal file
BIN
public/fonts/Kanit/Kanit-ExtraBold.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Kanit/Kanit-ExtraBoldItalic.ttf
Normal file
BIN
public/fonts/Kanit/Kanit-ExtraBoldItalic.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Kanit/Kanit-ExtraLight.ttf
Normal file
BIN
public/fonts/Kanit/Kanit-ExtraLight.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Kanit/Kanit-ExtraLightItalic.ttf
Normal file
BIN
public/fonts/Kanit/Kanit-ExtraLightItalic.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Kanit/Kanit-Italic.ttf
Normal file
BIN
public/fonts/Kanit/Kanit-Italic.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Kanit/Kanit-Light.ttf
Normal file
BIN
public/fonts/Kanit/Kanit-Light.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Kanit/Kanit-LightItalic.ttf
Normal file
BIN
public/fonts/Kanit/Kanit-LightItalic.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Kanit/Kanit-Medium.ttf
Normal file
BIN
public/fonts/Kanit/Kanit-Medium.ttf
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue