();
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/server/trpc/router/comments.ts:
--------------------------------------------------------------------------------
1 | import { TRPCError } from "@trpc/server";
2 | import { z } from "zod";
3 |
4 | import { protectedProcedure, t } from "../utils";
5 |
6 | export const commentsRouter = t.router({
7 | getAll: t.procedure
8 | .input(z.object({ id: z.string() }))
9 | .query(async ({ ctx, input: { id } }) => {
10 | try {
11 | const comments = await ctx.prisma.comment.findMany({
12 | where: { postId: id },
13 | include: { User: true },
14 | });
15 |
16 | return comments;
17 | } catch (error) {
18 | console.log(error);
19 |
20 | throw new TRPCError({ code: "BAD_REQUEST" });
21 | }
22 | }),
23 |
24 | create: protectedProcedure
25 | .input(
26 | z.object({
27 | id: z.string(),
28 | text: z.string(),
29 | parentId: z.string().optional(),
30 | })
31 | )
32 | .mutation(async ({ ctx, input }) => {
33 | const { id, text, parentId } = input;
34 | const { user } = ctx;
35 |
36 | try {
37 | const comment = await ctx.prisma.comment.create({
38 | data: {
39 | text,
40 | Post: { connect: { id } },
41 | User: { connect: { id: user.id } },
42 | ...(parentId && {
43 | parent: { connect: { id: parentId } },
44 | }),
45 | },
46 | });
47 |
48 | return comment;
49 | } catch (error) {
50 | console.log(error);
51 |
52 | throw new TRPCError({ code: "BAD_REQUEST" });
53 | }
54 | }),
55 | });
56 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
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/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 |
15 |
34 |
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/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 |
98 | );
99 | });
100 |
101 | export default Page;
102 |
--------------------------------------------------------------------------------