diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..72cb4c6 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,12 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "attach", + "name": "Launch Program", + "skipFiles": ["/**"], + "port": 9229 + } + ] +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..0a24236 --- /dev/null +++ b/README.md @@ -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 diff --git a/components/CustomNextCloudinaryImage.js b/components/CustomNextCloudinaryImage.js new file mode 100644 index 0000000..1cb6d40 --- /dev/null +++ b/components/CustomNextCloudinaryImage.js @@ -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 ( + + { + setOnloadCount((prev) => prev + 1); + if (onLoad) onLoad(e); + }} + src={imageUrl} + objectFit="cover" + width={width} + height={height} + {...other} + /> + + ); +}; + +export default CustomNextCloudinaryImage; diff --git a/components/CustomNextImage.js b/components/CustomNextImage.js new file mode 100644 index 0000000..375364d --- /dev/null +++ b/components/CustomNextImage.js @@ -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 ( + + { + setOnloadCount((prev) => prev + 1); + if (onLoad) onLoad(e); + }} + src={imageUrl} + objectFit="cover" + width={width} + height={height} + {...other} + /> + + ); +}; + +export default CustomNextImage; diff --git a/components/Event.js b/components/Event.js new file mode 100644 index 0000000..908a81e --- /dev/null +++ b/components/Event.js @@ -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 ( + +
+

{name}

+

{date}

+

+ {start} + {end && ` - ${end}`} +

+ {venueName &&
} + {attire &&

{attire}

} + {description &&

{description}

} +
+ {showSchedule && + scheduleEvents && + scheduleEvents.map(({ name, start, end, venueName }) => ( + +
+ {start && ( +

+ {start} + {end && ` - {end}`} +

+ )} +
+
+ {name &&

{name}

} + {venueName && ( +
+ )} +
+ + ))} + + ); +} diff --git a/components/Footer.js b/components/Footer.js new file mode 100644 index 0000000..552e43d --- /dev/null +++ b/components/Footer.js @@ -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 ( + +
+

+ + N & N + +

+ {user && user.isLoggedIn === true ? ( + <> +
+

06.03.2030

+ + ) : ( + '' + )} +
+
+

Created by Bradley

+
+
+ ); +} diff --git a/components/Form.js b/components/Form.js new file mode 100644 index 0000000..48f678d --- /dev/null +++ b/components/Form.js @@ -0,0 +1,43 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const Form = ({ errorMessage, onSubmit }) => ( +
+ + + + + {errorMessage &&

{errorMessage}

} + + +
+); + +export default Form; + +Form.propTypes = { + errorMessage: PropTypes.string, + onSubmit: PropTypes.func, +}; diff --git a/components/GuestRSVP.js b/components/GuestRSVP.js new file mode 100644 index 0000000..28cd94c --- /dev/null +++ b/components/GuestRSVP.js @@ -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

Loading...

; + } + + // const [plusOne, setPlusOne] = useState( + // guest?.rsvpStatus === 'accepted' && guest?.plusOne + // ); + + return ( + <> + +

+ {guest.firstName} {guest.lastName} +

+ + + + +
+ {guest?.hasPlusOne && inputs[guest?.id]?.rsvpStatus === 'accepted' ? ( + + ) : ( + '' + )} + + ); +} diff --git a/components/Header.js b/components/Header.js new file mode 100644 index 0000000..5729d7b --- /dev/null +++ b/components/Header.js @@ -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 ( + +
+ + +

Name & Name

+
+ + {user && user.isLoggedIn === true ? ( + <> +

+ June 3rd, 2030 • New York, New York +

+

+ Countdown: days! +

+ + ) : ( + '' + )} +
+ {user && user.isLoggedIn === true ?