Adding wedding website files.

This commit is contained in:
Bradley Shellnut 2021-06-03 17:58:40 -07:00
parent 8dcf63ff9d
commit 7400a5b50d
118 changed files with 19355 additions and 0 deletions

12
.vscode/launch.json vendored Normal file
View 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
View 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

View 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;

View 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
View 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
View 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
View 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
View 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
View 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 &#8226; 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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;

View 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
View 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;

View 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;

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View file

@ -0,0 +1,5 @@
module.exports = {
images: {
domains: ['res.cloudinary.com', 'via.placeholder.com'],
},
};

14745
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

111
package.json Normal file
View 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
View 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&#39;t exist.</p>
</>
);
}

51
pages/_app.js Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View file

@ -0,0 +1,5 @@
import Login from '../components/Login';
export default function LoginPage() {
return <Login />;
}

35
pages/logout.js Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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>
</>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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.

View 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.

View 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 arent 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.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show more