├── .env.example ├── .eslintrc.json ├── .gitignore ├── .npmrc ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── prisma └── schema.prisma ├── public └── favicon.ico ├── src ├── components │ ├── AuthGuard.tsx │ ├── Comments │ │ ├── Form.tsx │ │ ├── List.tsx │ │ └── index.tsx │ ├── Navbar.tsx │ └── Post.tsx ├── entry-client.tsx ├── entry-server.tsx ├── env │ ├── client.ts │ ├── schema.ts │ └── server.ts ├── root.css ├── root.tsx ├── routes │ ├── account.tsx │ ├── api │ │ ├── auth │ │ │ └── [...solidauth].ts │ │ └── trpc │ │ │ └── [trpc].ts │ ├── index.tsx │ ├── p │ │ └── [id].tsx │ └── submit.tsx ├── server │ ├── auth.ts │ ├── db │ │ └── client.ts │ └── trpc │ │ ├── context.ts │ │ ├── router │ │ ├── _app.ts │ │ ├── comments.ts │ │ └── posts.ts │ │ └── utils.ts └── utils │ ├── auth.ts │ ├── format-comments.ts │ └── trpc.ts ├── tsconfig.json ├── uno.config.ts └── vite.config.ts /.env.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL= 2 | GITHUB_CLIENT_SECRET= 3 | GITHUB_CLIENT_ID= 4 | VITE_SESSION_SECRET= 5 | SITE_URL= -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "project": "./tsconfig.json" 5 | }, 6 | "plugins": ["@typescript-eslint"], 7 | "extends": [ 8 | "plugin:solid/typescript", 9 | "plugin:@typescript-eslint/recommended" 10 | ], 11 | "rules": { 12 | "@typescript-eslint/consistent-type-imports": "warn" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | .solid 3 | .output 4 | .vercel 5 | .netlify 6 | netlify 7 | 8 | # dependencies 9 | /node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | *.launch 16 | .settings/ 17 | 18 | # Temp 19 | gitignore 20 | 21 | # System Files 22 | .DS_Store 23 | Thumbs.db 24 | 25 | .env 26 | db.sqlite 27 | beSchema.txt -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | enable-pre-post-scripts=true -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Shoubhit Dash 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | A simple [Hacker News](https://news.ycombinator.com/) clone built using [SolidStart](https://start.solidjs.com), [UnoCSS](https://uno.antfu.me), [tRPC](https://trpc.io), [Solid Auth](https://github.com/OrJDev/solid-auth), [Prisma](https://prisma.io), [PlanetScale](https://planetscale.com), bootstrapped using [create-jd-app](https://github.com/OrJDev/create-jd-app) and hosted on [Vercel](https://vercel.com). 4 | 5 | This is mostly a learning project to try out SolidStart and tRPC together. 6 | 7 | ## Features 8 | 9 | - [x] Authentication 10 | - [x] Nested comments 11 | - [ ] Better error handling 12 | - [ ] Upvote/Downvote 13 | - [ ] Pagination 14 | - [ ] Optimize queries 15 | - [ ] Search 16 | - [ ] Meta tags 17 | - [ ] Deploy on the edge 18 | 19 | ## Credits 20 | 21 | Huge shoutout to [OrJDev](https://github.com/OrJDev) for making the SolidJS ecosystem better with his amazing libraries and putting up with my issues. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test", 3 | "scripts": { 4 | "dev": "solid-start dev", 5 | "build": "solid-start build", 6 | "start": "solid-start start", 7 | "lint": "eslint --fix \"**/*.{ts,tsx,js,jsx}\"", 8 | "push": "prisma db push", 9 | "generate": "prisma generate", 10 | "postinstall": "prisma generate", 11 | "postbuild": "cp node_modules/@prisma/engines/*query* .vercel/output/functions/render.func/ && cp prisma/schema.prisma .vercel/output/functions/render.func/" 12 | }, 13 | "type": "module", 14 | "devDependencies": { 15 | "@typescript-eslint/eslint-plugin": "^5.42.1", 16 | "@typescript-eslint/parser": "^5.42.1", 17 | "@unocss/reset": "^0.46.5", 18 | "eslint": "^8.27.0", 19 | "eslint-plugin-solid": "^0.8.0", 20 | "prisma": "^4.6.1", 21 | "solid-start-node": "^0.2.1", 22 | "solid-start-vercel": "^0.2.5", 23 | "typescript": "^4.8.3", 24 | "unocss": "^0.46.5", 25 | "vite": "^3.1.0" 26 | }, 27 | "dependencies": { 28 | "@prisma/client": "^4.6.1", 29 | "@solidjs/meta": "^0.28.0", 30 | "@solidjs/router": "^0.5.0", 31 | "@tanstack/solid-query": "^4.15.1", 32 | "@trpc/client": "^10.0.0", 33 | "@trpc/server": "^10.0.0", 34 | "clsx": "^1.2.1", 35 | "date-fns": "^2.29.3", 36 | "dotenv": "^16.0.3", 37 | "solid-auth": "^0.0.1", 38 | "solid-js": "^1.5.7", 39 | "solid-start": "^0.2.1", 40 | "solid-start-trpc": "^0.0.13", 41 | "solid-trpc": "0.0.11-rc.2", 42 | "undici": "5.11.0", 43 | "zod": "^3.19.1" 44 | }, 45 | "engines": { 46 | "node": ">=16" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | previewFeatures = ["referentialIntegrity"] 4 | } 5 | 6 | datasource db { 7 | provider = "mysql" 8 | url = env("DATABASE_URL") 9 | referentialIntegrity = "prisma" 10 | } 11 | 12 | model Post { 13 | id String @id @default(cuid()) 14 | createdAt DateTime @default(now()) 15 | updatedAt DateTime @updatedAt 16 | title String @db.VarChar(255) 17 | description String? @db.Text 18 | link String @db.Text 19 | User User? @relation("PostedBy", fields: [userId], references: [id]) 20 | userId String? 21 | Comment Comment[] 22 | } 23 | 24 | model User { 25 | id String @id 26 | displayName String 27 | posts Post[] @relation("PostedBy") 28 | Comment Comment[] 29 | } 30 | 31 | model Comment { 32 | id String @id @default(cuid()) 33 | createdAt DateTime @default(now()) 34 | updatedAt DateTime @updatedAt 35 | text String @db.VarChar(1000) 36 | User User @relation(fields: [userId], references: [id]) 37 | userId String 38 | Post Post @relation(fields: [postId], references: [id]) 39 | postId String 40 | children Comment[] @relation("CommentChildren") 41 | parent Comment? @relation("CommentChildren", fields: [parentId], references: [id], onDelete: NoAction, onUpdate: NoAction) 42 | parentId String? 43 | } 44 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nexxeln/hackernews/96d2e4dfc0ecaa47731e377d93eccb8557586722/public/favicon.ico -------------------------------------------------------------------------------- /src/components/AuthGuard.tsx: -------------------------------------------------------------------------------- 1 | import { useRouteData } from "solid-start"; 2 | import { createServerData$, redirect } from "solid-start/server"; 3 | import { Match, Switch, type Component } from "solid-js"; 4 | import { type User } from "@prisma/client"; 5 | 6 | import { authenticator } from "~/server/auth"; 7 | 8 | export const AuthGuard = (Component: ProtectedRouter) => { 9 | const routeData = () => { 10 | return createServerData$(async (_, { request }) => { 11 | const user = await authenticator.isAuthenticated(request); 12 | if (!user) { 13 | throw redirect("/account"); 14 | } 15 | return user; 16 | }); 17 | }; 18 | return { 19 | routeData, 20 | Page: () => { 21 | const current = useRouteData(); 22 | return ( 23 | }> 24 | 25 |

Loading...

26 |
27 |
28 | ); 29 | }, 30 | }; 31 | }; 32 | 33 | export type ProtectedRouter = Component; 34 | -------------------------------------------------------------------------------- /src/components/Comments/Form.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigate } from "solid-start"; 2 | import { createSignal, Show, type Component } from "solid-js"; 3 | 4 | import { trpc } from "~/utils/trpc"; 5 | 6 | export const CommentForm: Component<{ id: string; parentId?: string }> = ( 7 | props 8 | ) => { 9 | const navigate = useNavigate(); 10 | const [text, setText] = createSignal(""); 11 | 12 | const utils = trpc.useContext(); 13 | const createPost = trpc.comments.create.useMutation({ 14 | onSuccess: () => { 15 | setText(""); 16 | utils.comments.getAll.invalidate({ id: props.id }); 17 | }, 18 | onError: (error) => { 19 | error.data?.code === "UNAUTHORIZED" && navigate("/account"); 20 | }, 21 | }); 22 | 23 | return ( 24 |
25 |
{ 28 | event.preventDefault(); 29 | 30 | createPost.mutate({ 31 | id: props.id, 32 | text: text(), 33 | parentId: props.parentId, 34 | }); 35 | }} 36 | > 37 | setText(event.currentTarget.value)} 45 | /> 46 | 47 | 56 |
57 |
58 | ); 59 | }; 60 | -------------------------------------------------------------------------------- /src/components/Comments/List.tsx: -------------------------------------------------------------------------------- 1 | import { useParams } from "solid-start"; 2 | import { createSignal, For, Show, type Component } from "solid-js"; 3 | import { formatDistanceToNow } from "date-fns"; 4 | 5 | import type { CommentWithChildren } from "~/server/trpc/router/_app"; 6 | import { CommentForm } from "./Form"; 7 | 8 | export const CommentCard: Component<{ comment: CommentWithChildren }> = ( 9 | props 10 | ) => { 11 | const { id } = useParams(); 12 | const [replying, setReplying] = createSignal(false); 13 | 14 | return ( 15 |
16 |

{props.comment.text}

17 |
18 | by {props.comment.User.displayName} 19 | 20 | 21 | {formatDistanceToNow(new Date(props.comment.createdAt))} ago 22 | 23 | 24 | 32 |
33 | 34 | 35 | 36 | 37 | 38 | 0}> 39 |
40 | 41 |
42 |
43 |
44 | ); 45 | }; 46 | 47 | export const ListComments: Component<{ comments: CommentWithChildren[] }> = ( 48 | props 49 | ) => { 50 | return ( 51 |
52 | 53 | {(comment) => ( 54 |
55 | 56 |
57 | )} 58 |
59 |
60 | ); 61 | }; 62 | -------------------------------------------------------------------------------- /src/components/Comments/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 2 | import { type Component, Switch, Match } from "solid-js"; 3 | 4 | import { formatComments } from "~/utils/format-comments"; 5 | import { trpc } from "~/utils/trpc"; 6 | import { ListComments } from "./List"; 7 | 8 | export const CommentSection: Component<{ id: string }> = (props) => { 9 | const comments = trpc.comments.getAll.useQuery(() => ({ id: props.id })); 10 | 11 | return ( 12 |
13 | Something went wrong

}> 14 | 15 |

Loading comments...

16 |
17 | 18 | {(comments) => ( 19 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 20 | 21 | )} 22 | 23 |
24 |
25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /src/components/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import { A } from "solid-start"; 2 | import type { Component } from "solid-js"; 3 | 4 | const NavItem: Component<{ href: string; text: string }> = (props) => { 5 | return ( 6 | 7 | {props.text} 8 | 9 | ); 10 | }; 11 | 12 | export const Navbar = () => { 13 | return ( 14 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /src/components/Post.tsx: -------------------------------------------------------------------------------- 1 | import { A } from "solid-start"; 2 | import { type Component, Show } from "solid-js"; 3 | import { formatDistanceToNow, formatRFC7231 } from "date-fns"; 4 | 5 | export const PostCard: Component<{ 6 | id: string; 7 | title: string; 8 | link: string; 9 | // fix this 10 | username: string | undefined; 11 | createdAt: string; 12 | }> = (props) => { 13 | return ( 14 | 35 | ); 36 | }; 37 | 38 | export const Post: Component<{ 39 | title: string; 40 | link: string; 41 | description: string | null; 42 | // fix this 43 | username: string | undefined; 44 | comments: number; 45 | createdAt: string; 46 | }> = (props) => { 47 | return ( 48 |
49 |
50 | 55 | {props.title} 56 | 57 | 58 | 59 |

{props.description}

60 |
61 | 62 |
63 | by {props.username} 64 | 65 | {formatRFC7231(new Date(props.createdAt))} 66 | 67 | {props.comments} comments 68 |
69 |
70 |
71 | ); 72 | }; 73 | -------------------------------------------------------------------------------- /src/entry-client.tsx: -------------------------------------------------------------------------------- 1 | import { mount, StartClient } from "solid-start/entry-client"; 2 | 3 | import { client, queryClient, trpc } from "./utils/trpc"; 4 | 5 | mount( 6 | () => ( 7 | 8 |
13 | 14 |
15 |
16 | ), 17 | document 18 | ); 19 | -------------------------------------------------------------------------------- /src/entry-server.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | StartServer, 3 | createHandler, 4 | renderAsync, 5 | } from "solid-start/entry-server"; 6 | 7 | export default createHandler( 8 | renderAsync((event) => ) 9 | ); 10 | -------------------------------------------------------------------------------- /src/env/client.ts: -------------------------------------------------------------------------------- 1 | import type { ZodFormattedError } from "zod"; 2 | 3 | import { clientSchema } from "./schema"; 4 | 5 | export const formatErrors = ( 6 | errors: ZodFormattedError, string> 7 | ) => 8 | Object.entries(errors) 9 | .map(([name, value]) => { 10 | if (value && "_errors" in value) 11 | return `${name}: ${value._errors.join(", ")}\n`; 12 | }) 13 | .filter(Boolean); 14 | 15 | const env = clientSchema.safeParse(import.meta.env); 16 | 17 | if (env.success === false) { 18 | console.error( 19 | "❌ Invalid environment variables:\n", 20 | ...formatErrors(env.error.format()) 21 | ); 22 | throw new Error("Invalid environment variables"); 23 | } 24 | 25 | export const clientEnv = env.data; 26 | -------------------------------------------------------------------------------- /src/env/schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const serverSchema = z.object({ 4 | NODE_ENV: z 5 | .enum(["development", "production", "test"]) 6 | .default("development"), 7 | DATABASE_URL: z.string(), 8 | GITHUB_CLIENT_ID: z.string(), 9 | GITHUB_CLIENT_SECRET: z.string(), 10 | SITE_URL: z.string(), 11 | }); 12 | 13 | export const clientSchema = z.object({ 14 | MODE: z.enum(["development", "production", "test"]).default("development"), 15 | VITE_SESSION_SECRET: z.string(), 16 | }); 17 | -------------------------------------------------------------------------------- /src/env/server.ts: -------------------------------------------------------------------------------- 1 | import type { ZodFormattedError } from "zod"; 2 | 3 | import { serverSchema } from "./schema"; 4 | 5 | export const formatErrors = ( 6 | errors: ZodFormattedError, string> 7 | ) => 8 | Object.entries(errors) 9 | .map(([name, value]) => { 10 | if (value && "_errors" in value) 11 | return `${name}: ${value._errors.join(", ")}\n`; 12 | }) 13 | .filter(Boolean); 14 | 15 | const env = serverSchema.safeParse(process.env); 16 | 17 | if (env.success === false) { 18 | console.error( 19 | "❌ Invalid environment variables:\n", 20 | ...formatErrors(env.error.format()) 21 | ); 22 | throw new Error("Invalid environment variables"); 23 | } 24 | 25 | export const serverEnv = env.data; 26 | -------------------------------------------------------------------------------- /src/root.css: -------------------------------------------------------------------------------- 1 | html { 2 | color-scheme: dark; 3 | } 4 | 5 | body { 6 | --at-apply: bg-neutral-9 text-neutral-1 font-sans; 7 | } 8 | -------------------------------------------------------------------------------- /src/root.tsx: -------------------------------------------------------------------------------- 1 | // @refresh reload 2 | import { 3 | Body, 4 | ErrorBoundary, 5 | FileRoutes, 6 | Head, 7 | Html, 8 | Meta, 9 | Routes, 10 | Scripts, 11 | Title, 12 | } from "solid-start"; 13 | import { Suspense } from "solid-js"; 14 | import "uno.css"; 15 | import "@unocss/reset/tailwind.css"; 16 | 17 | import { Navbar } from "~/components/Navbar"; 18 | import "./root.css"; 19 | 20 | export default function Root() { 21 | return ( 22 | 23 | 24 | Hacker News 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
33 | 34 | 35 | 36 |
37 |
38 |
39 | 40 | 41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/routes/account.tsx: -------------------------------------------------------------------------------- 1 | import { useRouteData } from "solid-start"; 2 | import { Match, Switch } from "solid-js"; 3 | import { createServerData$ } from "solid-start/server"; 4 | 5 | import { authenticator } from "~/server/auth"; 6 | import { authClient } from "~/utils/auth"; 7 | 8 | export const routeData = () => { 9 | return { 10 | user: createServerData$(async (_, { request }) => { 11 | const user = await authenticator.isAuthenticated(request); 12 | return user; 13 | }), 14 | }; 15 | }; 16 | 17 | export default function Account() { 18 | const { user } = useRouteData(); 19 | 20 | return ( 21 | <> 22 | 26 | authClient.login("github", { 27 | successRedirect: "/account", 28 | failureRedirect: "/account", 29 | }) 30 | } 31 | > 32 | Login with GitHub 33 | 34 | } 35 | > 36 | 37 |
38 | Hi {user()?.displayName}! 39 | 48 |
49 |
50 |
51 | 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /src/routes/api/auth/[...solidauth].ts: -------------------------------------------------------------------------------- 1 | import { createSolidAuthHandler } from "solid-auth"; 2 | import { type User } from "@prisma/client"; 3 | 4 | import { authenticator } from "~/server/auth"; 5 | 6 | const handler = createSolidAuthHandler(authenticator); 7 | 8 | export const POST = handler; 9 | export const GET = handler; 10 | -------------------------------------------------------------------------------- /src/routes/api/trpc/[trpc].ts: -------------------------------------------------------------------------------- 1 | import { createSolidAPIHandler } from "solid-start-trpc"; 2 | 3 | import { createContext } from "~/server/trpc/context"; 4 | import { appRouter } from "~/server/trpc/router/_app"; 5 | 6 | const handler = createSolidAPIHandler({ 7 | router: appRouter, 8 | createContext, 9 | }); 10 | 11 | export const GET = handler; 12 | export const POST = handler; 13 | -------------------------------------------------------------------------------- /src/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import { For, Match, Switch, type ParentComponent } from "solid-js"; 2 | 3 | import { PostCard } from "~/components/Post"; 4 | import { trpc } from "~/utils/trpc"; 5 | 6 | const Home: ParentComponent = () => { 7 | const posts = trpc.posts.getLatest.useQuery(); 8 | return ( 9 | <> 10 | 11 | 12 |

Loading...

13 |
14 | 15 |

Oh no! Something went wrong!

16 |
17 | 18 |
19 | 20 | {({ id, title, link, createdAt, User }) => ( 21 | 28 | )} 29 | 30 |
31 |
32 |
33 | 34 | ); 35 | }; 36 | 37 | export default Home; 38 | -------------------------------------------------------------------------------- /src/routes/p/[id].tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-non-null-assertion */ 2 | import { useParams } from "solid-start"; 3 | import { Match, Switch } from "solid-js"; 4 | 5 | import { CommentSection } from "~/components/Comments"; 6 | import { CommentForm } from "~/components/Comments/Form"; 7 | import { Post } from "~/components/Post"; 8 | import { trpc } from "~/utils/trpc"; 9 | 10 | export default function PostPage() { 11 | const { id } = useParams(); 12 | const post = trpc.posts.getPostById.useQuery(() => ({ id })); 13 | 14 | return ( 15 | 16 | 17 |

Loading...

18 |
19 | 20 | 21 |

Oh no! Something went wrong!

22 |
23 | 24 | 25 | {(post) => ( 26 |
27 | 35 | 36 |
37 | 38 |
39 | )} 40 | 41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/routes/submit.tsx: -------------------------------------------------------------------------------- 1 | import { createServerAction$, redirect } from "solid-start/server"; 2 | import { Show } from "solid-js"; 3 | import { z } from "zod"; 4 | 5 | import { AuthGuard } from "~/components/AuthGuard"; 6 | import { authenticator } from "~/server/auth"; 7 | import { prisma } from "~/server/db/client"; 8 | 9 | const inputSchema = z.object({ 10 | title: z.string().min(1).max(255), 11 | link: z.string().url(), 12 | description: z.string().max(1000).optional(), 13 | }); 14 | 15 | export const { routeData, Page } = AuthGuard(() => { 16 | const [submit, { Form }] = createServerAction$( 17 | async (form: FormData, { request }) => { 18 | const title = form.get("title"); 19 | const link = form.get("url"); 20 | const description = form.get("description"); 21 | 22 | const input = inputSchema.safeParse({ title, link, description }); 23 | 24 | if (input.success === false) { 25 | console.log(input.error.format()); 26 | throw new Error(input.error.format()._errors[0]); 27 | } 28 | 29 | const user = await authenticator.isAuthenticated(request); 30 | 31 | if (!user) { 32 | throw redirect("/account"); 33 | } 34 | 35 | await prisma.post.create({ 36 | data: { 37 | title: input.data.title, 38 | link: input.data.link, 39 | description: input.data.description, 40 | userId: user.id, 41 | }, 42 | }); 43 | 44 | throw redirect("/"); 45 | } 46 | ); 47 | 48 | return ( 49 |
50 |
51 | 52 | {submit.error.message} 53 | 54 |
55 | 56 | 63 |
64 | 65 |
66 | 67 | 74 |
75 | 76 |
77 | 78 |