mirror of
https://github.com/BradNut/weddingsite
synced 2025-09-08 17:40:36 +00:00
Rename all old folders, create new folders and files for the app directory, setting up header/footer/layout, and getting prisma lucia auth working to create a user.
This commit is contained in:
parent
85f859f1d2
commit
dd9fa6a832
167 changed files with 8309 additions and 23635 deletions
3
.eslintrc.json
Normal file
3
.eslintrc.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
||||
19
.gitignore
vendored
19
.gitignore
vendored
|
|
@ -4,6 +4,7 @@
|
|||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
.yarn/install-state.gz
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
|
@ -25,11 +26,21 @@ yarn-debug.log*
|
|||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# panda css
|
||||
styled-system
|
||||
|
||||
# JetBrains
|
||||
.idea
|
||||
.fleet
|
||||
1
.nvmrc
Normal file
1
.nvmrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
18.17
|
||||
28
.vscode copy/launch.json
Normal file
28
.vscode copy/launch.json
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Next.js: debug server-side",
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"command": "pnpm dev"
|
||||
},
|
||||
{
|
||||
"name": "Next.js: debug client-side",
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"url": "http://localhost:3000"
|
||||
},
|
||||
{
|
||||
"name": "Next.js: debug full stack",
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"command": "pnpm dev",
|
||||
"serverReadyAction": {
|
||||
"pattern": "- Local:.+(https?://.+)",
|
||||
"uriFormat": "%s",
|
||||
"action": "debugWithChrome"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
69
README.md
69
README.md
|
|
@ -1,59 +1,36 @@
|
|||
# Wedding Website
|
||||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||
|
||||
## This is a skeleton template of the wedding website I created
|
||||
## Getting Started
|
||||
|
||||
Features include:
|
||||
- Password login for site access
|
||||
- RSVP page for groups or individuals
|
||||
- Pages that include:
|
||||
- Home Page
|
||||
- Wedding Party
|
||||
- Photos pages
|
||||
- Q&A
|
||||
- Travel information
|
||||
- RSVP forms
|
||||
First, run the development server:
|
||||
|
||||
## Detailed Info
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Names, Dates, Locations are all hardcoded to a value
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
The site implements a basic auth with [next-iron-session](https://github.com/vvo/next-iron-session) to protect access without knowing the password to the site.
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
The code is set up to use a MongoDB instace, ENV MONGO_URL, but this could easily be swapped for any DB. For the purposes of deploying this template for viewing the data is hardcoded.
|
||||
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
|
||||
|
||||
Use of CSS variables at a Layout level allows for theming and is easily extensible.
|
||||
## Learn More
|
||||
|
||||
Adding, Updating, and Deleting of guests and groups is currently done manually on the DB or on a deployment of the admin specific branch.
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
This admin branch is not included yet in this example site as no roles or permissions have been set up. However, this branch does include additional pages to add, edit, and delete these guests and groups.
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
*If deploying to production please remove all sections that have the following:*
|
||||
```// TODO: REMOVE THIS WHEN TAKING YOUR SITE TO PRODUCTION```
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||
|
||||
## Tech
|
||||
## Deploy on Vercel
|
||||
|
||||
Overall a typical NextJS Application using ReactJS and basic authentication.
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
### Frontend
|
||||
|
||||
- ReactJS
|
||||
- Styled Components
|
||||
- Images
|
||||
- Loaded using either the default NextJS image with custom blur animation
|
||||
- Or loading using Cloudinary on NextJS image and custom blur
|
||||
|
||||
### Backend
|
||||
|
||||
- NextJS APIs
|
||||
- Next Iron Session for Login
|
||||
- Server side rendering of base pages checking to see if user is logged in
|
||||
- Requires ENV variable of SECRET_COOKIE_PASSWORD to be set
|
||||
- Mongoose DB for MongoDB
|
||||
- Used to store RSVPs and default logins
|
||||
|
||||
## Future Changes
|
||||
1. On/Off feature for public vs password protected sites
|
||||
2. Build in auth permissions to allow guest vs admin roles
|
||||
3. If roles available then add in the admin pages for create, update, and deletion of guests/groups
|
||||
4. Add more theming options and easy customization of pages, resources, etc.
|
||||
5. Email reminder option
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
import { render, screen } from '@testing-library/react';
|
||||
import HomePage from '../pages';
|
||||
|
||||
describe('Index Page <HomePage />', () => {
|
||||
it('should render', () => {
|
||||
render(<HomePage />);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
import { render, screen } from '@testing-library/react';
|
||||
import Nav from '../components/Nav';
|
||||
|
||||
const useRouter = jest.spyOn(require('next/router'), 'useRouter');
|
||||
|
||||
describe('<Nav/>', () => {
|
||||
it('Renders nav correctly and matches snapshot', () => {
|
||||
const router = { pathname: '/' };
|
||||
useRouter.mockReturnValue(router);
|
||||
const { container, debug } = render(<Nav />);
|
||||
expect(container).toMatchSnapshot();
|
||||
const link = screen.getByText('RSVP');
|
||||
expect(link).toHaveAttribute('href', '/rsvp');
|
||||
});
|
||||
});
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<Nav/> Renders nav correctly and matches snapshot 1`] = `
|
||||
<div>
|
||||
<nav
|
||||
class="NavStyles-sc-vewc8g-0 dWZdml"
|
||||
>
|
||||
<a
|
||||
aria-current="page"
|
||||
href="/"
|
||||
>
|
||||
Home
|
||||
</a>
|
||||
<a
|
||||
href="/story"
|
||||
>
|
||||
Our Story
|
||||
</a>
|
||||
<a
|
||||
href="/party"
|
||||
>
|
||||
Wedding Party
|
||||
</a>
|
||||
<a
|
||||
href="/photos"
|
||||
>
|
||||
Photos
|
||||
</a>
|
||||
<a
|
||||
href="/travelstay"
|
||||
>
|
||||
Travel & Stay
|
||||
</a>
|
||||
<a
|
||||
href="/qanda"
|
||||
>
|
||||
Q + A
|
||||
</a>
|
||||
<a
|
||||
href="/rsvp"
|
||||
>
|
||||
RSVP
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
import buildBase64Data from '../utils/buildBase64Data';
|
||||
|
||||
describe('build base 64 function', () => {
|
||||
const imageName = 'https://picsum.photos/1307/880';
|
||||
const alt = 'test alt';
|
||||
it('takes an image name and builds base64 image', async () => {
|
||||
const imageData = await buildBase64Data(false, imageName, alt, {});
|
||||
expect(imageData).toBeDefined();
|
||||
expect(imageData.alt).toEqual(alt);
|
||||
expect(imageData.imageProps).toBeDefined();
|
||||
expect(imageData.imageProps.blurDataURL).toContain(
|
||||
'data:image/jpeg;base64'
|
||||
);
|
||||
expect(imageData.imageProps.height).toBeGreaterThan(0);
|
||||
expect(imageData.imageProps.width).toBeGreaterThan(0);
|
||||
expect(imageData.imageProps.src).toContain(imageName);
|
||||
});
|
||||
|
||||
it('fails if image not resolved', async () => {
|
||||
expect(await buildBase64Data(false, 'Blah', alt, {})).toEqual({});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import { render, screen } from '@testing-library/react';
|
||||
import Home from '../pages/index';
|
||||
|
||||
describe('Home', () => {
|
||||
it('renders home page', () => {
|
||||
render(<Home />);
|
||||
});
|
||||
});
|
||||
9
app.d.ts
vendored
Normal file
9
app.d.ts
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
// app.d.ts
|
||||
/// <reference types="lucia" />
|
||||
declare namespace Lucia {
|
||||
type Auth = import("./auth/lucia").Auth;
|
||||
type DatabaseUserAttributes = {
|
||||
username: string;
|
||||
};
|
||||
type DatabaseSessionAttributes = {};
|
||||
}
|
||||
5
app/actions/signUpAction.ts
Normal file
5
app/actions/signUpAction.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
'use server'
|
||||
|
||||
export async function SignUpAction() {
|
||||
|
||||
}
|
||||
82
app/api/login/route.ts
Normal file
82
app/api/login/route.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import { auth } from "@/auth/lucia";
|
||||
import * as context from "next/headers";
|
||||
import { NextResponse } from "next/server";
|
||||
import { LuciaError } from "lucia";
|
||||
|
||||
import type { NextRequest } from "next/server";
|
||||
|
||||
export const POST = async (request: NextRequest) => {
|
||||
const formData = await request.formData();
|
||||
const username = formData.get("username");
|
||||
const password = formData.get("password");
|
||||
// basic check
|
||||
if (
|
||||
typeof username !== "string" ||
|
||||
username.length < 1 ||
|
||||
username.length > 31
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: "Invalid username",
|
||||
},
|
||||
{
|
||||
status: 400,
|
||||
}
|
||||
);
|
||||
}
|
||||
if (
|
||||
typeof password !== "string" ||
|
||||
password.length < 1 ||
|
||||
password.length > 255
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: "Invalid password",
|
||||
},
|
||||
{
|
||||
status: 400,
|
||||
}
|
||||
);
|
||||
}
|
||||
try {
|
||||
// find user by key
|
||||
// and validate password
|
||||
const key = await auth.useKey("username", username.toLowerCase(), password);
|
||||
const session = await auth.createSession({
|
||||
userId: key.userId,
|
||||
attributes: {},
|
||||
});
|
||||
const authRequest = auth.handleRequest(request.method, context);
|
||||
authRequest.setSession(session);
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: {
|
||||
Location: "/", // redirect to profile page
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
if (
|
||||
e instanceof LuciaError &&
|
||||
(e.message === "AUTH_INVALID_KEY_ID" ||
|
||||
e.message === "AUTH_INVALID_PASSWORD")
|
||||
) {
|
||||
// user does not exist or invalid password
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: "Incorrect username or password",
|
||||
},
|
||||
{
|
||||
status: 400,
|
||||
}
|
||||
);
|
||||
}
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: "An unknown error occurred",
|
||||
},
|
||||
{
|
||||
status: 500,
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
122
app/api/signup/route.ts
Normal file
122
app/api/signup/route.ts
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
import { auth } from "@/auth/lucia";
|
||||
import * as context from "next/headers";
|
||||
import { NextResponse } from "next/server";
|
||||
import { add_user_to_role } from '@/lib/roles';
|
||||
|
||||
import type { NextRequest } from "next/server";
|
||||
|
||||
type VercelPostgresError = {
|
||||
code: string;
|
||||
detail: string;
|
||||
schema?: string;
|
||||
table?: string;
|
||||
column?: string;
|
||||
dataType?: string;
|
||||
constraint?: "auth_user_username_key";
|
||||
};
|
||||
|
||||
export const POST = async (request: NextRequest) => {
|
||||
const formData = await request.formData();
|
||||
const username = formData.get("username");
|
||||
const password = formData.get("password");
|
||||
const adminPassword = formData.get("admin_password");
|
||||
|
||||
if (!adminPassword || adminPassword !== process.env.ADMIN_PASSWORD) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: "Failed to create user",
|
||||
},
|
||||
{
|
||||
status: 400
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// basic check
|
||||
if (
|
||||
typeof username !== "string" ||
|
||||
username.length < 4 ||
|
||||
username.length > 31
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: "Invalid username"
|
||||
},
|
||||
{
|
||||
status: 400
|
||||
}
|
||||
);
|
||||
}
|
||||
if (
|
||||
typeof password !== "string" ||
|
||||
password.length < 6 ||
|
||||
password.length > 255
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: "Invalid password"
|
||||
},
|
||||
{
|
||||
status: 400
|
||||
}
|
||||
);
|
||||
}
|
||||
try {
|
||||
const user = await auth.createUser({
|
||||
key: {
|
||||
providerId: "username", // auth method
|
||||
providerUserId: username.toLowerCase(), // unique id when using "username" auth method
|
||||
password // hashed by Lucia
|
||||
},
|
||||
attributes: {
|
||||
username
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`User created: ${JSON.stringify(user, null, 2)}`);
|
||||
|
||||
add_user_to_role(user.userId, 'admin');
|
||||
|
||||
const session = await auth.createSession({
|
||||
userId: user.userId,
|
||||
attributes: {}
|
||||
});
|
||||
const authRequest = auth.handleRequest(request.method, context);
|
||||
authRequest.setSession(session);
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: {
|
||||
Location: "/" // redirect to profile page
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(`Error: ${e}`);
|
||||
|
||||
// this part depends on the database you're using
|
||||
// check for unique constraint error in user table
|
||||
const maybeVercelPostgresError = (
|
||||
typeof e === 'object' ? e : {}
|
||||
) as Partial<VercelPostgresError>;
|
||||
|
||||
// error code for unique constraint violation
|
||||
if (maybeVercelPostgresError.code === "23505") {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: "Username already taken"
|
||||
},
|
||||
{
|
||||
status: 400
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: "An unknown error occurred"
|
||||
},
|
||||
{
|
||||
status: 500
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
35
app/layout.tsx
Normal file
35
app/layout.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import '@/styles/globals.css';
|
||||
import '@/styles/typeography.scss';
|
||||
import styles from "@/styles/layout.module.scss";
|
||||
import type { Metadata } from 'next'
|
||||
import { Inter } from 'next/font/google'
|
||||
import Header from '@/components/Header';
|
||||
import Footer from '@/components/Footer';
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] })
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Create Next App',
|
||||
description: 'Generated by create next app',
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={inter.className}>
|
||||
<noscript>
|
||||
<h1>Please enable JavaScript to view our site.</h1>
|
||||
</noscript>
|
||||
<div className={styles.layout}>
|
||||
<Header />
|
||||
<main className={styles.content}>{children}</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
29
app/login/page.tsx
Normal file
29
app/login/page.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import Link from "next/link";
|
||||
import * as context from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
import { auth } from "@/auth/lucia";
|
||||
import Form from "@/components/form";
|
||||
|
||||
const Page = async () => {
|
||||
const authRequest = auth.handleRequest("GET", context);
|
||||
const session = await authRequest.validate();
|
||||
if (session) redirect("/");
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1>Sign in</h1>
|
||||
<Form action="/api/login">
|
||||
<label htmlFor="username">Username</label>
|
||||
<input name="username" id="username" />
|
||||
<br />
|
||||
<label htmlFor="password">Password</label>
|
||||
<input type="password" name="password" id="password" />
|
||||
<br />
|
||||
<input type="submit" />
|
||||
</Form>
|
||||
<Link href="/signup">Create an account</Link>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
17
app/not-found.tsx
Normal file
17
app/not-found.tsx
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import React from 'react';
|
||||
import { Metadata } from 'next';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: '404 - Page not found',
|
||||
|
||||
}
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<>
|
||||
<h1>Sorry page not found! 😿</h1>
|
||||
<p>404.</p>
|
||||
<p>{"You just hit a route that doesn't exist."}</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
7
app/page.tsx
Normal file
7
app/page.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export default function Home() {
|
||||
return (
|
||||
<div>
|
||||
Hello 🐼!
|
||||
</div>
|
||||
);
|
||||
}
|
||||
26
app/signup/page.tsx
Normal file
26
app/signup/page.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import React from "react";
|
||||
import Form from "@/components/form";
|
||||
import Link from 'next/link';
|
||||
|
||||
const Page = async () => {
|
||||
return (
|
||||
<>
|
||||
<h1>Sign up</h1>
|
||||
<Form action="/api/signup">
|
||||
<label htmlFor="username">Username</label>
|
||||
<input name="username" id="username" />
|
||||
<br />
|
||||
<label htmlFor="password">Password</label>
|
||||
<input type="password" name="password" id="password" />
|
||||
<br />
|
||||
<label htmlFor="admin_password">Admin Password</label>
|
||||
<input type="password" name="admin_password" id="admin_password" />
|
||||
<br />
|
||||
<input type="submit" />
|
||||
</Form>
|
||||
<Link href="/login">Login</Link>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
32
auth/lucia.ts
Normal file
32
auth/lucia.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { lucia } from "lucia";
|
||||
import { nextjs_future } from "lucia/middleware";
|
||||
import { prisma } from '@lucia-auth/adapter-prisma';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import "lucia/polyfill/node";
|
||||
|
||||
import { cache } from "react";
|
||||
import * as context from "next/headers";
|
||||
|
||||
const client = new PrismaClient();
|
||||
|
||||
// expect error (see next section)
|
||||
export const auth = lucia({
|
||||
adapter: prisma(client),
|
||||
env: process.env.NODE_ENV === "development" ? "DEV" : "PROD",
|
||||
middleware: nextjs_future(),
|
||||
sessionCookie: {
|
||||
expires: false,
|
||||
},
|
||||
getUserAttributes: (data) => {
|
||||
return {
|
||||
username: data.username,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export type Auth = typeof auth;
|
||||
|
||||
export const getPageSession = cache(() => {
|
||||
const authRequest = auth.handleRequest("GET", context);
|
||||
return authRequest.validate();
|
||||
});
|
||||
38
components/Footer.tsx
Normal file
38
components/Footer.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import React from "react";
|
||||
import Link from "next/link";
|
||||
import styles from "@/styles/layout.module.scss";
|
||||
|
||||
export default function Footer() {
|
||||
// const { user } = useUser();
|
||||
|
||||
return (
|
||||
<footer className={styles.footer}>
|
||||
<div>
|
||||
<h2>
|
||||
<Link href="/">N & N</Link>
|
||||
</h2>
|
||||
{/* {user && user.isLoggedIn === true ? (
|
||||
<>
|
||||
<hr />
|
||||
<h3>06.03.2030</h3>
|
||||
</>
|
||||
) : (
|
||||
""
|
||||
)} */}
|
||||
</div>
|
||||
<div>
|
||||
<p>Created by Bradley Shellnut</p>
|
||||
<div>
|
||||
Icons made by{" "}
|
||||
<a href="https://www.freepik.com" title="Freepik">
|
||||
Freepik
|
||||
</a>{" "}
|
||||
from{" "}
|
||||
<a href="https://www.flaticon.com/" title="Flaticon">
|
||||
www.flaticon.com
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
34
components/Header.tsx
Normal file
34
components/Header.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import React from "react";
|
||||
import Link from "next/link";
|
||||
import styles from '@/styles/layout.module.scss';
|
||||
// import useWeddingStart from "@/lib/useWeddingStart";
|
||||
// import WeddingStart from "./WeddingStart";
|
||||
import Nav from "./Nav";
|
||||
|
||||
export default function Header() {
|
||||
// const { user } = useUser();
|
||||
// const { timeAsDays, pastWeddingDate } = useWeddingStart({
|
||||
// update: 60000,
|
||||
// });
|
||||
|
||||
return (
|
||||
<header className={styles.header}>
|
||||
<div>
|
||||
<Link href="/">
|
||||
<h1 className="center">Name & Name</h1>
|
||||
</Link>
|
||||
{/* {user && user.isLoggedIn === true && !pastWeddingDate ? (
|
||||
<>
|
||||
<h2 className="center">June 3rd, 2030 @ New York, New York</h2>
|
||||
<h3 className="center">
|
||||
Countdown: <WeddingStart /> days!
|
||||
</h3>
|
||||
</>
|
||||
) : (
|
||||
""
|
||||
)} */}
|
||||
</div>
|
||||
{/* {user && user.isLoggedIn === true ? <Nav /> : ""} */}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
16
components/Nav.tsx
Normal file
16
components/Nav.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { NavLink } from "./NavLink";
|
||||
import NavStyles from "@/styles/NavStyles";
|
||||
|
||||
export default function Nav() {
|
||||
return (
|
||||
<NavStyles>
|
||||
<NavLink href="/">Home</NavLink>
|
||||
<NavLink href="/story">Our Story</NavLink>
|
||||
<NavLink href="/party">Wedding Party</NavLink>
|
||||
<NavLink href="/photos">Photos</NavLink>
|
||||
<NavLink href="/travelstay">Travel & Stay</NavLink>
|
||||
<NavLink href="/qanda">Q & A</NavLink>
|
||||
<NavLink href="/rsvp">RSVP</NavLink>
|
||||
</NavStyles>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
export const NavLink = ({ href, children }) => {
|
||||
const { asPath } = useRouter();
|
||||
const ariaCurrent = href === asPath ? 'page' : undefined;
|
||||
|
||||
return (
|
||||
<Link prefetch href={href} aria-current={ariaCurrent}>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
19
components/NavLink.tsx
Normal file
19
components/NavLink.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
interface NavLinkType {
|
||||
href: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const NavLink: React.FC<NavLinkType> = ({ href, children }) => {
|
||||
const { asPath } = useRouter();
|
||||
const ariaCurrent = href === asPath ? "page" : undefined;
|
||||
|
||||
return (
|
||||
<Link prefetch href={href} aria-current={ariaCurrent}>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
12
components/WeddingStart.tsx
Normal file
12
components/WeddingStart.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import React from "react";
|
||||
import useWeddingStart from "@/lib/useWeddingStart";
|
||||
|
||||
export default function WeddingStart() {
|
||||
const { timeAsDays } = useWeddingStart({
|
||||
update: 60000,
|
||||
});
|
||||
|
||||
return (
|
||||
<span style={{ color: "#e64c44", fontSize: "3.157rem" }}>{timeAsDays}</span>
|
||||
);
|
||||
}
|
||||
40
components/form.tsx
Normal file
40
components/form.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
// components/form.tsx
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
const Form = ({
|
||||
children,
|
||||
action,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
action: string;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
return (
|
||||
<form
|
||||
action={action}
|
||||
method="post"
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.currentTarget);
|
||||
const response = await fetch(action, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
redirect: "manual",
|
||||
});
|
||||
|
||||
if (response.status === 0) {
|
||||
// redirected
|
||||
// when using `redirect: "manual"`, response status 0 is returned
|
||||
return router.refresh();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default Form;
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import Link from 'next/link';
|
||||
import useUser from '../lib/useUser';
|
||||
import useUser from '../lib_old/useUser';
|
||||
import WeddingStart from './WeddingStart';
|
||||
import Nav from './Nav';
|
||||
import useWeddingStart from '../lib/useWeddingStart';
|
||||
import useWeddingStart from '../lib_old/useWeddingStart';
|
||||
|
||||
const HeaderStyles = styled.header`
|
||||
display: grid;
|
||||
19
components_old/NavLink.tsx
Normal file
19
components_old/NavLink.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
interface NavLinkType {
|
||||
href: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const NavLink: React.FC<NavLinkType> = ({ href, children }) => {
|
||||
const { asPath } = useRouter();
|
||||
const ariaCurrent = href === asPath ? "page" : undefined;
|
||||
|
||||
return (
|
||||
<Link prefetch href={href} aria-current={ariaCurrent}>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import useWeddingStart from '../lib/useWeddingStart';
|
||||
import useWeddingStart from '../lib_old/useWeddingStart';
|
||||
|
||||
export default function WeddingStart() {
|
||||
const { timeAsDays } = useWeddingStart({
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
import '@testing-library/jest-dom';
|
||||
|
||||
window.alert = console.log;
|
||||
19
lib/db.ts
Normal file
19
lib/db.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
const prismaClientSingleton = () => {
|
||||
return new PrismaClient();
|
||||
};
|
||||
|
||||
type PrismaClientSingleton = ReturnType<typeof prismaClientSingleton>;
|
||||
|
||||
const globalForPrisma = globalThis as unknown as {
|
||||
prisma: PrismaClientSingleton | undefined;
|
||||
};
|
||||
|
||||
const prisma = globalForPrisma.prisma ?? prismaClientSingleton();
|
||||
|
||||
export default prisma;
|
||||
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
globalForPrisma.prisma = prisma;
|
||||
}
|
||||
29
lib/registry.tsx
Normal file
29
lib/registry.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useServerInsertedHTML } from 'next/navigation';
|
||||
import { ServerStyleSheet, StyleSheetManager } from 'styled-components';
|
||||
|
||||
export default function StyledComponentsRegistry({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
// Only create stylesheet once with lazy initial state
|
||||
// x-ref: https://reactjs.org/docs/hooks-reference.html#lazy-initial-state
|
||||
const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet());
|
||||
|
||||
useServerInsertedHTML(() => {
|
||||
const styles = styledComponentsStyleSheet.getStyleElement();
|
||||
styledComponentsStyleSheet.instance.clearTag();
|
||||
return <>{styles}</>;
|
||||
});
|
||||
|
||||
if (typeof window !== "undefined") return <>{children}</>;
|
||||
|
||||
return (
|
||||
<StyleSheetManager sheet={styledComponentsStyleSheet.instance}>
|
||||
{children}
|
||||
</StyleSheetManager>
|
||||
);
|
||||
}
|
||||
32
lib/roles.ts
Normal file
32
lib/roles.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import prisma from "./db";
|
||||
|
||||
export async function add_user_to_role(user_id: string, role_name: string) {
|
||||
// Find the role by its name
|
||||
const role = await prisma.role.findUnique({
|
||||
where: {
|
||||
name: role_name,
|
||||
},
|
||||
});
|
||||
|
||||
if (!role) {
|
||||
throw new Error(`Role with name ${role_name} not found`);
|
||||
}
|
||||
|
||||
// Create a UserRole entry linking the user and the role
|
||||
const userRole = await prisma.userRole.create({
|
||||
data: {
|
||||
user: {
|
||||
connect: {
|
||||
id: user_id,
|
||||
},
|
||||
},
|
||||
role: {
|
||||
connect: {
|
||||
id: role.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return userRole;
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { useState } from 'react';
|
||||
import useInterval from '../utils/useInterval';
|
||||
import useInterval from '@/lib/useInterval';
|
||||
|
||||
export default function useWeddingStart({ update = 60000 }) {
|
||||
const weddingDate = 1906736400000;
|
||||
|
|
|
|||
69
lib_old/events.json
Normal file
69
lib_old/events.json
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
[
|
||||
{
|
||||
"name": "Rehearsal",
|
||||
"date": "Sunday, June 2nd, 2030",
|
||||
"start": "",
|
||||
"end": "",
|
||||
"venueName": "",
|
||||
"attire": "",
|
||||
"description": "Rehearsal & Rehearsal Dinner",
|
||||
"openToAll": true,
|
||||
"showSchedule": false,
|
||||
"scheduleEvents": []
|
||||
},
|
||||
{
|
||||
"name": "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": "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": "Saying Goodbye",
|
||||
"date": "Tuesday, June 4th, 2030",
|
||||
"start": "9:00 AM",
|
||||
"end": "11:30 AM",
|
||||
"venueName": "",
|
||||
"attire": "",
|
||||
"description": "Farewell Brunch",
|
||||
"openToAll": true,
|
||||
"showSchedule": false,
|
||||
"scheduleEvents": []
|
||||
}
|
||||
]
|
||||
24
lib_old/fetchJson.js
Normal file
24
lib_old/fetchJson.js
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
export default async function fetcher(...args) {
|
||||
try {
|
||||
const response = await fetch(...args);
|
||||
|
||||
// if the server replies, there's always some data in json
|
||||
// if there's a network error, it will throw at the previous line
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
return data;
|
||||
}
|
||||
|
||||
// console.log(JSON.stringify(data));
|
||||
const error = new Error(response?.statusText || data?.message);
|
||||
error.response = response;
|
||||
error.data = data;
|
||||
throw error;
|
||||
} catch (error) {
|
||||
if (!error.data) {
|
||||
error.data = { message: error.message };
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
15
lib_old/session.js
Normal file
15
lib_old/session.js
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
// this file is a wrapper with defaults to be used in both API routes and `getServerSideProps` functions
|
||||
import { withIronSessionApiRoute } from 'iron-session/next';
|
||||
|
||||
export default function withSession(handler) {
|
||||
return withIronSessionApiRoute(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)
|
||||
// maxAge default is 14 days
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
},
|
||||
});
|
||||
}
|
||||
35
lib_old/svgs.js
Normal file
35
lib_old/svgs.js
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
export const CalendarIcon = () => (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M11 6.5C11 6.36739 11.0527 6.24021 11.1464 6.14645C11.2402 6.05268 11.3674 6 11.5 6H12.5C12.6326 6 12.7598 6.05268 12.8536 6.14645C12.9473 6.24021 13 6.36739 13 6.5V7.5C13 7.63261 12.9473 7.75979 12.8536 7.85355C12.7598 7.94732 12.6326 8 12.5 8H11.5C11.3674 8 11.2402 7.94732 11.1464 7.85355C11.0527 7.75979 11 7.63261 11 7.5V6.5Z"
|
||||
fill="#FCCFB9"
|
||||
/>
|
||||
<path
|
||||
d="M3.5 0C3.63261 0 3.75979 0.0526784 3.85355 0.146447C3.94732 0.240215 4 0.367392 4 0.5V1H12V0.5C12 0.367392 12.0527 0.240215 12.1464 0.146447C12.2402 0.0526784 12.3674 0 12.5 0C12.6326 0 12.7598 0.0526784 12.8536 0.146447C12.9473 0.240215 13 0.367392 13 0.5V1H14C14.5304 1 15.0391 1.21071 15.4142 1.58579C15.7893 1.96086 16 2.46957 16 3V14C16 14.5304 15.7893 15.0391 15.4142 15.4142C15.0391 15.7893 14.5304 16 14 16H2C1.46957 16 0.960859 15.7893 0.585786 15.4142C0.210714 15.0391 0 14.5304 0 14V3C0 2.46957 0.210714 1.96086 0.585786 1.58579C0.960859 1.21071 1.46957 1 2 1H3V0.5C3 0.367392 3.05268 0.240215 3.14645 0.146447C3.24021 0.0526784 3.36739 0 3.5 0V0ZM1 4V14C1 14.2652 1.10536 14.5196 1.29289 14.7071C1.48043 14.8946 1.73478 15 2 15H14C14.2652 15 14.5196 14.8946 14.7071 14.7071C14.8946 14.5196 15 14.2652 15 14V4H1Z"
|
||||
fill="#FCCFB9"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const MapIcon = () => (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M15.817 0.112823C15.8743 0.159759 15.9204 0.218822 15.952 0.285748C15.9837 0.352674 16 0.425792 16 0.499823V14.4998C15.9999 14.6154 15.9598 14.7273 15.8866 14.8167C15.8133 14.906 15.7113 14.9672 15.598 14.9898L10.598 15.9898C10.5333 16.0027 10.4667 16.0027 10.402 15.9898L5.5 15.0098L0.598 15.9898C0.525489 16.0043 0.450665 16.0025 0.378921 15.9846C0.307176 15.9667 0.240296 15.9331 0.183099 15.8863C0.125903 15.8394 0.0798134 15.7804 0.0481518 15.7136C0.0164902 15.6468 4.46527e-05 15.5738 0 15.4998L0 1.49982C6.9782e-05 1.38428 0.0401561 1.27232 0.113443 1.18299C0.186731 1.09366 0.288695 1.03247 0.402 1.00982L5.402 0.00982311C5.46669 -0.00310763 5.53331 -0.00310763 5.598 0.00982311L10.5 0.989823L15.402 0.00982311C15.4745 -0.00476108 15.5493 -0.00308756 15.6211 0.0147231C15.6928 0.0325338 15.7597 0.0660382 15.817 0.112823V0.112823ZM10 1.90982L6 1.10982V14.0898L10 14.8898V1.90982ZM11 14.8898L15 14.0898V1.10982L11 1.90982V14.8898ZM5 14.0898V1.10982L1 1.90982V14.8898L5 14.0898Z"
|
||||
fill="#FCCFB9"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
71
lib_old/useForm.js
Normal file
71
lib_old/useForm.js
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
|
||||
export default function useForm(initial = {}) {
|
||||
// Create a state object for our inputs
|
||||
const [inputs, setInputs] = useState(initial);
|
||||
const initialValues = Object.values(initial).join('');
|
||||
|
||||
useEffect(() => {
|
||||
// This function runs when the things we are watching change
|
||||
setInputs(initial);
|
||||
}, [initialValues]);
|
||||
|
||||
function handleChange(e) {
|
||||
let { value, name, type } = e.target;
|
||||
console.log(value, name, type);
|
||||
if (name.includes('_')) {
|
||||
const values = name.split('_');
|
||||
// console.log(`Values: ${JSON.stringify(values)}`);
|
||||
const [id, property] = values;
|
||||
const data = inputs[id];
|
||||
// console.log(`Data: ${JSON.stringify(data)}`);
|
||||
// console.log(`Value: ${JSON.stringify(value)}`);
|
||||
// console.log(`Property: ${JSON.stringify(property)}`);
|
||||
data[property] = value;
|
||||
|
||||
if (property === 'rsvpStatus' && value !== 'accepted') {
|
||||
// console.log('Setting plus one to false');
|
||||
data.plusOne = false;
|
||||
}
|
||||
|
||||
setInputs({
|
||||
...inputs,
|
||||
[id]: data,
|
||||
});
|
||||
} else {
|
||||
if (type === 'number') {
|
||||
value = parseInt(value);
|
||||
}
|
||||
if (type === 'file') {
|
||||
value[0] = e.target.files;
|
||||
}
|
||||
|
||||
setInputs({
|
||||
// Copy the existing state
|
||||
...inputs,
|
||||
[name]: value,
|
||||
});
|
||||
}
|
||||
|
||||
// console.log(`Inputs after: ${JSON.stringify(inputs)}`);
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
setInputs(initial);
|
||||
}
|
||||
|
||||
function clearForm() {
|
||||
const blankState = Object.fromEntries(
|
||||
Object.entries(inputs).map(([key, value]) => [key, ''])
|
||||
);
|
||||
setInputs(blankState);
|
||||
}
|
||||
|
||||
// Return the things we want to surface from this custom hook
|
||||
return {
|
||||
inputs,
|
||||
handleChange,
|
||||
resetForm,
|
||||
clearForm,
|
||||
};
|
||||
}
|
||||
16
lib_old/useModal.js
Normal file
16
lib_old/useModal.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { useState } from 'react';
|
||||
|
||||
const useModal = () => {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
function toggleModal() {
|
||||
setIsVisible(!isVisible);
|
||||
}
|
||||
|
||||
return {
|
||||
isVisible,
|
||||
toggleModal,
|
||||
};
|
||||
};
|
||||
|
||||
export default useModal;
|
||||
27
lib_old/useUser.js
Normal file
27
lib_old/useUser.js
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { useEffect } from 'react';
|
||||
import Router from 'next/router';
|
||||
import useSWR from 'swr';
|
||||
|
||||
export default function useUser({
|
||||
redirectTo = false,
|
||||
redirectIfFound = false,
|
||||
} = {}) {
|
||||
const { data: user, mutate: mutateUser } = useSWR('/api/user');
|
||||
|
||||
useEffect(() => {
|
||||
// if no redirect needed, just return (example: already on /dashboard)
|
||||
// if user data not yet there (fetch in progress, logged in or not) then don't do anything yet
|
||||
if (!redirectTo || !user) return;
|
||||
|
||||
if (
|
||||
// If redirectTo is set, redirect if the user was not found.
|
||||
(redirectTo && !redirectIfFound && !user?.isLoggedIn) ||
|
||||
// If redirectIfFound is also set, redirect if the user was found
|
||||
(redirectIfFound && user?.isLoggedIn)
|
||||
) {
|
||||
Router.push(redirectTo);
|
||||
}
|
||||
}, [user, redirectIfFound, redirectTo]);
|
||||
|
||||
return { user, mutateUser };
|
||||
}
|
||||
17
lib_old/useWeddingStart.js
Normal file
17
lib_old/useWeddingStart.js
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { useState } from 'react';
|
||||
import useInterval from '../utils_old/useInterval';
|
||||
|
||||
export default function useWeddingStart({ update = 60000 }) {
|
||||
const weddingDate = 1906736400000;
|
||||
const [timeToWedding, setTime] = useState(
|
||||
weddingDate - Date.now() <= 0 ? 0 : weddingDate - Date.now()
|
||||
);
|
||||
useInterval(() => {
|
||||
const time = weddingDate - Date.now();
|
||||
setTime(time <= 0 ? 0 : time);
|
||||
}, update);
|
||||
return {
|
||||
timeToWedding,
|
||||
timeAsDays: Math.ceil(timeToWedding / 1000 / 60 / 60 / 24),
|
||||
};
|
||||
}
|
||||
7
lib_old/utils.js
Normal file
7
lib_old/utils.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export async function copyToClipboard(textToCopy) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(textToCopy);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
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);
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
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);
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
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);
|
||||
|
|
@ -1,8 +1,29 @@
|
|||
module.exports = {
|
||||
compiler: {
|
||||
styledComponents: true,
|
||||
},
|
||||
images: {
|
||||
domains: ['res.cloudinary.com', 'via.placeholder.com', 'picsum.photos'],
|
||||
},
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
compiler: {
|
||||
styledComponents: true,
|
||||
},
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "res.cloudinary.com",
|
||||
},
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "via.placeholder.com",
|
||||
},
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "picsum.photos",
|
||||
},
|
||||
],
|
||||
formats: ["image/avif", "image/webp"],
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
|
|
|||
23248
package-lock.json
generated
23248
package-lock.json
generated
File diff suppressed because it is too large
Load diff
213
package.json
213
package.json
|
|
@ -1,143 +1,74 @@
|
|||
{
|
||||
"name": "weddingsite",
|
||||
"version": "0.2.0",
|
||||
"description": "Wedding Website",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "NODE_OPTIONS='--inspect' next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"test": "NODE_ENV=test jest --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@plaiceholder/next": "^2.5.0",
|
||||
"@radix-ui/react-dialog": "^1.0.2",
|
||||
"babel-core": "^6.26.3",
|
||||
"babel-plugin-styled-components": "^2.0.7",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"cloudinary-build-url": "^0.2.4",
|
||||
"dotenv": "^16.0.3",
|
||||
"escape-html": "^1.0.3",
|
||||
"html-escaper": "^3.0.3",
|
||||
"iron-session": "^6.3.1",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"mongodb": "^4.11.0",
|
||||
"mongoose": "^6.11.3",
|
||||
"next": "^13.0.3",
|
||||
"normalize.css": "^8.0.1",
|
||||
"nprogress": "^0.2.0",
|
||||
"plaiceholder": "^2.5.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-icons": "^4.6.0",
|
||||
"sharp": "^0.31.2",
|
||||
"styled-components": "^5.3.6",
|
||||
"swr": "^1.3.0",
|
||||
"waait": "^1.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.2",
|
||||
"@babel/preset-env": "^7.20.2",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.42.1",
|
||||
"@typescript-eslint/parser": "^5.42.1",
|
||||
"eslint": "^8.27.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-config-wesbos": "^3.1.4",
|
||||
"eslint-plugin-html": "^7.1.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.6.1",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-react": "^7.31.10",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"jest": "^29.3.1",
|
||||
"prettier": "^2.7.1",
|
||||
"typescript": "^4.8.4"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"wesbos"
|
||||
],
|
||||
"rules": {
|
||||
"react/prop-types": 0
|
||||
}
|
||||
},
|
||||
"jest": {
|
||||
"testEnvironment": "jsdom",
|
||||
"setupFiles": [
|
||||
"./.jest/setEnvVars.js"
|
||||
],
|
||||
"setupFilesAfterEnv": [
|
||||
"./jest.setup.js"
|
||||
]
|
||||
},
|
||||
"//": "This is our babel config, I prefer this over a .babelrc file",
|
||||
"babel": {
|
||||
"plugins": [
|
||||
[
|
||||
"styled-components",
|
||||
{
|
||||
"ssr": true,
|
||||
"displayName": true,
|
||||
"preprocess": false
|
||||
}
|
||||
]
|
||||
],
|
||||
"env": {
|
||||
"development": {
|
||||
"presets": [
|
||||
"next/babel"
|
||||
],
|
||||
"plugins": [
|
||||
[
|
||||
"styled-components",
|
||||
{
|
||||
"ssr": true,
|
||||
"displayName": true,
|
||||
"preprocess": false
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"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
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
"name": "weddingsite",
|
||||
"version": "0.2.0",
|
||||
"description": "Wedding Website",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "NODE_OPTIONS='--inspect' next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"test": "NODE_ENV=test jest --watch",
|
||||
"db:studio": "prisma studio",
|
||||
"db:push": "prisma db push",
|
||||
"db:generate": "prisma generate",
|
||||
"db:seed": "prisma db seed"
|
||||
},
|
||||
"prisma": {
|
||||
"seed": "node --loader ts-node/esm prisma/seed.ts"
|
||||
},
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0 || >=20.0.0 <21.0.0",
|
||||
"pnpm": ">=8"
|
||||
},
|
||||
"dependencies": {
|
||||
"@lucia-auth/adapter-postgresql": "^2.0.2",
|
||||
"@lucia-auth/adapter-prisma": "^3.0.2",
|
||||
"@plaiceholder/next": "^2.5.0",
|
||||
"@prisma/client": "^5.6.0",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-dialog": "^1.0.2",
|
||||
"@radix-ui/react-form": "^0.0.3",
|
||||
"@vercel/postgres": "^0.5.1",
|
||||
"bcrypt": "^5.1.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"cloudinary-build-url": "^0.2.4",
|
||||
"dotenv": "^16.0.3",
|
||||
"escape-html": "^1.0.3",
|
||||
"html-escaper": "^3.0.3",
|
||||
"iron-session": "^6.3.1",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"lucia": "^2.7.4",
|
||||
"mongodb": "^4.11.0",
|
||||
"mongoose": "^6.11.3",
|
||||
"next": "^14.0.2",
|
||||
"next-auth": "5.0.0-beta.3",
|
||||
"normalize.css": "^8.0.1",
|
||||
"nprogress": "^0.2.0",
|
||||
"plaiceholder": "^2.5.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-icons": "^4.6.0",
|
||||
"sass": "^1.69.5",
|
||||
"sharp": "^0.32.4",
|
||||
"swr": "^1.3.0",
|
||||
"waait": "^1.0.5",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"@types/node": "^20.9.0",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"@typescript-eslint/eslint-plugin": "^5.42.1",
|
||||
"@typescript-eslint/parser": "^5.42.1",
|
||||
"eslint": "^8.27.0",
|
||||
"eslint-config-next": "^14.0.2",
|
||||
"prettier": "^2.7.1",
|
||||
"prisma": "^5.6.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.2.2"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,10 +9,10 @@ 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 connectDb from '../../utils_old/db';
|
||||
import { CalendarIcon, MapIcon } from '../../lib/svgs';
|
||||
import Modal from '../../components/Modal';
|
||||
import { handleUmamiEvent } from '../../utils/handleUmamiEvent';
|
||||
import { handleUmamiEvent } from '../../utils_old/handleUmamiEvent';
|
||||
|
||||
const RSVPGroupStyles = styled.div`
|
||||
h2 {
|
||||
5648
pnpm-lock.yaml
Normal file
5648
pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load diff
4
postcss.config.cjs
Normal file
4
postcss.config.cjs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
},
|
||||
}
|
||||
13
prisma/role-permissions.ts
Normal file
13
prisma/role-permissions.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
enum Permission {
|
||||
UPDATE_USER = "UPDATE_USER",
|
||||
VIEW_USER = "VIEW_USER",
|
||||
}
|
||||
|
||||
export const RolePermissions = {
|
||||
admin: {
|
||||
permission: [Permission.UPDATE_USER, Permission.VIEW_USER],
|
||||
},
|
||||
guest: {
|
||||
permission: [Permission.VIEW_USER],
|
||||
}
|
||||
}
|
||||
130
prisma/schema.prisma
Normal file
130
prisma/schema.prisma
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
// This is your Prisma schema file,
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
directUrl = env("POSTGRES_URL_NON_POOLING")
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(uuid())
|
||||
username String @unique
|
||||
roles UserRole[]
|
||||
auth_session Session[]
|
||||
key Key[]
|
||||
created_at DateTime @default(now()) @db.Timestamp(6)
|
||||
updated_at DateTime @updatedAt @db.Timestamp(6)
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
|
||||
enum RSVP_STATUS {
|
||||
INVITED
|
||||
ACCEPTED
|
||||
DECLINED
|
||||
}
|
||||
|
||||
model Guest {
|
||||
id String @id @default(uuid())
|
||||
first_name String
|
||||
last_name String
|
||||
rsvp_status RSVP_STATUS
|
||||
dietary_notes String
|
||||
song_requests String
|
||||
has_plus_one Boolean @default(false)
|
||||
plus_one Boolean @default(false)
|
||||
plus_one_first_name String?
|
||||
plus_one_last_name String?
|
||||
group Group @relation(fields: [group_id], references: [id])
|
||||
group_id String
|
||||
|
||||
@@map("guests")
|
||||
}
|
||||
|
||||
model Group {
|
||||
id String @id @default(uuid())
|
||||
name String
|
||||
note String
|
||||
rsvp_submitted Boolean @default(false)
|
||||
guests Guest[]
|
||||
|
||||
@@map("groups")
|
||||
}
|
||||
|
||||
model Role {
|
||||
id String @id @default(uuid())
|
||||
name String @unique
|
||||
user_roles UserRole[]
|
||||
role_permissions RolePermission[]
|
||||
|
||||
@@map("roles")
|
||||
}
|
||||
|
||||
model UserRole {
|
||||
id String @id @default(uuid())
|
||||
user User @relation(fields: [user_id], references: [id])
|
||||
user_id String
|
||||
role Role @relation(fields: [role_id], references: [id])
|
||||
role_id String
|
||||
created_at DateTime @default(now()) @db.Timestamp(6)
|
||||
updated_at DateTime @updatedAt @db.Timestamp(6)
|
||||
|
||||
@@unique([user_id, role_id])
|
||||
@@index([user_id])
|
||||
@@index([role_id])
|
||||
@@map("user_roles")
|
||||
}
|
||||
|
||||
enum PERMISSION_NAME {
|
||||
VIEW_USER
|
||||
MODIFY_USER
|
||||
}
|
||||
|
||||
model Permission {
|
||||
id String @id @default(uuid())
|
||||
name PERMISSION_NAME
|
||||
description String
|
||||
role_permissions RolePermission[]
|
||||
|
||||
@@map("permissions")
|
||||
}
|
||||
|
||||
model RolePermission {
|
||||
id String @id @default(uuid())
|
||||
role Role @relation(fields: [role_id], references: [id], onDelete: Cascade)
|
||||
role_id String
|
||||
permission Permission @relation(fields: [permission_id], references: [id], onDelete: Cascade)
|
||||
permission_id String
|
||||
created_at DateTime @default(now()) @db.Timestamp(6)
|
||||
updated_at DateTime @updatedAt @db.Timestamp(6)
|
||||
|
||||
@@map("role_permissions")
|
||||
}
|
||||
|
||||
model Key {
|
||||
id String @id @default(uuid())
|
||||
hashed_password String?
|
||||
user_id String
|
||||
user User @relation(references: [id], fields: [user_id], onDelete: Cascade)
|
||||
|
||||
@@index([user_id])
|
||||
@@map("keys")
|
||||
}
|
||||
|
||||
model Session {
|
||||
id String @id @default(uuid())
|
||||
user_id String
|
||||
active_expires BigInt
|
||||
idle_expires BigInt
|
||||
user User @relation(references: [id], fields: [user_id], onDelete: Cascade)
|
||||
created_at DateTime @default(now()) @db.Timestamp(6)
|
||||
updated_at DateTime @updatedAt @db.Timestamp(6)
|
||||
|
||||
@@index([user_id])
|
||||
@@map("sessions")
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue