├── .prettierignore
├── public
├── og.png
├── favicon.ico
├── apple-touch-icon.png
├── android-chrome-192x192.png
├── android-chrome-512x512.png
├── site.webmanifest
└── favicon.svg
├── src
├── assets
│ └── fonts
│ │ ├── Geist-Bold.woff2
│ │ ├── Geist-Medium.woff2
│ │ └── Geist-Regular.woff2
├── app
│ ├── api
│ │ ├── auth
│ │ │ └── [...nextauth]
│ │ │ │ └── route.ts
│ │ └── post
│ │ │ └── route.ts
│ ├── providers.tsx
│ ├── layout.tsx
│ └── page.tsx
├── utils
│ ├── cn.ts
│ └── react.ts
├── server
│ ├── auth.ts
│ └── db
│ │ ├── index.ts
│ │ └── schema.ts
├── styles
│ └── globals.css
├── components
│ ├── Skeleton.tsx
│ ├── SigninButton.tsx
│ ├── Messages.tsx
│ ├── Menu.tsx
│ ├── CreatePostWizard.tsx
│ └── Icons.tsx
└── config.ts
├── postcss.config.js
├── next.config.mjs
├── drizzle.config.ts
├── .gitignore
├── prettier.config.mjs
├── tsconfig.json
├── README.md
├── LICENSE
├── .github
└── workflows
│ └── code-check.yml
├── .eslintrc.cjs
├── package.json
└── tailwind.config.ts
/.prettierignore:
--------------------------------------------------------------------------------
1 | pnpm-lock.yaml
--------------------------------------------------------------------------------
/public/og.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SameerJadav/guestbook/HEAD/public/og.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SameerJadav/guestbook/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SameerJadav/guestbook/HEAD/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SameerJadav/guestbook/HEAD/public/android-chrome-192x192.png
--------------------------------------------------------------------------------
/public/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SameerJadav/guestbook/HEAD/public/android-chrome-512x512.png
--------------------------------------------------------------------------------
/src/assets/fonts/Geist-Bold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SameerJadav/guestbook/HEAD/src/assets/fonts/Geist-Bold.woff2
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/src/assets/fonts/Geist-Medium.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SameerJadav/guestbook/HEAD/src/assets/fonts/Geist-Medium.woff2
--------------------------------------------------------------------------------
/src/app/api/auth/[...nextauth]/route.ts:
--------------------------------------------------------------------------------
1 | import { handlers } from "~/server/auth";
2 |
3 | export const { GET, POST } = handlers;
4 |
--------------------------------------------------------------------------------
/src/assets/fonts/Geist-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SameerJadav/guestbook/HEAD/src/assets/fonts/Geist-Regular.woff2
--------------------------------------------------------------------------------
/src/utils/cn.ts:
--------------------------------------------------------------------------------
1 | import type { ClassValue } from "clsx";
2 | import { clsx } from "clsx";
3 | import { twMerge } from "tailwind-merge";
4 |
5 | export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs));
6 |
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | typescript: {
4 | ignoreBuildErrors: true,
5 | },
6 | eslint: {
7 | ignoreDuringBuilds: true,
8 | },
9 | };
10 |
11 | export default nextConfig;
12 |
--------------------------------------------------------------------------------
/src/server/auth.ts:
--------------------------------------------------------------------------------
1 | import type { NextAuthConfig } from "next-auth";
2 | import NextAuth from "next-auth";
3 | import GitHubProvider from "next-auth/providers/github";
4 |
5 | const authOptions: NextAuthConfig = {
6 | providers: [GitHubProvider],
7 | };
8 |
9 | export const { handlers, auth } = NextAuth(authOptions);
10 |
--------------------------------------------------------------------------------
/src/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | html {
6 | color-scheme: dark;
7 | scroll-behavior: smooth;
8 | }
9 |
10 | @media (min-width: 1024px) {
11 | ::-webkit-scrollbar {
12 | width: 6px;
13 | }
14 |
15 | ::-webkit-scrollbar-thumb {
16 | @apply rounded-full bg-gray-3;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/Skeleton.tsx:
--------------------------------------------------------------------------------
1 | import type { ComponentPropsWithRef } from "react";
2 | import { cn } from "~/utils/cn";
3 |
4 | export default function Skeleton({
5 | className,
6 | ...props
7 | }: ComponentPropsWithRef<"div">) {
8 | return (
9 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/src/server/db/index.ts:
--------------------------------------------------------------------------------
1 | import { createClient } from "@libsql/client";
2 | import { drizzle } from "drizzle-orm/libsql";
3 | import * as schema from "./schema";
4 |
5 | const client = createClient({
6 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
7 | url: process.env.DATABASE_URL!,
8 | authToken: process.env.DATABASE_AUTH_TOKEN,
9 | });
10 |
11 | export const db = drizzle(client, { schema });
12 |
--------------------------------------------------------------------------------
/drizzle.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "drizzle-kit";
2 |
3 | const config: Config = {
4 | schema: "./src/server/db/schema.ts",
5 | driver: "turso",
6 | dbCredentials: {
7 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
8 | url: process.env.DATABASE_URL!,
9 | authToken: process.env.DATABASE_AUTH_TOKEN,
10 | },
11 | tablesFilter: ["guestbook_*"],
12 | out: "./drizzle",
13 | };
14 |
15 | export default config;
16 |
--------------------------------------------------------------------------------
/src/components/SigninButton.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { signIn } from "next-auth/react";
4 | import Icons from "~/components/Icons";
5 |
6 | export default function SigninButton() {
7 | return (
8 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/Messages.tsx:
--------------------------------------------------------------------------------
1 | import { desc } from "drizzle-orm";
2 | import { db } from "~/server/db";
3 | import { posts } from "~/server/db/schema";
4 |
5 | export default async function Messages() {
6 | const messages = await db.query.posts.findMany({
7 | limit: 100,
8 | orderBy: [desc(posts.createdAt)],
9 | });
10 | return messages.map(({ id, message, authorName }) => (
11 |
12 |
13 | {authorName || "Some guy/girl"}:
14 | {" "}
15 | {message}
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 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
38 | # turso local database files
39 | *.db*
40 | *.sql
--------------------------------------------------------------------------------
/src/utils/react.ts:
--------------------------------------------------------------------------------
1 | type GetEventHandlers = Extract<
2 | keyof JSX.IntrinsicElements[T],
3 | `on${string}`
4 | >;
5 |
6 | /**
7 | * Provides the event type for a given element and handler.
8 | *
9 | * @example
10 | * ```ts
11 | * type MyEvent = EventFor<"input", "onChange">;
12 | * ```
13 | */
14 | export type EventFor<
15 | TElement extends keyof JSX.IntrinsicElements,
16 | THandler extends GetEventHandlers,
17 | > = JSX.IntrinsicElements[TElement][THandler] extends
18 | | ((e: infer TEvent) => unknown)
19 | | undefined
20 | ? TEvent
21 | : never;
22 |
--------------------------------------------------------------------------------
/src/app/api/post/route.ts:
--------------------------------------------------------------------------------
1 | import { db } from "~/server/db";
2 | import { posts } from "~/server/db/schema";
3 |
4 | interface PostSchema {
5 | authorName: string;
6 | message: string;
7 | }
8 |
9 | export async function POST(req: Request) {
10 | try {
11 | const { authorName, message } = (await req.json()) as PostSchema;
12 |
13 | const result = await db.insert(posts).values({
14 | authorName,
15 | message,
16 | });
17 |
18 | return Response.json(result);
19 | } catch (e) {
20 | // eslint-disable-next-line no-console
21 | console.error(e);
22 | return Response.error();
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/prettier.config.mjs:
--------------------------------------------------------------------------------
1 | /** @typedef {import("prettier").Config} PrettierConfig */
2 | /** @typedef {import("@trivago/prettier-plugin-sort-imports").PluginConfig} SortImportsConfig */
3 |
4 | /** @type { PrettierConfig | SortImportsConfig } */
5 | const config = {
6 | importOrder: [
7 | "^(react/(.*)$)|^(react$)",
8 | "^(next/(.*)$)|^(next$)",
9 | "",
10 | "^(~/utils/(.*)$)|^(~/utils)",
11 | "^~/components/(.*)$",
12 | "^~/config",
13 | "^~/styles/(.*)$",
14 | "^[./]",
15 | ],
16 | plugins: [
17 | "@trivago/prettier-plugin-sort-imports",
18 | "prettier-plugin-tailwindcss",
19 | ],
20 | };
21 |
22 | export default config;
23 |
--------------------------------------------------------------------------------
/public/site.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Guestbook - Sameer Jadav",
3 | "short_name": "Guestbook",
4 | "description": "Beautiful and user-friendly guestbook app. Open source. Performant. Accessible.",
5 | "start_url": "/",
6 | "icons": [
7 | {
8 | "src": "/android-chrome-192x192.png",
9 | "sizes": "192x192",
10 | "type": "image/png",
11 | "purpose": "any maskable"
12 | },
13 | {
14 | "src": "/android-chrome-512x512.png",
15 | "sizes": "512x512",
16 | "type": "image/png",
17 | "purpose": "any maskable"
18 | }
19 | ],
20 | "theme_color": "#111111",
21 | "background_color": "#111111",
22 | "display": "standalone"
23 | }
24 |
--------------------------------------------------------------------------------
/src/config.ts:
--------------------------------------------------------------------------------
1 | export const site = {
2 | name: "Guestbook",
3 | description:
4 | "Beautiful and user-friendly guestbook app. Open source. Performant. Accessible.",
5 | url:
6 | process.env.NODE_ENV === "development"
7 | ? "http://localhost:3000"
8 | : "https://guestbook-sam.vercel.app",
9 | repo: "https://github.com/SameerJadav/guestbook",
10 | author: {
11 | name: "Sameer Jadav",
12 | web: "https://sameerjadav.me",
13 | github: "https://github.com/SameerJadav",
14 | twitter: "https://twitter.com/SameerJadav_",
15 | twitterId: "@SameerJadav_",
16 | linkedin: "https://www.linkedin.com/in/sameerjadav",
17 | mail: "mailto:sameerjdav001@gmail.com",
18 | },
19 | };
20 |
--------------------------------------------------------------------------------
/src/server/db/schema.ts:
--------------------------------------------------------------------------------
1 | import { sql } from "drizzle-orm";
2 | import { integer, sqliteTableCreator, text } from "drizzle-orm/sqlite-core";
3 |
4 | /**
5 | * Using the multi-project schema feature of Drizzle ORM. Use the same database instance for multiple projects.
6 | *
7 | * @see https://orm.drizzle.team/docs/goodies#multi-project-schema
8 | */
9 | const mysqliteTable = sqliteTableCreator((name) => `guestbook_${name}`);
10 |
11 | export const posts = mysqliteTable("posts", {
12 | id: integer("id", { mode: "number" })
13 | .primaryKey({ autoIncrement: true })
14 | .notNull(),
15 | authorName: text("author_name"),
16 | message: text("message").notNull(),
17 | createdAt: text("created_at")
18 | .default(sql`CURRENT_TIMESTAMP`)
19 | .notNull(),
20 | });
21 |
--------------------------------------------------------------------------------
/src/app/providers.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState } from "react";
4 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
5 |
6 | interface ProvidersProps {
7 | children: React.ReactNode;
8 | }
9 |
10 | export default function Providers({ children }: ProvidersProps) {
11 | // eslint-disable-next-line react/hook-use-state
12 | const [queryClient] = useState(
13 | () =>
14 | new QueryClient({
15 | defaultOptions: {
16 | queries: {
17 | // With SSR, we usually want to set some default staleTime
18 | // above 0 to avoid refetching immediately on the client
19 | staleTime: 60 * 1000,
20 | },
21 | },
22 | }),
23 | );
24 |
25 | return (
26 | {children}
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Base Options: */
4 | "esModuleInterop": true,
5 | "skipLibCheck": true,
6 | "target": "ES2022",
7 | "allowJs": true,
8 | "resolveJsonModule": true,
9 | "moduleDetection": "force",
10 | "isolatedModules": true,
11 |
12 | /* Strictness */
13 | "strict": true,
14 | "noUncheckedIndexedAccess": true,
15 |
16 | /* Bundled projects */
17 | "lib": ["DOM", "DOM.Iterable", "ES2022"],
18 | "noEmit": true,
19 | "module": "ESNext",
20 | "moduleResolution": "Bundler",
21 | "jsx": "preserve",
22 | "plugins": [{ "name": "next" }],
23 | "incremental": true,
24 |
25 | /* Path Aliases */
26 | "baseUrl": ".",
27 | "paths": {
28 | "~/*": ["./src/*"],
29 | },
30 | },
31 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
32 | "exclude": ["node_modules"],
33 | }
34 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Guestbook
2 |
3 | A modern, performant, and accessible web application where users can sign in using GitHub to leave messages. Built using Next.js 14, this app uses server-side rendering to deliver content swiftly, ensuring a seamless experience. BTW this app is blazingly fast ([PageSpeed Insights](https://pagespeed.web.dev/analysis/https-guestbook-sameerjadav-me/gbcv3to95b?form_factor=mobile)).
4 |
5 | ## Tech Stack
6 |
7 | - **Framework:** [Next.js](https://nextjs.org)
8 | - **Databases:** [Turso](https://turso.tech)
9 | - **ORM:** [Drizzle ORM](https://orm.drizzle.team)
10 | - **CSS Framework:** [Tailwind CSS](https://tailwindcss.com)
11 | - **Color System:** [Radix](https://www.radix-ui.com/colors)
12 | - **Deployment:** [Vercel](https://vercel.com)
13 | - **Analytics:** [Vercel Analytics](https://vercel.com/analytics)
14 |
15 | ## Contributing
16 |
17 | If you'd like to contribute, please feel free to open an issue or submit a pull request. I welcome your suggestions and improvements!
18 |
19 | ## License
20 |
21 | Guestbook is open-source and available under the [MIT License](./LICENSE).
22 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Sameer Jadav
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/.github/workflows/code-check.yml:
--------------------------------------------------------------------------------
1 | name: Code check
2 |
3 | on:
4 | pull_request:
5 | branches: ["*"]
6 | push:
7 | branches: ["*"]
8 |
9 | jobs:
10 | typecheck-lint:
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - name: Checkout repo
15 | uses: actions/checkout@v3
16 |
17 | - name: Setup Node 20
18 | uses: actions/setup-node@v3
19 | with:
20 | node-version: 20
21 |
22 | - name: Setup pnpm
23 | uses: pnpm/action-setup@v2.2.4
24 | with:
25 | version: 8.12.0
26 |
27 | - name: Get pnpm store directory
28 | id: pnpm-cache
29 | run: |
30 | echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
31 |
32 | - name: Setup pnpm cache
33 | uses: actions/cache@v3
34 | with:
35 | path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
36 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
37 | restore-keys: |
38 | ${{ runner.os }}-pnpm-store-
39 |
40 | - name: Install deps (with cache)
41 | run: pnpm install
42 |
43 | - name: Run lint
44 | run: pnpm run lint
45 |
46 | - name: Run typecheck
47 | run: pnpm run typecheck
48 |
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | const { resolve } = require("node:path");
2 |
3 | const project = resolve(process.cwd(), "tsconfig.json");
4 |
5 | /** @type {import("eslint").Linter.Config} */
6 | const config = {
7 | extends: [
8 | require.resolve("@vercel/style-guide/eslint/node"),
9 | require.resolve("@vercel/style-guide/eslint/browser"),
10 | require.resolve("@vercel/style-guide/eslint/typescript"),
11 | require.resolve("@vercel/style-guide/eslint/react"),
12 | require.resolve("@vercel/style-guide/eslint/next"),
13 | ],
14 | parserOptions: {
15 | project,
16 | },
17 | globals: {
18 | React: true,
19 | JSX: true,
20 | },
21 | settings: {
22 | "import/resolver": {
23 | typescript: {
24 | project,
25 | },
26 | },
27 | },
28 | ignorePatterns: ["node_modules/", "dist/"],
29 | rules: {
30 | "unicorn/filename-case": "off",
31 | "eslint-comments/require-description": "off",
32 | "@typescript-eslint/explicit-function-return-type": "off",
33 | "react/button-has-type": "off",
34 | "import/no-default-export": "off",
35 | "import/no-extraneous-dependencies": [
36 | "error",
37 | { includeInternal: false, includeTypes: true },
38 | ],
39 | },
40 | };
41 |
42 | module.exports = config;
43 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "guestbook",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start --port 5000",
9 | "lint": "prettier --check . && next lint",
10 | "format": "prettier --write .",
11 | "typecheck": "tsc --noEmit"
12 | },
13 | "dependencies": {
14 | "@libsql/client": "0.4.0-pre.7",
15 | "@radix-ui/colors": "^3.0.0",
16 | "@radix-ui/react-dialog": "^1.0.5",
17 | "@tanstack/react-query": "^5.17.19",
18 | "@vercel/analytics": "^1.1.2",
19 | "clsx": "^2.1.0",
20 | "drizzle-orm": "^0.29.3",
21 | "next": "14.1.0",
22 | "next-auth": "5.0.0-beta.5",
23 | "react": "^18",
24 | "react-dom": "^18",
25 | "tailwind-merge": "^2.2.1"
26 | },
27 | "devDependencies": {
28 | "@trivago/prettier-plugin-sort-imports": "^4.3.0",
29 | "@types/node": "^20",
30 | "@types/react": "^18",
31 | "@types/react-dom": "^18",
32 | "@vercel/style-guide": "^5.1.0",
33 | "autoprefixer": "^10.0.1",
34 | "drizzle-kit": "^0.20.13",
35 | "eslint": "^8",
36 | "eslint-config-next": "14.1.0",
37 | "postcss": "^8",
38 | "prettier": "^3.2.4",
39 | "prettier-plugin-tailwindcss": "^0.5.11",
40 | "tailwindcss": "^3.3.0",
41 | "typescript": "^5"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
13 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import { grayDark, blue, red, green, yellow } from "@radix-ui/colors";
2 | import type { Config } from "tailwindcss";
3 | import { fontFamily } from "tailwindcss/defaultTheme";
4 |
5 | const config: Config = {
6 | content: ["./src/**/*.tsx"],
7 | theme: {
8 | extend: {
9 | fontFamily: {
10 | sans: ["var(--font-geist)", ...fontFamily.sans],
11 | },
12 | colors: {
13 | gray: {
14 | 1: grayDark.gray1,
15 | 2: grayDark.gray2,
16 | 3: grayDark.gray3,
17 | 4: grayDark.gray4,
18 | 5: grayDark.gray5,
19 | 6: grayDark.gray6,
20 | 7: grayDark.gray7,
21 | 8: grayDark.gray8,
22 | 9: grayDark.gray9,
23 | 10: grayDark.gray10,
24 | 11: grayDark.gray11,
25 | 12: grayDark.gray12,
26 | },
27 | blue: {
28 | 1: blue.blue1,
29 | 2: blue.blue2,
30 | 3: blue.blue3,
31 | 4: blue.blue4,
32 | 5: blue.blue5,
33 | 6: blue.blue6,
34 | 7: blue.blue7,
35 | 8: blue.blue8,
36 | 9: blue.blue9,
37 | 10: blue.blue10,
38 | 11: blue.blue11,
39 | 12: blue.blue12,
40 | },
41 | red: {
42 | 1: red.red1,
43 | 2: red.red2,
44 | 3: red.red3,
45 | 4: red.red4,
46 | 5: red.red5,
47 | 6: red.red6,
48 | 7: red.red7,
49 | 8: red.red8,
50 | 9: red.red9,
51 | 10: red.red10,
52 | 11: red.red11,
53 | 12: red.red12,
54 | },
55 | yellow: {
56 | 1: yellow.yellow1,
57 | 2: yellow.yellow2,
58 | 3: yellow.yellow3,
59 | 4: yellow.yellow4,
60 | 5: yellow.yellow5,
61 | 6: yellow.yellow6,
62 | 7: yellow.yellow7,
63 | 8: yellow.yellow8,
64 | 9: yellow.yellow9,
65 | 10: yellow.yellow10,
66 | 11: yellow.yellow11,
67 | 12: yellow.yellow12,
68 | },
69 | green: {
70 | 1: green.green1,
71 | 2: green.green2,
72 | 3: green.green3,
73 | 4: green.green4,
74 | 5: green.green5,
75 | 6: green.green6,
76 | 7: green.green7,
77 | 8: green.green8,
78 | 9: green.green9,
79 | 10: green.green10,
80 | 11: green.green11,
81 | 12: green.green12,
82 | },
83 | },
84 | keyframes: {
85 | "fade-in": {
86 | from: { opacity: "0" },
87 | to: { opacity: "1" },
88 | },
89 | },
90 | animation: {
91 | "fade-in": "fade-in 150ms ease-in",
92 | },
93 | },
94 | },
95 | future: {
96 | hoverOnlyWhenSupported: true,
97 | },
98 | plugins: [],
99 | };
100 |
101 | export default config;
102 |
--------------------------------------------------------------------------------
/src/components/Menu.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import type { ComponentPropsWithoutRef } from "react";
4 | import { Close, Content, Root, Trigger } from "@radix-ui/react-dialog";
5 | import Icons from "~/components/Icons";
6 | import { site } from "~/config";
7 |
8 | export default function Menu() {
9 | return (
10 |
11 |
12 |
16 |
17 |
18 |
19 |
20 |
Sign my Guestbook
21 |
22 |
26 |
27 |
28 |
29 |
30 |
31 | Website
32 |
33 |
34 |
35 | GitHub
36 |
37 |
38 |
39 | Mail
40 |
41 |
42 |
43 | Twitter
44 |
45 |
46 |
47 | LinkedIn
48 |
49 |
50 |
51 | Source code
52 |
53 |
54 |
55 |
Avaliable for work
56 |
57 |
58 |
59 |
60 |
61 | );
62 | }
63 |
64 | interface ExternalLinkProps extends ComponentPropsWithoutRef<"a"> {
65 | href: string;
66 | children: React.ReactNode;
67 | }
68 |
69 | function ExternalLink({ href, children }: ExternalLinkProps) {
70 | return (
71 |
77 | {children}
78 |
79 | );
80 | }
81 |
--------------------------------------------------------------------------------
/src/components/CreatePostWizard.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState } from "react";
4 | import { useRouter } from "next/navigation";
5 | import { useMutation } from "@tanstack/react-query";
6 | import { signOut } from "next-auth/react";
7 | import { cn } from "~/utils/cn";
8 | import type { EventFor } from "~/utils/react";
9 |
10 | interface CreatePostWizardProps {
11 | authorName: string | null | undefined;
12 | }
13 |
14 | export default function CreatePostWizard({
15 | authorName,
16 | }: CreatePostWizardProps) {
17 | const [message, setMessage] = useState("");
18 | const router = useRouter();
19 |
20 | const { mutate, isPending, error } = useMutation({
21 | mutationFn: () =>
22 | fetch("/api/post", {
23 | method: "POST",
24 | headers: {
25 | "Content-Type": "application/json",
26 | },
27 | body: JSON.stringify({
28 | authorName,
29 | message: message.trim(),
30 | }),
31 | }),
32 | onSuccess: () => {
33 | router.refresh();
34 | setMessage("");
35 | },
36 | });
37 |
38 | // eslint-disable-next-line no-console
39 | if (error) console.error(error);
40 |
41 | function addMessage(e: EventFor<"button", "onClick">) {
42 | e.preventDefault();
43 | if (message.trim() !== "") {
44 | mutate();
45 | }
46 | }
47 |
48 | function addMessageOnEnter(e: EventFor<"input", "onKeyDown">) {
49 | if (e.ctrlKey && e.key === "Enter" && message.trim() !== "") {
50 | e.preventDefault();
51 | mutate();
52 | }
53 | }
54 |
55 | return (
56 |
57 |
79 |
86 |
87 | Something went wrong. Try again later.
88 |
89 |
92 | Posting your message...
93 |
94 |
100 |
101 |
102 | );
103 | }
104 |
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata, Viewport } from "next";
2 | import localfont from "next/font/local";
3 | import { Analytics } from "@vercel/analytics/react";
4 | import Providers from "~/app/providers";
5 | import { cn } from "~/utils/cn";
6 | import { site } from "~/config";
7 | import "~/styles/globals.css";
8 |
9 | interface RootLayoutProps {
10 | children: React.ReactNode;
11 | }
12 |
13 | const geist = localfont({
14 | src: [
15 | {
16 | path: "../assets/fonts/Geist-Regular.woff2",
17 | style: "normal",
18 | weight: "400",
19 | },
20 | {
21 | path: "../assets/fonts/Geist-Medium.woff2",
22 | style: "normal",
23 | weight: "500",
24 | },
25 | {
26 | path: "../assets/fonts/Geist-Bold.woff2",
27 | style: "normal",
28 | weight: "700",
29 | },
30 | ],
31 | variable: "--font-geist",
32 | });
33 |
34 | const title = `${site.name} - ${site.author.name}`;
35 | const description = site.description;
36 | const url = site.url;
37 | const author = site.author;
38 |
39 | export const metadata: Metadata = {
40 | title,
41 | description,
42 | applicationName: title,
43 | generator: "Next.js",
44 | authors: [{ name: author.name, url: author.web }],
45 | creator: author.name,
46 | publisher: author.name,
47 | metadataBase: new URL(url),
48 | keywords: [
49 | "Sameer Jadav",
50 | "Guestbook",
51 | "Typescript",
52 | "Full-stack Developer",
53 | "Next.js",
54 | ],
55 | icons: [
56 | {
57 | rel: "icon",
58 | url: "/favicon.ico",
59 | },
60 | {
61 | rel: "icon",
62 | url: "/favicon.svg",
63 | type: "image/svg+xml",
64 | },
65 | {
66 | rel: "apple-touch-icon",
67 | url: "/apple-touch-icon.png",
68 | },
69 | ],
70 | formatDetection: {
71 | email: false,
72 | address: false,
73 | telephone: false,
74 | },
75 | openGraph: {
76 | type: "website",
77 | locale: "en_US",
78 | url: "/",
79 | title,
80 | description,
81 | siteName: title,
82 | images: {
83 | url: "/og.png",
84 | width: 1200,
85 | height: 630,
86 | alt: title,
87 | },
88 | },
89 | twitter: {
90 | card: "summary_large_image",
91 | title,
92 | description,
93 | images: {
94 | url: "/og.png",
95 | width: 1200,
96 | height: 630,
97 | alt: title,
98 | },
99 | creator: author.twitterId,
100 | site: author.twitterId,
101 | },
102 | robots: {
103 | index: true,
104 | follow: true,
105 | googleBot: {
106 | index: true,
107 | follow: true,
108 | "max-video-preview": -1,
109 | "max-image-preview": "large",
110 | "max-snippet": -1,
111 | },
112 | },
113 | manifest: `${url}/site.webmanifest`,
114 | alternates: { canonical: "/" },
115 | };
116 |
117 | export const viewport: Viewport = {
118 | themeColor: "#111111",
119 | colorScheme: "dark",
120 | };
121 |
122 | export default function RootLayout({ children }: RootLayoutProps) {
123 | return (
124 |
125 |
128 | {children}
129 |
130 |
131 |
132 | );
133 | }
134 |
--------------------------------------------------------------------------------
/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | import type { ComponentPropsWithoutRef } from "react";
2 | import dynamic from "next/dynamic";
3 | import { auth } from "~/server/auth";
4 | import Icons from "~/components/Icons";
5 | import Messages from "~/components/Messages";
6 | import Skeleton from "~/components/Skeleton";
7 | import { site } from "~/config";
8 |
9 | const CreatePostWizard = dynamic(
10 | () => import("~/components/CreatePostWizard"),
11 | {
12 | ssr: false,
13 | loading: () => (
14 |
15 |
16 |
17 |
18 | ),
19 | },
20 | );
21 |
22 | const SigninButton = dynamic(() => import("~/components/SigninButton"), {
23 | ssr: false,
24 | loading: () => ,
25 | });
26 |
27 | const Menu = dynamic(() => import("~/components/Menu"), {
28 | ssr: false,
29 | loading: () => ,
30 | });
31 |
32 | export default async function page() {
33 | const session = await auth();
34 | return (
35 |
36 |
37 |
38 |
39 |
40 | Contacts
41 |
42 |
43 |
44 |
45 | Website
46 |
47 |
48 |
49 | GitHub
50 |
51 |
52 |
53 | Mail
54 |
55 |
56 |
57 | Twitter
58 |
59 |
60 |
61 | LinkedIn
62 |
63 |
64 |
65 |
80 |
81 |
82 |
83 |
Sign my Guestbook
84 |
85 |
86 | {session?.user ? (
87 |
88 | ) : (
89 |
90 |
91 |
92 | )}
93 |
94 |
95 |
96 |
97 |
98 |
99 | );
100 | }
101 |
102 | interface ExternalLinkProps extends ComponentPropsWithoutRef<"a"> {
103 | href: string;
104 | children: React.ReactNode;
105 | }
106 |
107 | function ExternalLink({ href, children }: ExternalLinkProps) {
108 | return (
109 |
115 | {children}
116 |
117 | );
118 | }
119 |
--------------------------------------------------------------------------------
/src/components/Icons.tsx:
--------------------------------------------------------------------------------
1 | import type { ComponentPropsWithoutRef } from "react";
2 |
3 | type IconProps = ComponentPropsWithoutRef<"svg">;
4 |
5 | const Icons = {
6 | Cross: ({ ...props }: IconProps) => (
7 |
22 | ),
23 | HamburgerMenu: ({ ...props }: IconProps) => (
24 |
39 | ),
40 | Github: ({ ...props }: IconProps) => (
41 |
56 | ),
57 | Twitter: ({ ...props }: IconProps) => (
58 |
73 | ),
74 | LinkedIn: ({ ...props }: IconProps) => (
75 |
90 | ),
91 | Mail: ({ ...props }: IconProps) => (
92 |
107 | ),
108 | Link: ({ ...props }: IconProps) => (
109 |
124 | ),
125 | Code: ({ ...props }: IconProps) => (
126 |
141 | ),
142 | };
143 |
144 | export default Icons;
145 |
--------------------------------------------------------------------------------