├── robots.txt ├── public ├── favicon.ico └── images │ ├── cat.jpg │ └── camelshump.JPG ├── postcss.config.js ├── prisma ├── migrations │ └── migration_lock.toml └── schema.prisma ├── pages ├── api │ ├── hello.ts │ └── guestbook │ │ ├── index.ts │ │ └── new.ts ├── _app.tsx ├── book.tsx └── index.tsx ├── next-env.d.ts ├── lib ├── date.ts └── prisma.ts ├── README.md ├── components ├── guestbook │ ├── Entry.tsx │ └── Form.tsx ├── footer.tsx ├── nav.tsx ├── theme.tsx ├── layout.tsx └── Meta.tsx ├── .gitignore ├── .github └── dependabot.yml ├── tsconfig.json ├── .eslintrc.js ├── next.config.js ├── styles └── globals.css ├── package.json ├── tailwind.config.js ├── LICENSE └── yarn.lock /robots.txt: -------------------------------------------------------------------------------- 1 | Hello there, Robot. 2 | 3 | - A human -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exu3/site/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/images/cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exu3/site/HEAD/public/images/cat.jpg -------------------------------------------------------------------------------- /public/images/camelshump.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exu3/site/HEAD/public/images/camelshump.JPG -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | 3 | export default (req, res) => { 4 | res.status(200).json({ name: 'John Doe' }) 5 | } 6 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /lib/date.ts: -------------------------------------------------------------------------------- 1 | // Formats an ISO date string to a human readable string (eg. May 1, 2020) 2 | export const formatDate = (dateString: string) => { 3 | return new Date(dateString).toLocaleDateString(undefined, { 4 | year: "numeric", 5 | month: "long", 6 | day: "numeric", 7 | }); 8 | }; 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built with Next.js and Tailwind CSS. 4 | 5 | How to run it locally: 6 | 7 | ```shell 8 | git clone https://github.com/exu3/site && cd site 9 | ``` 10 | 11 | Install dependencies with pnpm 12 | 13 | ```sh 14 | pnpm install 15 | ``` 16 | 17 | Then 18 | 19 | ``` 20 | pnpm dev 21 | ``` 22 | 23 | Visit http://localhost:3000 to see the result. 24 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import "../styles/globals.css"; 2 | import type { AppProps } from "next/app"; 3 | import { ThemeProvider } from "next-themes"; 4 | 5 | function MyApp({ Component, pageProps }: AppProps): JSX.Element { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | } 12 | 13 | export default MyApp; 14 | -------------------------------------------------------------------------------- /components/guestbook/Entry.tsx: -------------------------------------------------------------------------------- 1 | import { formatDate } from "../../lib/date"; 2 | 3 | export default function Entry({ name, message, createdAt }): JSX.Element { 4 | return ( 5 |
6 |

7 | {name} on {formatDate(createdAt)} 8 |

9 |

{message}

10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /components/footer.tsx: -------------------------------------------------------------------------------- 1 | import NextLink from "next/link"; 2 | 3 | export default function Footer({ 4 | flavorText = "Made with the entrails of a Wahoo Fish.", 5 | }): JSX.Element { 6 | return ( 7 |
8 |

{flavorText}

9 |

10 | 11 | 12 | This website is open source. 13 | 14 | 15 |

16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules/ 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | 36 | .env -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "postgresql" 10 | url = env("DATABASE_URL") 11 | } 12 | 13 | model guestbook { 14 | id Int @id @default(autoincrement()) 15 | name String @db.VarChar(255) 16 | email String @db.VarChar(255) 17 | message String @db.VarChar(500) 18 | createdAt DateTime @default(now()) 19 | } 20 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "monthly" 12 | -------------------------------------------------------------------------------- /lib/prisma.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | // PrismaClient is attached to the `global` object in development to prevent 4 | // exhausting your database connection limit. 5 | // 6 | // Learn more: 7 | // https://pris.ly/d/help/next-js-best-practices 8 | 9 | let prisma: PrismaClient; 10 | 11 | if (process.env.NODE_ENV === "production") { 12 | prisma = new PrismaClient(); 13 | } else { 14 | if (!global.prisma) { 15 | global.prisma = new PrismaClient(); 16 | } 17 | prisma = global.prisma; 18 | } 19 | 20 | export default prisma; 21 | -------------------------------------------------------------------------------- /pages/api/guestbook/index.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from "next"; 2 | import prisma from "../../../lib/prisma"; 3 | 4 | export const getGuestbook = async ( 5 | req: NextApiRequest, 6 | res: NextApiResponse 7 | ) => { 8 | const guestbook = await prisma.guestbook.findMany({ 9 | orderBy: { 10 | createdAt: "desc", 11 | }, 12 | select: { 13 | id: true, 14 | name: true, 15 | message: true, 16 | createdAt: true, 17 | }, 18 | }); 19 | 20 | res.status(200).json({ 21 | guestbook, 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": false, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "lib/flavor.js"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /pages/api/guestbook/new.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from "next"; 2 | import prisma from "../../../lib/prisma"; 3 | 4 | export default async function handler( 5 | req: NextApiRequest, 6 | res: NextApiResponse 7 | ) { 8 | const { name, message, email } = req.body; 9 | const guestbook = await prisma.guestbook.create({ 10 | data: { 11 | name, 12 | message: message.slice(0, 500), 13 | email, 14 | }, 15 | }); 16 | res.status(200).json({ 17 | id: guestbook.id, 18 | name: guestbook.name, 19 | message: guestbook.message, 20 | created_at: guestbook.createdAt, 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /components/nav.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | const links = [ 4 | { name: "home", destination: "/" }, 5 | // { name: "guestbook", destination: "/book" }, 6 | ]; 7 | 8 | export default function Nav(): JSX.Element { 9 | return ( 10 |
11 | {links.map(({ name, destination }) => ( 12 | 13 | 14 | {name} 15 | 16 | 17 | ))} 18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | settings: { 3 | react: { 4 | version: "detect", 5 | }, 6 | }, 7 | env: { 8 | browser: true, 9 | es2021: true, 10 | node: true, 11 | }, 12 | extends: [ 13 | "eslint:recommended", 14 | "plugin:react/recommended", 15 | "plugin:@typescript-eslint/recommended", 16 | ], 17 | parser: "@typescript-eslint/parser", 18 | parserOptions: { 19 | ecmaFeatures: { 20 | jsx: true, 21 | }, 22 | ecmaVersion: 12, 23 | sourceType: "module", 24 | }, 25 | plugins: ["react", "@typescript-eslint"], 26 | rules: { "react/prop-types": "off", "react/react-in-jsx-scope": "off" }, 27 | }; 28 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const withMDX = require("@next/mdx")({ 2 | extension: /\.mdx?$/, 3 | options: { 4 | remarkPlugins: [], 5 | rehypePlugins: [], 6 | // If you use `MDXProvider`, uncomment the following line. 7 | // providerImportSource: "@mdx-js/react", 8 | }, 9 | }); 10 | 11 | module.exports = withMDX({ 12 | // Append the default value with md extensions 13 | pageExtensions: ["ts", "tsx", "js", "jsx", "md", "mdx"], 14 | }); 15 | 16 | module.exports = { 17 | typescript: { 18 | // !! WARN !! 19 | // Dangerously allow production builds to successfully complete even if 20 | // your project has type errors. 21 | // !! WARN !! 22 | ignoreBuildErrors: true, 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /components/theme.tsx: -------------------------------------------------------------------------------- 1 | import Icon from "supercons"; 2 | import { useTheme } from "next-themes"; 3 | 4 | export default function ThemeToggle(): JSX.Element { 5 | const { theme, setTheme } = useTheme(); 6 | 7 | return ( 8 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /components/layout.tsx: -------------------------------------------------------------------------------- 1 | import Footer from "./footer"; 2 | import Meta from "./meta"; 3 | import Nav from "./nav"; 4 | import ThemeToggle from "./theme"; 5 | 6 | interface LayoutProps { 7 | children: React.ReactNode; 8 | heading: string; 9 | } 10 | 11 | export default function Layout({ 12 | heading, 13 | children, 14 | }: LayoutProps): JSX.Element { 15 | return ( 16 |
17 | 18 | 19 |
20 |
26 |
27 |
28 |
29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&family=IBM+Plex+Sans:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500;1,600&family=Playfair+Display:ital,wght@1,600;1,700;1,800;1,900&display=swap"); 2 | 3 | @tailwind base; 4 | @tailwind components; 5 | @tailwind utilities; 6 | 7 | @layer components { 8 | .link { 9 | @apply text-gray-400 hover:underline; 10 | } 11 | .btn { 12 | @apply px-3 py-1 text-gray-700 bg-gray-200 rounded-md hover:bg-gray-300 dark:bg-blue-400 dark:text-gray-100 max-w-min focus:outline-none focus:border-gray-400 focus:ring-gray-400; 13 | } 14 | .card { 15 | @apply min-w-0 p-8 m-0 overflow-hidden bg-white rounded-md shadow-xl dark:bg-slate-500; 16 | } 17 | .card-interactive { 18 | @apply cursor-pointer; 19 | } 20 | .input { 21 | @apply px-2 py-1 border-2 border-gray-200 rounded-lg dark:border-slate-400 focus:outline-none focus:border-gray-400 focus:ring-1 focus:ring-gray-400; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@exu3/site", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "eslint ./ --ext js,jsx,ts,tsx" 10 | }, 11 | "dependencies": { 12 | "@mdx-js/loader": "^2.1.2", 13 | "@next/mdx": "^12.1.6", 14 | "@prisma/client": "^3.13.0", 15 | "framer-motion": "^6.3.2", 16 | "lodash": "^4.17.21", 17 | "next": "12.1.5", 18 | "next-themes": "^0.1.1", 19 | "prisma": "^3.13.0", 20 | "react": "18.1.0", 21 | "react-dom": "18.0.0", 22 | "supercons": "^0.0.1", 23 | "superjson": "^1.9.1", 24 | "swr": "^1.3.0" 25 | }, 26 | "devDependencies": { 27 | "@types/node": "^17.0.26", 28 | "@types/react": "^18.0.6", 29 | "@typescript-eslint/eslint-plugin": "^5.20.0", 30 | "@typescript-eslint/parser": "^5.20.0", 31 | "autoprefixer": "^10.4.5", 32 | "eslint": "^8.14.0", 33 | "eslint-plugin-react": "^7.29.4", 34 | "postcss": "^8.4.12", 35 | "tailwindcss": "^3.0.24", 36 | "typescript": "^4.6.3" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /components/Meta.tsx: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | 3 | export default function Meta(): JSX.Element { 4 | const title = "Ella"; 5 | const description = "Personal website."; 6 | const keywords = "flying pig"; 7 | const author = "Ella"; 8 | const twitter = "@ella"; 9 | //const image = "/ogimage.png"; // This is your OpenGraph image 10 | return ( 11 | 12 | 13 | 14 | 15 | {title} 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | {/* */} 24 | 25 | 26 | 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /pages/book.tsx: -------------------------------------------------------------------------------- 1 | import Layout from "../components/layout"; 2 | import prisma from "../lib/prisma"; 3 | import Entry from "../components/guestbook/Entry"; 4 | import Form from "../components/guestbook/Form"; 5 | 6 | const GuestBook = ({ records }) => { 7 | return ( 8 | 9 |
10 |
11 |
12 | 13 |
14 | 15 |
16 | {records 17 | .filter((r) => r.message) 18 | .map((r) => ( 19 | 25 | ))} 26 |
27 |
28 | 29 | ); 30 | }; 31 | 32 | export default GuestBook; 33 | 34 | export async function getServerSideProps() { 35 | const data = await prisma.guestbook.findMany({ 36 | orderBy: { 37 | createdAt: "desc", 38 | }, 39 | }); 40 | 41 | const records = data.map((record) => ({ 42 | id: record.id, 43 | name: record.name, 44 | message: record.message, 45 | createdAt: record.createdAt.toISOString(), 46 | })); 47 | 48 | return { props: { records } }; 49 | } 50 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Layout from "../components/layout"; 2 | import Link from "next/link"; 3 | import Image from "next/image"; 4 | import { useState } from "react"; 5 | 6 | const HomePage = () => { 7 | const time = new Date(); 8 | const greeting = time.getHours() < 12 ? "Good morning ☀️" : "Good day 👋"; 9 | 10 | const [hello, setHello] = useState("Hello."); 11 | const hellos = ["Hello.", "Bonjour.", "Hola.", "Hallo.", "你好。"]; 12 | return ( 13 | <> 14 | 15 |
16 |

17 | 19 | setHello(hellos[Math.floor(Math.random() * hellos.length)]) 20 | } 21 | className="cursor-pointer" 22 | > 23 | {hello} 24 | {" "} 25 | My name is Ella. Welcome to my humble internet home. There 26 | isn't much to do here. 27 |

28 | {/*

29 | Perhaps you may want to{" "} 30 | 31 | read the guestbook 32 | 33 | ? 34 |

*/} 35 |
36 | Cat gazing down 44 |
45 | 46 |

Vermont, yeah.

47 |
48 |
49 | 50 | ); 51 | }; 52 | 53 | export default HomePage; 54 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: [ 3 | "./pages/**/*.{js,ts,jsx,tsx}", 4 | "./components/**/*.{js,ts,jsx,tsx}", 5 | ], 6 | darkMode: "class", 7 | theme: { 8 | extend: { 9 | fontFamily: { 10 | playfair: "Playfair Display, serif", 11 | mono: "IBM Plex Mono, monospace", 12 | sans: "IBM Plex Sans, sans-serif", 13 | }, 14 | colors: { 15 | white: "#FFFEFD", 16 | black: "#181818", 17 | }, 18 | keyframes: { 19 | "cursor-party": { 20 | "0%": { 21 | cursor: "grabbing", 22 | }, 23 | "5%": { 24 | cursor: "grab", 25 | }, 26 | "10%": { 27 | cursor: "zoom-out", 28 | }, 29 | "15%": { 30 | cursor: "zoom-in", 31 | }, 32 | "20%": { 33 | cursor: "all-scroll", 34 | }, 35 | "25%": { 36 | cursor: "row-resize", 37 | }, 38 | "30%": { 39 | cursor: "zoom-in", 40 | }, 41 | "35%": { 42 | cursor: "text", 43 | }, 44 | "40%": { 45 | cursor: "crosshair", 46 | }, 47 | "45%": { 48 | cursor: "progress", 49 | }, 50 | "50%": { 51 | cursor: "pointer", 52 | }, 53 | "55%": { 54 | cursor: "context-menu", 55 | }, 56 | "60%": { 57 | cursor: "none", 58 | }, 59 | "65%": { 60 | cursor: "help", 61 | }, 62 | "70%": { 63 | cursor: "vertical-text", 64 | }, 65 | "75%": { 66 | cursor: "alias", 67 | }, 68 | "80%": { 69 | cursor: "copy", 70 | }, 71 | "85%": { 72 | cursor: "move", 73 | }, 74 | "90%": { 75 | cursor: "no-drop", 76 | }, 77 | "95%": { 78 | cursor: "pointer", 79 | }, 80 | "100%": { 81 | cursor: "ew-resize", 82 | }, 83 | }, 84 | }, 85 | animation: { 86 | "cursor-party": "cursor-party 3s infinite ease-in-out", 87 | }, 88 | }, 89 | }, 90 | variants: { 91 | extend: {}, 92 | }, 93 | plugins: [], 94 | }; 95 | -------------------------------------------------------------------------------- /components/guestbook/Form.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | export const GuestbookNew = () => { 4 | const [name, setName] = useState(""); 5 | const [message, setMessage] = useState(""); 6 | const [email, setEmail] = useState(""); 7 | const [error, setError] = useState(""); 8 | const [success, setSuccess] = useState(""); 9 | const [isLoading, setIsLoading] = useState(false); 10 | 11 | const addRecord = async () => { 12 | setIsLoading(true); 13 | setError(""); 14 | setSuccess(""); 15 | }; 16 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 17 | const handleSubmit = async (e: any) => { 18 | e.preventDefault(); 19 | setIsLoading(true); 20 | setError(""); 21 | setSuccess(""); 22 | const res = await fetch("/api/guestbook/new", { 23 | body: JSON.stringify({ 24 | name, 25 | message, 26 | email, 27 | }), 28 | headers: { 29 | "Content-Type": "application/json", 30 | }, 31 | method: "POST", 32 | }); 33 | const data = await res.json(); 34 | if (data.error) { 35 | setError(data.error); 36 | setIsLoading(false); 37 | } else { 38 | setSuccess("Entry added successfully"); 39 | setIsLoading(false); 40 | setName(""); 41 | setMessage(""); 42 | setEmail(""); 43 | } 44 | // refresh page so the entry shows up 45 | window.location.reload(); 46 | }; 47 | return ( 48 | <> 49 |
50 | 51 | 52 | setName(e.target.value)} 58 | required 59 | /> 60 | {/* 61 | setEmail(e.target.value)} /> */} 62 | 63 |