My personal site. theclashfruit.me
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: get started on admin

+956 -122
+2
.drizzle/0002_mature_doorman.sql
··· 1 + ALTER TABLE "posts" ALTER COLUMN "excerpt" SET NOT NULL;--> statement-breakpoint 2 + ALTER TABLE "posts" ADD COLUMN "draft" boolean DEFAULT true NOT NULL;
+12
.drizzle/0003_perfect_chimera.sql
··· 1 + ALTER TABLE "comments" DROP CONSTRAINT "comments_post_id_posts_id_fk"; 2 + --> statement-breakpoint 3 + ALTER TABLE "comments" DROP CONSTRAINT "comments_parent_id_comments_id_fk"; 4 + --> statement-breakpoint 5 + ALTER TABLE "comments" DROP CONSTRAINT "comments_author_id_users_id_fk"; 6 + --> statement-breakpoint 7 + ALTER TABLE "posts" DROP CONSTRAINT "posts_author_id_users_id_fk"; 8 + --> statement-breakpoint 9 + ALTER TABLE "comments" ADD CONSTRAINT "comments_post_id_posts_id_fk" FOREIGN KEY ("post_id") REFERENCES "public"."posts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint 10 + ALTER TABLE "comments" ADD CONSTRAINT "comments_parent_id_comments_id_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."comments"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint 11 + ALTER TABLE "comments" ADD CONSTRAINT "comments_author_id_users_id_fk" FOREIGN KEY ("author_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint 12 + ALTER TABLE "posts" ADD CONSTRAINT "posts_author_id_users_id_fk" FOREIGN KEY ("author_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
+278
.drizzle/meta/0002_snapshot.json
··· 1 + { 2 + "id": "2aa4066a-c421-4e1e-a587-c0d6332470fd", 3 + "prevId": "2cbd9483-c81e-45f3-8892-b9539e29f8c1", 4 + "version": "7", 5 + "dialect": "postgresql", 6 + "tables": { 7 + "public.comments": { 8 + "name": "comments", 9 + "schema": "", 10 + "columns": { 11 + "id": { 12 + "name": "id", 13 + "type": "bigint", 14 + "primaryKey": true, 15 + "notNull": true 16 + }, 17 + "post_id": { 18 + "name": "post_id", 19 + "type": "bigint", 20 + "primaryKey": false, 21 + "notNull": false 22 + }, 23 + "parent_id": { 24 + "name": "parent_id", 25 + "type": "bigint", 26 + "primaryKey": false, 27 + "notNull": false 28 + }, 29 + "author_id": { 30 + "name": "author_id", 31 + "type": "bigint", 32 + "primaryKey": false, 33 + "notNull": false 34 + }, 35 + "display_name": { 36 + "name": "display_name", 37 + "type": "varchar(64)", 38 + "primaryKey": false, 39 + "notNull": false 40 + }, 41 + "content": { 42 + "name": "content", 43 + "type": "text", 44 + "primaryKey": false, 45 + "notNull": true 46 + } 47 + }, 48 + "indexes": {}, 49 + "foreignKeys": { 50 + "comments_post_id_posts_id_fk": { 51 + "name": "comments_post_id_posts_id_fk", 52 + "tableFrom": "comments", 53 + "tableTo": "posts", 54 + "columnsFrom": [ 55 + "post_id" 56 + ], 57 + "columnsTo": [ 58 + "id" 59 + ], 60 + "onDelete": "no action", 61 + "onUpdate": "no action" 62 + }, 63 + "comments_parent_id_comments_id_fk": { 64 + "name": "comments_parent_id_comments_id_fk", 65 + "tableFrom": "comments", 66 + "tableTo": "comments", 67 + "columnsFrom": [ 68 + "parent_id" 69 + ], 70 + "columnsTo": [ 71 + "id" 72 + ], 73 + "onDelete": "no action", 74 + "onUpdate": "no action" 75 + }, 76 + "comments_author_id_users_id_fk": { 77 + "name": "comments_author_id_users_id_fk", 78 + "tableFrom": "comments", 79 + "tableTo": "users", 80 + "columnsFrom": [ 81 + "author_id" 82 + ], 83 + "columnsTo": [ 84 + "id" 85 + ], 86 + "onDelete": "no action", 87 + "onUpdate": "no action" 88 + } 89 + }, 90 + "compositePrimaryKeys": {}, 91 + "uniqueConstraints": {}, 92 + "policies": {}, 93 + "checkConstraints": {}, 94 + "isRLSEnabled": false 95 + }, 96 + "public.posts": { 97 + "name": "posts", 98 + "schema": "", 99 + "columns": { 100 + "id": { 101 + "name": "id", 102 + "type": "bigint", 103 + "primaryKey": true, 104 + "notNull": true 105 + }, 106 + "author_id": { 107 + "name": "author_id", 108 + "type": "bigint", 109 + "primaryKey": false, 110 + "notNull": false 111 + }, 112 + "slug": { 113 + "name": "slug", 114 + "type": "varchar(128)", 115 + "primaryKey": false, 116 + "notNull": true 117 + }, 118 + "title": { 119 + "name": "title", 120 + "type": "varchar(256)", 121 + "primaryKey": false, 122 + "notNull": true 123 + }, 124 + "excerpt": { 125 + "name": "excerpt", 126 + "type": "text", 127 + "primaryKey": false, 128 + "notNull": true 129 + }, 130 + "content": { 131 + "name": "content", 132 + "type": "text", 133 + "primaryKey": false, 134 + "notNull": true 135 + }, 136 + "draft": { 137 + "name": "draft", 138 + "type": "boolean", 139 + "primaryKey": false, 140 + "notNull": true, 141 + "default": true 142 + }, 143 + "published_at": { 144 + "name": "published_at", 145 + "type": "timestamp", 146 + "primaryKey": false, 147 + "notNull": true, 148 + "default": "now()" 149 + }, 150 + "updated_at": { 151 + "name": "updated_at", 152 + "type": "timestamp", 153 + "primaryKey": false, 154 + "notNull": true, 155 + "default": "now()" 156 + } 157 + }, 158 + "indexes": {}, 159 + "foreignKeys": { 160 + "posts_author_id_users_id_fk": { 161 + "name": "posts_author_id_users_id_fk", 162 + "tableFrom": "posts", 163 + "tableTo": "users", 164 + "columnsFrom": [ 165 + "author_id" 166 + ], 167 + "columnsTo": [ 168 + "id" 169 + ], 170 + "onDelete": "no action", 171 + "onUpdate": "no action" 172 + } 173 + }, 174 + "compositePrimaryKeys": {}, 175 + "uniqueConstraints": { 176 + "posts_slug_unique": { 177 + "name": "posts_slug_unique", 178 + "nullsNotDistinct": false, 179 + "columns": [ 180 + "slug" 181 + ] 182 + } 183 + }, 184 + "policies": {}, 185 + "checkConstraints": {}, 186 + "isRLSEnabled": false 187 + }, 188 + "public.users": { 189 + "name": "users", 190 + "schema": "", 191 + "columns": { 192 + "id": { 193 + "name": "id", 194 + "type": "bigint", 195 + "primaryKey": true, 196 + "notNull": true 197 + }, 198 + "username": { 199 + "name": "username", 200 + "type": "varchar(32)", 201 + "primaryKey": false, 202 + "notNull": true 203 + }, 204 + "display_name": { 205 + "name": "display_name", 206 + "type": "varchar(64)", 207 + "primaryKey": false, 208 + "notNull": false 209 + }, 210 + "email": { 211 + "name": "email", 212 + "type": "varchar(320)", 213 + "primaryKey": false, 214 + "notNull": true 215 + }, 216 + "password": { 217 + "name": "password", 218 + "type": "varchar(72)", 219 + "primaryKey": false, 220 + "notNull": true 221 + }, 222 + "avatar": { 223 + "name": "avatar", 224 + "type": "varchar(40)", 225 + "primaryKey": false, 226 + "notNull": false 227 + }, 228 + "permissions": { 229 + "name": "permissions", 230 + "type": "integer", 231 + "primaryKey": false, 232 + "notNull": true, 233 + "default": 0 234 + } 235 + }, 236 + "indexes": {}, 237 + "foreignKeys": {}, 238 + "compositePrimaryKeys": {}, 239 + "uniqueConstraints": { 240 + "users_username_unique": { 241 + "name": "users_username_unique", 242 + "nullsNotDistinct": false, 243 + "columns": [ 244 + "username" 245 + ] 246 + }, 247 + "users_email_unique": { 248 + "name": "users_email_unique", 249 + "nullsNotDistinct": false, 250 + "columns": [ 251 + "email" 252 + ] 253 + }, 254 + "users_password_unique": { 255 + "name": "users_password_unique", 256 + "nullsNotDistinct": false, 257 + "columns": [ 258 + "password" 259 + ] 260 + } 261 + }, 262 + "policies": {}, 263 + "checkConstraints": {}, 264 + "isRLSEnabled": false 265 + } 266 + }, 267 + "enums": {}, 268 + "schemas": {}, 269 + "sequences": {}, 270 + "roles": {}, 271 + "policies": {}, 272 + "views": {}, 273 + "_meta": { 274 + "columns": {}, 275 + "schemas": {}, 276 + "tables": {} 277 + } 278 + }
+278
.drizzle/meta/0003_snapshot.json
··· 1 + { 2 + "id": "d4271530-af67-49ae-a6df-f87645ab4cc5", 3 + "prevId": "2aa4066a-c421-4e1e-a587-c0d6332470fd", 4 + "version": "7", 5 + "dialect": "postgresql", 6 + "tables": { 7 + "public.comments": { 8 + "name": "comments", 9 + "schema": "", 10 + "columns": { 11 + "id": { 12 + "name": "id", 13 + "type": "bigint", 14 + "primaryKey": true, 15 + "notNull": true 16 + }, 17 + "post_id": { 18 + "name": "post_id", 19 + "type": "bigint", 20 + "primaryKey": false, 21 + "notNull": false 22 + }, 23 + "parent_id": { 24 + "name": "parent_id", 25 + "type": "bigint", 26 + "primaryKey": false, 27 + "notNull": false 28 + }, 29 + "author_id": { 30 + "name": "author_id", 31 + "type": "bigint", 32 + "primaryKey": false, 33 + "notNull": false 34 + }, 35 + "display_name": { 36 + "name": "display_name", 37 + "type": "varchar(64)", 38 + "primaryKey": false, 39 + "notNull": false 40 + }, 41 + "content": { 42 + "name": "content", 43 + "type": "text", 44 + "primaryKey": false, 45 + "notNull": true 46 + } 47 + }, 48 + "indexes": {}, 49 + "foreignKeys": { 50 + "comments_post_id_posts_id_fk": { 51 + "name": "comments_post_id_posts_id_fk", 52 + "tableFrom": "comments", 53 + "tableTo": "posts", 54 + "columnsFrom": [ 55 + "post_id" 56 + ], 57 + "columnsTo": [ 58 + "id" 59 + ], 60 + "onDelete": "cascade", 61 + "onUpdate": "no action" 62 + }, 63 + "comments_parent_id_comments_id_fk": { 64 + "name": "comments_parent_id_comments_id_fk", 65 + "tableFrom": "comments", 66 + "tableTo": "comments", 67 + "columnsFrom": [ 68 + "parent_id" 69 + ], 70 + "columnsTo": [ 71 + "id" 72 + ], 73 + "onDelete": "cascade", 74 + "onUpdate": "no action" 75 + }, 76 + "comments_author_id_users_id_fk": { 77 + "name": "comments_author_id_users_id_fk", 78 + "tableFrom": "comments", 79 + "tableTo": "users", 80 + "columnsFrom": [ 81 + "author_id" 82 + ], 83 + "columnsTo": [ 84 + "id" 85 + ], 86 + "onDelete": "cascade", 87 + "onUpdate": "no action" 88 + } 89 + }, 90 + "compositePrimaryKeys": {}, 91 + "uniqueConstraints": {}, 92 + "policies": {}, 93 + "checkConstraints": {}, 94 + "isRLSEnabled": false 95 + }, 96 + "public.posts": { 97 + "name": "posts", 98 + "schema": "", 99 + "columns": { 100 + "id": { 101 + "name": "id", 102 + "type": "bigint", 103 + "primaryKey": true, 104 + "notNull": true 105 + }, 106 + "author_id": { 107 + "name": "author_id", 108 + "type": "bigint", 109 + "primaryKey": false, 110 + "notNull": false 111 + }, 112 + "slug": { 113 + "name": "slug", 114 + "type": "varchar(128)", 115 + "primaryKey": false, 116 + "notNull": true 117 + }, 118 + "title": { 119 + "name": "title", 120 + "type": "varchar(256)", 121 + "primaryKey": false, 122 + "notNull": true 123 + }, 124 + "excerpt": { 125 + "name": "excerpt", 126 + "type": "text", 127 + "primaryKey": false, 128 + "notNull": true 129 + }, 130 + "content": { 131 + "name": "content", 132 + "type": "text", 133 + "primaryKey": false, 134 + "notNull": true 135 + }, 136 + "draft": { 137 + "name": "draft", 138 + "type": "boolean", 139 + "primaryKey": false, 140 + "notNull": true, 141 + "default": true 142 + }, 143 + "published_at": { 144 + "name": "published_at", 145 + "type": "timestamp", 146 + "primaryKey": false, 147 + "notNull": true, 148 + "default": "now()" 149 + }, 150 + "updated_at": { 151 + "name": "updated_at", 152 + "type": "timestamp", 153 + "primaryKey": false, 154 + "notNull": true, 155 + "default": "now()" 156 + } 157 + }, 158 + "indexes": {}, 159 + "foreignKeys": { 160 + "posts_author_id_users_id_fk": { 161 + "name": "posts_author_id_users_id_fk", 162 + "tableFrom": "posts", 163 + "tableTo": "users", 164 + "columnsFrom": [ 165 + "author_id" 166 + ], 167 + "columnsTo": [ 168 + "id" 169 + ], 170 + "onDelete": "cascade", 171 + "onUpdate": "no action" 172 + } 173 + }, 174 + "compositePrimaryKeys": {}, 175 + "uniqueConstraints": { 176 + "posts_slug_unique": { 177 + "name": "posts_slug_unique", 178 + "nullsNotDistinct": false, 179 + "columns": [ 180 + "slug" 181 + ] 182 + } 183 + }, 184 + "policies": {}, 185 + "checkConstraints": {}, 186 + "isRLSEnabled": false 187 + }, 188 + "public.users": { 189 + "name": "users", 190 + "schema": "", 191 + "columns": { 192 + "id": { 193 + "name": "id", 194 + "type": "bigint", 195 + "primaryKey": true, 196 + "notNull": true 197 + }, 198 + "username": { 199 + "name": "username", 200 + "type": "varchar(32)", 201 + "primaryKey": false, 202 + "notNull": true 203 + }, 204 + "display_name": { 205 + "name": "display_name", 206 + "type": "varchar(64)", 207 + "primaryKey": false, 208 + "notNull": false 209 + }, 210 + "email": { 211 + "name": "email", 212 + "type": "varchar(320)", 213 + "primaryKey": false, 214 + "notNull": true 215 + }, 216 + "password": { 217 + "name": "password", 218 + "type": "varchar(72)", 219 + "primaryKey": false, 220 + "notNull": true 221 + }, 222 + "avatar": { 223 + "name": "avatar", 224 + "type": "varchar(40)", 225 + "primaryKey": false, 226 + "notNull": false 227 + }, 228 + "permissions": { 229 + "name": "permissions", 230 + "type": "integer", 231 + "primaryKey": false, 232 + "notNull": true, 233 + "default": 0 234 + } 235 + }, 236 + "indexes": {}, 237 + "foreignKeys": {}, 238 + "compositePrimaryKeys": {}, 239 + "uniqueConstraints": { 240 + "users_username_unique": { 241 + "name": "users_username_unique", 242 + "nullsNotDistinct": false, 243 + "columns": [ 244 + "username" 245 + ] 246 + }, 247 + "users_email_unique": { 248 + "name": "users_email_unique", 249 + "nullsNotDistinct": false, 250 + "columns": [ 251 + "email" 252 + ] 253 + }, 254 + "users_password_unique": { 255 + "name": "users_password_unique", 256 + "nullsNotDistinct": false, 257 + "columns": [ 258 + "password" 259 + ] 260 + } 261 + }, 262 + "policies": {}, 263 + "checkConstraints": {}, 264 + "isRLSEnabled": false 265 + } 266 + }, 267 + "enums": {}, 268 + "schemas": {}, 269 + "sequences": {}, 270 + "roles": {}, 271 + "policies": {}, 272 + "views": {}, 273 + "_meta": { 274 + "columns": {}, 275 + "schemas": {}, 276 + "tables": {} 277 + } 278 + }
+14
.drizzle/meta/_journal.json
··· 15 15 "when": 1774691481484, 16 16 "tag": "0001_late_agent_zero", 17 17 "breakpoints": true 18 + }, 19 + { 20 + "idx": 2, 21 + "version": "7", 22 + "when": 1774712783794, 23 + "tag": "0002_mature_doorman", 24 + "breakpoints": true 25 + }, 26 + { 27 + "idx": 3, 28 + "version": "7", 29 + "when": 1774713093687, 30 + "tag": "0003_perfect_chimera", 31 + "breakpoints": true 18 32 } 19 33 ] 20 34 }
+14 -14
.drizzle/relations.ts
··· 1 1 import { relations } from "drizzle-orm/relations"; 2 - import { users, posts, comments } from "./schema"; 3 - 4 - export const postsRelations = relations(posts, ({one, many}) => ({ 5 - user: one(users, { 6 - fields: [posts.authorId], 7 - references: [users.id] 8 - }), 9 - comments: many(comments), 10 - })); 11 - 12 - export const usersRelations = relations(users, ({many}) => ({ 13 - posts: many(posts), 14 - comments: many(comments), 15 - })); 2 + import { posts, comments, users } from "./schema"; 16 3 17 4 export const commentsRelations = relations(comments, ({one, many}) => ({ 18 5 post: one(posts, { ··· 31 18 fields: [comments.authorId], 32 19 references: [users.id] 33 20 }), 21 + })); 22 + 23 + export const postsRelations = relations(posts, ({one, many}) => ({ 24 + comments: many(comments), 25 + user: one(users, { 26 + fields: [posts.authorId], 27 + references: [users.id] 28 + }), 29 + })); 30 + 31 + export const usersRelations = relations(users, ({many}) => ({ 32 + comments: many(comments), 33 + posts: many(posts), 34 34 }));
+25 -23
.drizzle/schema.ts
··· 1 - import { pgTable, foreignKey, unique, bigint, varchar, text, timestamp, integer } from "drizzle-orm/pg-core" 1 + import { pgTable, foreignKey, bigint, varchar, text, unique, integer, timestamp, boolean } from "drizzle-orm/pg-core" 2 2 import { sql } from "drizzle-orm" 3 3 4 4 5 5 6 - export const posts = pgTable("posts", { 7 - // You can use { mode: "bigint" } if numbers are exceeding js number limitations 8 - id: bigint({ mode: "number" }).primaryKey().notNull(), 9 - // You can use { mode: "bigint" } if numbers are exceeding js number limitations 10 - authorId: bigint("author_id", { mode: "number" }), 11 - slug: varchar({ length: 128 }).notNull(), 12 - title: varchar({ length: 256 }).notNull(), 13 - content: text().notNull(), 14 - publishedAt: timestamp("published_at", { mode: 'string' }).defaultNow().notNull(), 15 - updatedAt: timestamp("updated_at", { mode: 'string' }).defaultNow().notNull(), 16 - }, (table) => [ 17 - foreignKey({ 18 - columns: [table.authorId], 19 - foreignColumns: [users.id], 20 - name: "posts_author_id_users_id_fk" 21 - }), 22 - unique("posts_slug_unique").on(table.slug), 23 - ]); 24 - 25 6 export const comments = pgTable("comments", { 26 7 // You can use { mode: "bigint" } if numbers are exceeding js number limitations 27 8 id: bigint({ mode: "number" }).primaryKey().notNull(), ··· 38 19 columns: [table.postId], 39 20 foreignColumns: [posts.id], 40 21 name: "comments_post_id_posts_id_fk" 41 - }), 22 + }).onDelete("cascade"), 42 23 foreignKey({ 43 24 columns: [table.parentId], 44 25 foreignColumns: [table.id], 45 26 name: "comments_parent_id_comments_id_fk" 46 - }), 27 + }).onDelete("cascade"), 47 28 foreignKey({ 48 29 columns: [table.authorId], 49 30 foreignColumns: [users.id], 50 31 name: "comments_author_id_users_id_fk" 51 - }), 32 + }).onDelete("cascade"), 52 33 ]); 53 34 54 35 export const users = pgTable("users", { ··· 65 46 unique("users_email_unique").on(table.email), 66 47 unique("users_password_unique").on(table.password), 67 48 ]); 49 + 50 + export const posts = pgTable("posts", { 51 + // You can use { mode: "bigint" } if numbers are exceeding js number limitations 52 + id: bigint({ mode: "number" }).primaryKey().notNull(), 53 + // You can use { mode: "bigint" } if numbers are exceeding js number limitations 54 + authorId: bigint("author_id", { mode: "number" }), 55 + slug: varchar({ length: 128 }).notNull(), 56 + title: varchar({ length: 256 }).notNull(), 57 + content: text().notNull(), 58 + publishedAt: timestamp("published_at", { mode: 'string' }).defaultNow().notNull(), 59 + updatedAt: timestamp("updated_at", { mode: 'string' }).defaultNow().notNull(), 60 + excerpt: text().notNull(), 61 + draft: boolean().default(true).notNull(), 62 + }, (table) => [ 63 + foreignKey({ 64 + columns: [table.authorId], 65 + foreignColumns: [users.id], 66 + name: "posts_author_id_users_id_fk" 67 + }).onDelete("cascade"), 68 + unique("posts_slug_unique").on(table.slug), 69 + ]);
+15
app/(admin)/admin/art/page.tsx
··· 1 + import type { Metadata } from 'next'; 2 + 3 + export const metadata: Metadata = { 4 + title: 'Admin > Art' 5 + }; 6 + 7 + export default function Art() { 8 + return ( 9 + <> 10 + <p>Admin {'>'} Art</p> 11 + 12 + <ul></ul> 13 + </> 14 + ); 15 + }
+15
app/(admin)/admin/comments/page.tsx
··· 1 + import type { Metadata } from 'next'; 2 + 3 + export const metadata: Metadata = { 4 + title: 'Admin > Comments' 5 + }; 6 + 7 + export default function Comments() { 8 + return ( 9 + <> 10 + <p>Admin {'>'} Comments</p> 11 + 12 + <ul></ul> 13 + </> 14 + ); 15 + }
+9
app/(admin)/admin/page.mdx
··· 1 + export const metadata = { 2 + title: 'Admin' 3 + }; 4 + 5 + There's nothing here _yet_. 6 + 7 + I will someday decide what to put here :3. 8 + 9 + [Logout >](/logout)
+15
app/(admin)/admin/photos/page.tsx
··· 1 + import type { Metadata } from 'next'; 2 + 3 + export const metadata: Metadata = { 4 + title: 'Admin > Photos' 5 + }; 6 + 7 + export default function Photos() { 8 + return ( 9 + <> 10 + <p>Admin {'>'} Photos</p> 11 + 12 + <ul></ul> 13 + </> 14 + ); 15 + }
+60
app/(admin)/admin/posts/page.tsx
··· 1 + import { db } from '@/lib/db/drizzle'; 2 + import { postsTable } from '@/lib/db/schema'; 3 + 4 + import type { Metadata } from 'next'; 5 + 6 + import styles from '@/styles/pages/Admin.module.scss' 7 + 8 + export const metadata: Metadata = { 9 + title: 'Admin > Posts' 10 + }; 11 + 12 + export default async function Posts() { 13 + const posts = await db 14 + .select() 15 + .from(postsTable) 16 + .orderBy(postsTable.publishedAt); 17 + 18 + return ( 19 + <> 20 + <p>Admin {'>'} Posts</p> 21 + 22 + <div className={styles.tableWrapper}> 23 + <table> 24 + <thead> 25 + <tr> 26 + <th scope="col"></th> 27 + <th scope="col">Slug</th> 28 + <th scope="col">Title</th> 29 + <th scope="col">Excerpt</th> 30 + <th scope="col">Draft</th> 31 + <th scope="col">Published</th> 32 + <th scope="col">Updated</th> 33 + <th scope="col">Actions</th> 34 + </tr> 35 + </thead> 36 + <tbody> 37 + {posts.map((p) => ( 38 + <tr key={p.id}> 39 + <td>{p.id}</td> 40 + <td>{p.slug}</td> 41 + <td>{p.title}</td> 42 + <td>{p.excerpt}</td> 43 + <td> 44 + <input type="checkbox" disabled checked={p.draft} /> 45 + </td> 46 + <td>{p.publishedAt.toLocaleString('hu-HU')}</td> 47 + <td>{p.updatedAt.toLocaleString('hu-HU')}</td> 48 + <td> 49 + <button>Edit</button> 50 + <button>Publish</button> 51 + <button>Delete</button> 52 + </td> 53 + </tr> 54 + ))} 55 + </tbody> 56 + </table> 57 + </div> 58 + </> 59 + ); 60 + }
+59
app/(admin)/layout.tsx
··· 1 + import NavBar from '@/components/NavBar'; 2 + 3 + import { ViewTransition } from 'react'; 4 + 5 + import { 6 + BookText, 7 + LayoutDashboard, 8 + MessageCircle, 9 + LineSquiggle, 10 + ImageIcon 11 + } from 'lucide-react'; 12 + 13 + import styles from '@/styles/layout/Main.module.scss'; 14 + 15 + export default function MarketLayout({ 16 + children 17 + }: Readonly<{ 18 + children: React.ReactNode; 19 + }>) { 20 + return ( 21 + <> 22 + <aside className={styles.sideBar}> 23 + <NavBar 24 + items={[ 25 + { 26 + icon: <LayoutDashboard />, 27 + href: '/admin', 28 + text: 'Admin' 29 + }, 30 + { 31 + icon: <BookText />, 32 + href: '/admin/posts', 33 + text: 'Posts' 34 + }, 35 + { 36 + icon: <MessageCircle />, 37 + href: '/admin/comments', 38 + text: 'Comments' 39 + }, 40 + { 41 + icon: <LineSquiggle />, 42 + href: '/admin/art', 43 + text: 'Art' 44 + }, 45 + { 46 + icon: <ImageIcon />, 47 + href: '/admin/photos', 48 + text: 'Photos' 49 + } 50 + ]} 51 + /> 52 + </aside> 53 + 54 + <ViewTransition default="scale"> 55 + <main>{children}</main> 56 + </ViewTransition> 57 + </> 58 + ); 59 + }
+44 -5
app/(main)/layout.tsx
··· 1 1 import Header from '@/components/Header'; 2 2 import NavBar from '@/components/NavBar'; 3 3 import Footer from '@/components/Footer'; 4 - import Container from '@/components/Container'; 5 4 6 5 import { ViewTransition } from 'react'; 7 6 7 + import { 8 + BookText, 9 + Home, 10 + LineSquiggle, 11 + ImageIcon, 12 + Bookmark, 13 + GitPullRequestCreateArrow 14 + } from 'lucide-react'; 15 + 8 16 import styles from '@/styles/layout/Main.module.scss'; 9 17 10 18 export default function MarketLayout({ ··· 17 25 <Header /> 18 26 19 27 <aside className={styles.sideBar}> 20 - <NavBar /> 28 + <NavBar 29 + items={[ 30 + { 31 + icon: <Home />, 32 + href: '/', 33 + text: 'Home' 34 + }, 35 + { 36 + icon: <BookText />, 37 + href: '/blog', 38 + text: 'Blog' 39 + }, 40 + { 41 + icon: <LineSquiggle />, 42 + href: '/art', 43 + text: 'Art' 44 + }, 45 + { 46 + icon: <ImageIcon />, 47 + href: '/photos', 48 + text: 'Photos' 49 + }, 50 + { 51 + icon: <GitPullRequestCreateArrow />, 52 + href: '/projects', 53 + text: 'Projects' 54 + }, 55 + { 56 + icon: <Bookmark />, 57 + href: '/links', 58 + text: 'Links' 59 + } 60 + ]} 61 + /> 21 62 </aside> 22 63 23 64 <ViewTransition default="scale"> 24 - <main> 25 - {children} 26 - </main> 65 + <main>{children}</main> 27 66 </ViewTransition> 28 67 29 68 <Footer />
+13 -7
app/api/v1/post/route.ts
··· 1 1 import { db } from '@/lib/db/drizzle'; 2 2 import { postsTable } from '@/lib/db/schema'; 3 3 4 - import { count, ilike, or } from 'drizzle-orm'; 4 + import { and, count, ilike, not, or } from 'drizzle-orm'; 5 5 6 6 import { NextRequest } from 'next/server'; 7 7 ··· 16 16 .select({ count: count() }) 17 17 .from(postsTable) 18 18 .where( 19 - or( 20 - ilike(postsTable.title, `%${query}%`), 21 - ilike(postsTable.content, `%${query}%`) 19 + and( 20 + or( 21 + ilike(postsTable.title, `%${query}%`), 22 + ilike(postsTable.content, `%${query}%`) 23 + ), 24 + not(postsTable.draft) 22 25 ) 23 26 ); 24 27 ··· 26 29 .select() 27 30 .from(postsTable) 28 31 .where( 29 - or( 30 - ilike(postsTable.title, `%${query}%`), 31 - ilike(postsTable.content, `%${query}%`) 32 + and( 33 + or( 34 + ilike(postsTable.title, `%${query}%`), 35 + ilike(postsTable.content, `%${query}%`) 36 + ), 37 + not(postsTable.draft) 32 38 ) 33 39 ) 34 40 .offset(offset >= 0 ? offset : 0)
+1 -1
app/not-found.tsx
··· 15 15 <Header /> 16 16 17 17 <Container as="main" className={styles.main}> 18 - <NavBar /> 18 + 19 19 20 20 <div className={styles.error}> 21 21 <div>
+2 -3
components/Header.tsx
··· 1 - import { Leaf } from 'lucide-react'; 2 - 3 - import Container from './Container'; 1 + import { Leaf, LogIn } from 'lucide-react'; 4 2 5 3 import styles from '@/styles/components/Header.module.scss'; 4 + import Link from 'next/link'; 6 5 7 6 export default function Header() { 8 7 return (
+21 -61
components/NavBar.tsx
··· 3 3 import Link from 'next/link'; 4 4 import { usePathname } from 'next/navigation'; 5 5 6 - import { 7 - BookText, 8 - Home, 9 - LineSquiggle, 10 - ImageIcon, 11 - Bookmark, 12 - GitPullRequestCreateArrow, 13 - X, 14 - Menu 15 - } from 'lucide-react'; 6 + import { Home, X, Menu } from 'lucide-react'; 16 7 17 8 import styles from '@/styles/components/NavBar.module.scss'; 18 - import { useState } from 'react'; 9 + import { type ReactNode, useState } from 'react'; 19 10 20 - export default function NavBar() { 11 + interface NavItem { 12 + icon: ReactNode; 13 + href: string; 14 + text: string; 15 + } 16 + 17 + export default function NavBar({ items }: { items: NavItem[] }) { 21 18 const path = usePathname(); 22 19 23 20 const [open, setOpen] = useState(false); ··· 34 31 {open ? <X size={26} /> : <Menu size={26} />} 35 32 </button> 36 33 37 - <Link href="/" className={path === '/' ? styles.active : ''}> 34 + <Link href={items[0].href} className={path === items[0].href ? styles.active : ''}> 38 35 <Home size={26} /> 39 36 </Link> 40 37 </div> ··· 47 44 }} 48 45 > 49 46 <ul> 50 - <li> 51 - <Link href="/" className={path === '/' ? styles.active : ''}> 52 - <Home /> 53 - Home 54 - </Link> 55 - </li> 56 - <li> 57 - <Link 58 - href="/blog" 59 - className={path === '/blog' ? styles.active : ''} 60 - > 61 - <BookText /> 62 - Blog 63 - </Link> 64 - </li> 65 - <li> 66 - <Link href="/art" className={path === '/art' ? styles.active : ''}> 67 - <LineSquiggle /> 68 - Art 69 - </Link> 70 - </li> 71 - <li> 72 - <Link 73 - href="/photos" 74 - className={path === '/photos' ? styles.active : ''} 75 - > 76 - <ImageIcon /> 77 - Photos 78 - </Link> 79 - </li> 80 - <li> 81 - <Link 82 - href="/projects" 83 - className={path === '/projects' ? styles.active : ''} 84 - > 85 - <GitPullRequestCreateArrow /> 86 - Projects 87 - </Link> 88 - </li> 89 - <li> 90 - <Link 91 - href="/links" 92 - className={path === '/links' ? styles.active : ''} 93 - > 94 - <Bookmark /> 95 - Links 96 - </Link> 97 - </li> 47 + {items.map((i) => ( 48 + <li key={i.href}> 49 + <Link 50 + href={i.href} 51 + className={path === i.href ? styles.active : ''} 52 + > 53 + {i.icon} 54 + {i.text} 55 + </Link> 56 + </li> 57 + ))} 98 58 </ul> 99 59 </nav> 100 60 </>
+19 -7
lib/db/schema.ts
··· 1 1 import { 2 + boolean, 2 3 integer, 3 4 pgTable, 4 5 text, ··· 8 9 9 10 import { snowflake } from '@/lib/db/types/snowflake'; 10 11 import { snowflakeGenerator } from '@/lib/snowflake'; 11 - import { relations } from 'drizzle-orm'; 12 12 13 13 export const usersTable = pgTable('users', { 14 14 id: snowflake() ··· 31 31 .primaryKey() 32 32 .$defaultFn(() => snowflakeGenerator.nextId()), 33 33 34 - author: snowflake('author_id').references(() => usersTable.id), 34 + author: snowflake('author_id').references(() => usersTable.id, { 35 + onDelete: 'cascade' 36 + }), 35 37 36 38 slug: varchar({ length: 128 }).unique().notNull(), 37 39 title: varchar({ length: 256 }).notNull(), 38 40 39 - excerpt: text(), 41 + excerpt: text().notNull(), 40 42 content: text().notNull(), 41 43 42 - publishedAt: timestamp().notNull().defaultNow(), 44 + draft: boolean().default(true).notNull(), 45 + 46 + publishedAt: timestamp() 47 + .notNull() 48 + .defaultNow(), 43 49 updatedAt: timestamp() 44 50 .notNull() 45 51 .defaultNow() ··· 51 57 .primaryKey() 52 58 .$defaultFn(() => snowflakeGenerator.nextId()), 53 59 54 - post: snowflake('post_id').references(() => postsTable.id), 60 + post: snowflake('post_id').references(() => postsTable.id, { 61 + onDelete: 'cascade' 62 + }), 55 63 // eslint-disable-next-line @typescript-eslint/no-explicit-any 56 - parent: snowflake('parent_id').references((): any => commentsTable.id), 57 - author: snowflake('author_id').references(() => usersTable.id), 64 + parent: snowflake('parent_id').references((): any => commentsTable.id, { 65 + onDelete: 'cascade' 66 + }), 67 + author: snowflake('author_id').references(() => usersTable.id, { 68 + onDelete: 'cascade' 69 + }), 58 70 59 71 displayName: varchar({ length: 64 }), 60 72
+4
lib/enums.ts
··· 1 + export enum Permissions { 2 + None = 0, 3 + Admin = 1 << 0 4 + }
+1
styles/components/Header.module.scss
··· 7 7 display: flex; 8 8 9 9 align-items: center; 10 + justify-content: space-between; 10 11 11 12 gap: 16px; 12 13
+30
styles/pages/Admin.module.scss
··· 1 + .tableWrapper { 2 + overflow-x: auto; 3 + 4 + > table { 5 + width: 70vw; 6 + 7 + border-collapse: collapse; 8 + 9 + th { 10 + padding: 12px 16px; 11 + border-bottom: 1px solid var(--outlineVariant); 12 + } 13 + 14 + td { 15 + padding: 12px 16px; 16 + 17 + border-bottom: 1px solid var(--outlineVariant); 18 + 19 + vertical-align: top; 20 + } 21 + 22 + tbody tr:hover td { 23 + background-color: var(--surfaceContainer); 24 + } 25 + 26 + tbody tr:last-child td { 27 + border-bottom: none; 28 + } 29 + } 30 + }
+25 -1
styles/typography.module.scss
··· 49 49 50 50 p { 51 51 margin-bottom: 8px; 52 - 52 + 53 53 text-align: justify; 54 54 text-justify: inter-word; 55 55 ··· 85 85 font-size: 100%; 86 86 87 87 color: inherit; 88 + } 89 + 90 + input, 91 + textarea { 92 + background: none; 93 + 94 + padding: 12px; 95 + 96 + outline: 1px solid transparent; 97 + outline-offset: -1px; 98 + 99 + border: 1px solid var(--outlineVariant); 100 + border-radius: 12px; 101 + 102 + width: 100%; 103 + 104 + transition: 0.24s cubic-bezier(0.34, 1.56, 0.64, 1); 105 + 106 + &:hover, 107 + &:active, 108 + &:focus { 109 + outline-width: 2px; 110 + outline-color: var(--primary); 111 + } 88 112 } 89 113 90 114 ul {