No idea where I'm at with this, I think somewhat near the end though
This commit is contained in:
9
.continue/agents/new-agent.yaml
Normal file
9
.continue/agents/new-agent.yaml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# This is an example agent configuration file
|
||||||
|
# It is used to define custom AI agents within Continue
|
||||||
|
# Each agent file can be accessed by selecting it from the agent dropdown
|
||||||
|
|
||||||
|
# To learn more, see the full config.yaml reference: https://docs.continue.dev/reference
|
||||||
|
|
||||||
|
name: Example Agent
|
||||||
|
version: 1.0.0
|
||||||
|
schema: v1
|
||||||
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
@@ -11,7 +11,7 @@
|
|||||||
"name": "Next.js: debug client-side",
|
"name": "Next.js: debug client-side",
|
||||||
"type": "chrome",
|
"type": "chrome",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"url": "https://3000.vscode.home.joemonk.co.uk/"
|
"url": "http://3000.vscode.localhost/"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Next.js: debug full stack",
|
"name": "Next.js: debug full stack",
|
||||||
|
|||||||
14
.vscode/settings.json
vendored
14
.vscode/settings.json
vendored
@@ -4,5 +4,17 @@
|
|||||||
"https://json.schemastore.org/github-workflow.json": "file:///workspace/next-portfolio/.gitea/workflows/deploy.yaml"
|
"https://json.schemastore.org/github-workflow.json": "file:///workspace/next-portfolio/.gitea/workflows/deploy.yaml"
|
||||||
},
|
},
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"typescript.format.enable": true
|
"typescript.format.enable": true,
|
||||||
|
"biome.enabled": true,
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.action.organizeImports.biome": "explicit",
|
||||||
|
"source.action.useSortedAttributes.biome": "explicit",
|
||||||
|
"source.action.useSortedKeys.biome": "explicit",
|
||||||
|
"source.fixAll.biome": "explicit",
|
||||||
|
"source.organizeImports.biome": "explicit"
|
||||||
|
},
|
||||||
|
"editor.defaultFormatter": "biomejs.biome",
|
||||||
|
"files.associations": {
|
||||||
|
"*.css": "tailwindcss"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:22 AS base
|
FROM node:24 AS base
|
||||||
|
|
||||||
# Install dependencies only when needed
|
# Install dependencies only when needed
|
||||||
FROM base AS deps
|
FROM base AS deps
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://biomejs.dev/schemas/2.0.6/schema.json",
|
"$schema": "https://biomejs.dev/schemas/2.2.5/schema.json",
|
||||||
"vcs": {
|
"vcs": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"clientKind": "git",
|
"clientKind": "git",
|
||||||
@@ -10,7 +10,8 @@
|
|||||||
},
|
},
|
||||||
"formatter": {
|
"formatter": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"indentStyle": "tab"
|
"indentStyle": "tab",
|
||||||
|
"lineWidth": 320
|
||||||
},
|
},
|
||||||
"linter": {
|
"linter": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@@ -19,7 +20,8 @@
|
|||||||
},
|
},
|
||||||
"domains": {
|
"domains": {
|
||||||
"next": "recommended",
|
"next": "recommended",
|
||||||
"react": "recommended"
|
"react": "recommended",
|
||||||
|
"project": "recommended"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"javascript": {
|
"javascript": {
|
||||||
|
|||||||
23
docker-compose.yaml
Normal file
23
docker-compose.yaml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
services:
|
||||||
|
traefik:
|
||||||
|
container_name: traefik
|
||||||
|
image: traefik:latest
|
||||||
|
ports:
|
||||||
|
- 80:80
|
||||||
|
command:
|
||||||
|
- --api.insecure=true
|
||||||
|
|
||||||
|
- --providers.docker=true
|
||||||
|
- --providers.docker.watch=true
|
||||||
|
- --providers.docker.exposedbydefault=false
|
||||||
|
|
||||||
|
- --providers.file.directory=/config
|
||||||
|
- --providers.file.watch=true
|
||||||
|
|
||||||
|
- --entryPoints.http.address=:80
|
||||||
|
|
||||||
|
- --accesslog
|
||||||
|
- --accesslog.format=json
|
||||||
|
volumes:
|
||||||
|
- ./docker/traefik/config:/config
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
11
docker/traefik/config/portfolio.yaml
Normal file
11
docker/traefik/config/portfolio.yaml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
http:
|
||||||
|
routers:
|
||||||
|
vscode:
|
||||||
|
entryPoints: http
|
||||||
|
rule: "Host(`3000.vscode.localhost`)"
|
||||||
|
service: vscode
|
||||||
|
services:
|
||||||
|
vscode:
|
||||||
|
loadBalancer:
|
||||||
|
servers:
|
||||||
|
- url: http://host.docker.internal:3000
|
||||||
11
docker/traefik/config/traefik.yaml
Normal file
11
docker/traefik/config/traefik.yaml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
http:
|
||||||
|
routers:
|
||||||
|
traefik:
|
||||||
|
entryPoints: http
|
||||||
|
rule: "Host(`traefik.localhost`)"
|
||||||
|
service: traefik
|
||||||
|
services:
|
||||||
|
traefik:
|
||||||
|
loadBalancer:
|
||||||
|
servers:
|
||||||
|
- url: http://traefik:8080
|
||||||
@@ -6,7 +6,7 @@ CREATE TABLE `photo` (
|
|||||||
`blur` blob NOT NULL,
|
`blur` blob NOT NULL,
|
||||||
`camera` text(128),
|
`camera` text(128),
|
||||||
`title` text(128),
|
`title` text(128),
|
||||||
`description` text(1024),
|
`description` text,
|
||||||
`exposureBiasValue` integer,
|
`exposureBiasValue` integer,
|
||||||
`fNumber` real,
|
`fNumber` real,
|
||||||
`isoSpeedRatings` integer,
|
`isoSpeedRatings` integer,
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
CREATE TABLE `challenges` (
|
|
||||||
`token` text PRIMARY KEY NOT NULL,
|
|
||||||
`data` text NOT NULL,
|
|
||||||
`expires` integer NOT NULL
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE `tokens` (
|
|
||||||
`key` text PRIMARY KEY NOT NULL,
|
|
||||||
`expires` integer NOT NULL
|
|
||||||
);
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": "6",
|
"version": "6",
|
||||||
"dialect": "sqlite",
|
"dialect": "sqlite",
|
||||||
"id": "246363f6-a664-4ec6-b43d-0033fe21ff8c",
|
"id": "2a201e4f-713d-4ee8-bf6f-2126752f16d4",
|
||||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||||
"tables": {
|
"tables": {
|
||||||
"photo": {
|
"photo": {
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
},
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"name": "description",
|
"name": "description",
|
||||||
"type": "text(1024)",
|
"type": "text",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false,
|
"notNull": false,
|
||||||
"autoincrement": false
|
"autoincrement": false
|
||||||
@@ -109,7 +109,9 @@
|
|||||||
"indexes": {
|
"indexes": {
|
||||||
"photo_src_unique": {
|
"photo_src_unique": {
|
||||||
"name": "photo_src_unique",
|
"name": "photo_src_unique",
|
||||||
"columns": ["src"],
|
"columns": [
|
||||||
|
"src"
|
||||||
|
],
|
||||||
"isUnique": true
|
"isUnique": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,187 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "6",
|
|
||||||
"dialect": "sqlite",
|
|
||||||
"id": "40ac544c-a258-4628-b121-7ed0403516d7",
|
|
||||||
"prevId": "246363f6-a664-4ec6-b43d-0033fe21ff8c",
|
|
||||||
"tables": {
|
|
||||||
"challenges": {
|
|
||||||
"name": "challenges",
|
|
||||||
"columns": {
|
|
||||||
"token": {
|
|
||||||
"name": "token",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"name": "data",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"expires": {
|
|
||||||
"name": "expires",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"photo": {
|
|
||||||
"name": "photo",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"src": {
|
|
||||||
"name": "src",
|
|
||||||
"type": "text(256)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"width": {
|
|
||||||
"name": "width",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"height": {
|
|
||||||
"name": "height",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"blur": {
|
|
||||||
"name": "blur",
|
|
||||||
"type": "blob",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"camera": {
|
|
||||||
"name": "camera",
|
|
||||||
"type": "text(128)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"title": {
|
|
||||||
"name": "title",
|
|
||||||
"type": "text(128)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"name": "description",
|
|
||||||
"type": "text(1024)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"exposureBiasValue": {
|
|
||||||
"name": "exposureBiasValue",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"fNumber": {
|
|
||||||
"name": "fNumber",
|
|
||||||
"type": "real",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"isoSpeedRatings": {
|
|
||||||
"name": "isoSpeedRatings",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"focalLength": {
|
|
||||||
"name": "focalLength",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"takenAt": {
|
|
||||||
"name": "takenAt",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"lensModel": {
|
|
||||||
"name": "lensModel",
|
|
||||||
"type": "text(128)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {
|
|
||||||
"photo_src_unique": {
|
|
||||||
"name": "photo_src_unique",
|
|
||||||
"columns": ["src"],
|
|
||||||
"isUnique": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"tokens": {
|
|
||||||
"name": "tokens",
|
|
||||||
"columns": {
|
|
||||||
"key": {
|
|
||||||
"name": "key",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"expires": {
|
|
||||||
"name": "expires",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"views": {},
|
|
||||||
"enums": {},
|
|
||||||
"_meta": {
|
|
||||||
"schemas": {},
|
|
||||||
"tables": {},
|
|
||||||
"columns": {}
|
|
||||||
},
|
|
||||||
"internal": {
|
|
||||||
"indexes": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,15 +5,8 @@
|
|||||||
{
|
{
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"version": "6",
|
"version": "6",
|
||||||
"when": 1746743224792,
|
"when": 1759536349615,
|
||||||
"tag": "0000_harsh_toad_men",
|
"tag": "0000_adorable_golden_guardian",
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 1,
|
|
||||||
"version": "6",
|
|
||||||
"when": 1756863378002,
|
|
||||||
"tag": "0001_familiar_gambit",
|
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
2156
package-lock.json
generated
2156
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
34
package.json
34
package.json
@@ -12,54 +12,54 @@
|
|||||||
"db:migrate": "drizzle-kit migrate",
|
"db:migrate": "drizzle-kit migrate",
|
||||||
"db:push": "drizzle-kit push",
|
"db:push": "drizzle-kit push",
|
||||||
"db:studio": "drizzle-kit studio",
|
"db:studio": "drizzle-kit studio",
|
||||||
"dev": "next dev --turbopack",
|
"dev": "next dev --hostname 0.0.0.0 --turbopack",
|
||||||
"preview": "next build && next start",
|
"preview": "next build && next start",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@auth/drizzle-adapter": "^1.10.0",
|
"@auth/drizzle-adapter": "^1.10.0",
|
||||||
"@aws-sdk/client-s3": "^3.839.0",
|
"@aws-sdk/client-s3": "^3.896.0",
|
||||||
"@heroicons/react": "^2.2.0",
|
"@heroicons/react": "^2.2.0",
|
||||||
"@hookform/resolvers": "^5.2.1",
|
"@hookform/resolvers": "^5.2.1",
|
||||||
"@libsql/client": "^0.15.9",
|
"@libsql/client": "^0.15.9",
|
||||||
"@mdx-js/loader": "^3.1.0",
|
"@mdx-js/loader": "^3.1.0",
|
||||||
"@mdx-js/react": "^3.1.0",
|
"@mdx-js/react": "^3.1.0",
|
||||||
"@next/mdx": "^15.3.4",
|
"@next/mdx": "^15.5.4",
|
||||||
"@t3-oss/env-nextjs": "^0.13.8",
|
"@t3-oss/env-nextjs": "^0.13.8",
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"@tanstack/react-query": "^5.81.5",
|
"@tanstack/react-query": "^5.90.2",
|
||||||
"@tiptap/extension-typography": "^3.2.2",
|
"@tiptap/extension-typography": "^3.5.1",
|
||||||
"@tiptap/pm": "^3.2.2",
|
"@tiptap/pm": "^3.5.1",
|
||||||
"@tiptap/react": "^3.2.2",
|
"@tiptap/react": "^3.5.1",
|
||||||
"@tiptap/starter-kit": "^3.2.2",
|
"@tiptap/starter-kit": "^3.5.1",
|
||||||
"@total-typescript/ts-reset": "^0.6.1",
|
"@total-typescript/ts-reset": "^0.6.1",
|
||||||
"@trpc/client": "^11.4.3",
|
"@trpc/client": "^11.4.3",
|
||||||
"@trpc/react-query": "^11.4.3",
|
"@trpc/react-query": "^11.4.3",
|
||||||
"@trpc/server": "^11.4.3",
|
"@trpc/server": "^11.4.3",
|
||||||
"@types/bun": "^1.2.17",
|
|
||||||
"@types/mdx": "^2.0.13",
|
"@types/mdx": "^2.0.13",
|
||||||
"babel-plugin-react-compiler": "^19.1.0-rc.2",
|
"babel-plugin-react-compiler": "^19.1.0-rc.2",
|
||||||
"daisyui": "^5.0.43",
|
"daisyui": "^5.1.18",
|
||||||
"drizzle-orm": "^0.44.2",
|
"drizzle-orm": "^0.44.2",
|
||||||
"exif-reader": "^2.0.2",
|
"exif-reader": "^2.0.2",
|
||||||
"framer-motion": "^12.19.2",
|
"framer-motion": "^12.23.21",
|
||||||
"glob": "^11.0.3",
|
"glob": "^11.0.3",
|
||||||
"next": "^15.3.4",
|
"next": "^15.5.4",
|
||||||
"next-auth": "5.0.0-beta.29",
|
"next-auth": "beta",
|
||||||
"radash": "^12.1.1",
|
"radash": "^12.1.1",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
|
"react-error-boundary": "^6.0.0",
|
||||||
"react-hook-form": "^7.62.0",
|
"react-hook-form": "^7.62.0",
|
||||||
"react-zoom-pan-pinch": "^3.7.0",
|
"react-zoom-pan-pinch": "^3.7.0",
|
||||||
"server-only": "^0.0.1",
|
"server-only": "^0.0.1",
|
||||||
"sharp": "^0.34.2",
|
"sharp": "^0.34.4",
|
||||||
"superjson": "^2.2.2",
|
"superjson": "^2.2.2",
|
||||||
"yet-another-react-lightbox": "^3.23.4",
|
"yet-another-react-lightbox": "^3.23.4",
|
||||||
"zod": "^3.25.67"
|
"zod": "^4.1.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "2.0.6",
|
"@biomejs/biome": "2.2.5",
|
||||||
"@tailwindcss/postcss": "^4.1.11",
|
"@tailwindcss/postcss": "^4.1.11",
|
||||||
"@types/node": "24.5.2",
|
"@types/node": "24.5.2",
|
||||||
"@types/react": "^19.1.8",
|
"@types/react": "^19.1.8",
|
||||||
|
|||||||
18
src/app/(root)/error.tsx
Normal file
18
src/app/(root)/error.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
"use client"; // Error boundaries must be Client Components
|
||||||
|
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
// biome-ignore lint/suspicious/noShadowRestrictedNames: This is a NextJS standard
|
||||||
|
export default function Error({ error }: { error: Error & { digest?: string } }) {
|
||||||
|
useEffect(() => {
|
||||||
|
// Log the error to an error reporting service
|
||||||
|
console.error(error);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="text-center">
|
||||||
|
<h1>Sorry, something went wrong!</h1>
|
||||||
|
<p>The error has been reported, try reloading but please reach out with information.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -9,9 +9,7 @@ export default function RootLayout({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<NavBar />
|
<NavBar />
|
||||||
<main className="mx-auto w-full flex-1 px-6 pt-8 pb-12 align-middle lg:max-w-5xl">
|
<main className="mx-auto w-full flex-1 px-6 pt-8 pb-12 align-middle lg:max-w-5xl">{children}</main>
|
||||||
{children}
|
|
||||||
</main>
|
|
||||||
<Footer />
|
<Footer />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,19 +1,8 @@
|
|||||||
export default function DirSvg(): React.JSX.Element {
|
export default function DirSvg(): React.JSX.Element {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" className="h-4 w-4">
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
strokeWidth="1.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
className="h-4 w-4"
|
|
||||||
>
|
|
||||||
<title>Directory</title>
|
<title>Directory</title>
|
||||||
<path
|
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 12.75V12A2.25 2.25 0 014.5 9.75h15A2.25 2.25 0 0121.75 12v.75m-8.69-6.44l-2.12-2.12a1.5 1.5 0 00-1.061-.44H4.5A2.25 2.25 0 002.25 6v12a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9a2.25 2.25 0 00-2.25-2.25h-5.379a1.5 1.5 0 01-1.06-.44z" />
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
d="M2.25 12.75V12A2.25 2.25 0 014.5 9.75h15A2.25 2.25 0 0121.75 12v.75m-8.69-6.44l-2.12-2.12a1.5 1.5 0 00-1.061-.44H4.5A2.25 2.25 0 002.25 6v12a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9a2.25 2.25 0 00-2.25-2.25h-5.379a1.5 1.5 0 01-1.06-.44z"
|
|
||||||
/>
|
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
export default function ImageSvg(): React.JSX.Element {
|
export default function ImageSvg(): React.JSX.Element {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" className="h-4 w-4">
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
strokeWidth="1.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
className="h-4 w-4"
|
|
||||||
>
|
|
||||||
<title>Item</title>
|
<title>Item</title>
|
||||||
<path
|
<path
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
|
|||||||
@@ -7,24 +7,10 @@ import Paragraph from "@tiptap/extension-paragraph";
|
|||||||
import Text from "@tiptap/extension-text";
|
import Text from "@tiptap/extension-text";
|
||||||
import Typography from "@tiptap/extension-typography";
|
import Typography from "@tiptap/extension-typography";
|
||||||
import { Placeholder, UndoRedo } from "@tiptap/extensions";
|
import { Placeholder, UndoRedo } from "@tiptap/extensions";
|
||||||
import {
|
import { type Content, type Editor, EditorContent, useEditor, useEditorState } from "@tiptap/react";
|
||||||
Editor,
|
|
||||||
EditorContent,
|
|
||||||
useEditor,
|
|
||||||
useEditorState,
|
|
||||||
type Content,
|
|
||||||
} from "@tiptap/react";
|
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
export default function Tiptap({
|
export default function Tiptap({ onChange, initContent, editorRef }: { onChange: (args: unknown) => void; initContent?: Content; editorRef: React.RefObject<Editor | null> }) {
|
||||||
onChange,
|
|
||||||
initContent,
|
|
||||||
editorRef,
|
|
||||||
}: {
|
|
||||||
onChange: (args: unknown) => void;
|
|
||||||
initContent?: Content;
|
|
||||||
editorRef: React.RefObject<Editor | null>;
|
|
||||||
}) {
|
|
||||||
const editor = useEditor({
|
const editor = useEditor({
|
||||||
extensions: [
|
extensions: [
|
||||||
Text,
|
Text,
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { ArrowPathIcon } from "@heroicons/react/24/outline";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import type { Editor } from "@tiptap/react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useRef, useState } from "react";
|
import { type JSX, useRef, useState } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import type { PhotoData } from "@/server/api/routers/photos/list";
|
import type { PhotoData } from "@/server/api/routers/photos/list";
|
||||||
@@ -11,14 +13,10 @@ import { api } from "@/trpc/react";
|
|||||||
import DirSvg from "./dir-svg";
|
import DirSvg from "./dir-svg";
|
||||||
import ImageSvg from "./file-svg";
|
import ImageSvg from "./file-svg";
|
||||||
import Tiptap from "./photo-editor";
|
import Tiptap from "./photo-editor";
|
||||||
import type { Editor } from "@tiptap/react";
|
|
||||||
|
|
||||||
// - TODO - Pull this from trpc
|
// - TODO - Pull this from trpc
|
||||||
const FormSchema = z.object({
|
const FormSchema = z.object({
|
||||||
title: z
|
title: z.string().min(3, "Title should be over 3 characters").max(128, "Title cannot be over 128 characters"),
|
||||||
.string()
|
|
||||||
.min(3, "Title should be over 3 characters")
|
|
||||||
.max(128, "Title cannot be over 128 characters"),
|
|
||||||
description: z.object({
|
description: z.object({
|
||||||
type: z.string(),
|
type: z.string(),
|
||||||
content: z.array(z.unknown()),
|
content: z.array(z.unknown()),
|
||||||
@@ -85,15 +83,9 @@ function renderTree(node: DirectoryTree, pathSoFar = ""): Item[] {
|
|||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
function RenderLeaf(
|
function RenderLeaf(leaf: Item[], selectImageTab: (path: string) => void, selectedImage: PhotoData | undefined) {
|
||||||
leaf: Item[],
|
|
||||||
selectImageTab: (path: string) => void,
|
|
||||||
selectedImage: PhotoData | undefined,
|
|
||||||
) {
|
|
||||||
return leaf.map((leaf) => {
|
return leaf.map((leaf) => {
|
||||||
const selectedLeaf =
|
const selectedLeaf = `https://fly.storage.tigris.dev/joemonk-photos/${leaf.fullPath}` === selectedImage?.src;
|
||||||
`https://fly.storage.tigris.dev/joemonk-photos/${leaf.fullPath}` ===
|
|
||||||
selectedImage?.src;
|
|
||||||
if (leaf.children?.length) {
|
if (leaf.children?.length) {
|
||||||
return (
|
return (
|
||||||
<li key={leaf.fullPath}>
|
<li key={leaf.fullPath}>
|
||||||
@@ -109,11 +101,7 @@ function RenderLeaf(
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<li key={leaf.fullPath}>
|
<li key={leaf.fullPath}>
|
||||||
<button
|
<button type="button" className={selectedLeaf ? "active" : ""} onClick={() => selectImageTab(leaf.fullPath)}>
|
||||||
type="button"
|
|
||||||
className={selectedLeaf ? "active" : ""}
|
|
||||||
onClick={() => selectImageTab(leaf.fullPath)}
|
|
||||||
>
|
|
||||||
<ImageSvg />
|
<ImageSvg />
|
||||||
{leaf.name}
|
{leaf.name}
|
||||||
</button>
|
</button>
|
||||||
@@ -122,10 +110,27 @@ function RenderLeaf(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function UpdatePhotos(): JSX.Element {
|
||||||
|
const updateQuery = api.photos.update.useQuery(undefined, {
|
||||||
|
enabled: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button type="button" disabled={updateQuery.isFetching} onClick={() => updateQuery.refetch()} className={`button btn btn-secondary btn-outline w-full border-1 group flex justify-between h-8 transition-colors disabled:border-info`}>
|
||||||
|
Load S3 into DB
|
||||||
|
<ArrowPathIcon
|
||||||
|
className="w-4 h-4
|
||||||
|
animate-spin pause group-disabled:play group-disabled:stroke-info transition-colors"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function PhotoTab(): React.JSX.Element {
|
export function PhotoTab(): React.JSX.Element {
|
||||||
const [selectedImage, setSelectedImage] = useState<PhotoData>();
|
const [selectedImage, setSelectedImage] = useState<PhotoData>();
|
||||||
const editorRef = useRef<Editor>(null);
|
const editorRef = useRef<Editor>(null);
|
||||||
const titleRef = useRef<HTMLInputElement>(null);
|
const titleRef = useRef<HTMLInputElement>(null);
|
||||||
|
const countQuery = api.photos.count.useQuery();
|
||||||
const listQuery = api.photos.list.useInfiniteQuery(
|
const listQuery = api.photos.list.useInfiniteQuery(
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
@@ -136,7 +141,6 @@ export function PhotoTab(): React.JSX.Element {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
const modifyMutate = api.photos.modify.useMutation();
|
const modifyMutate = api.photos.modify.useMutation();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
@@ -155,40 +159,38 @@ export function PhotoTab(): React.JSX.Element {
|
|||||||
return <p>{listQuery.error.message}</p>;
|
return <p>{listQuery.error.message}</p>;
|
||||||
}
|
}
|
||||||
const images = listQuery.data?.pages.flatMap((data) => data.data);
|
const images = listQuery.data?.pages.flatMap((data) => data.data);
|
||||||
if (!images || images?.length === 0) {
|
|
||||||
return <p>No Images</p>;
|
|
||||||
}
|
|
||||||
if (listQuery.hasNextPage) {
|
if (listQuery.hasNextPage) {
|
||||||
listQuery.fetchNextPage();
|
listQuery.fetchNextPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectImage = (path: string) => {
|
const selectImage = (path: string) => {
|
||||||
const img = images.find(
|
if (images) {
|
||||||
(img) =>
|
const img = images.find((img) => img.src === `https://fly.storage.tigris.dev/joemonk-photos/${path}`);
|
||||||
img.src === `https://fly.storage.tigris.dev/joemonk-photos/${path}`,
|
|
||||||
);
|
|
||||||
setSelectedImage(img);
|
setSelectedImage(img);
|
||||||
modifyMutate.reset();
|
modifyMutate.reset();
|
||||||
editorRef.current?.commands.setContent(img?.description ?? null);
|
editorRef.current?.commands.setContent(img?.description ?? null);
|
||||||
setValue("title", img?.title ?? "", { shouldTouch: true });
|
setValue("title", img?.title ?? "", { shouldTouch: true });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const tree = buildDirectoryTree(
|
const tree = buildDirectoryTree(images?.map((img) => img.src.substring("https://fly.storage.tigris.dev/joemonk-photos/".length)) ?? []);
|
||||||
images.map((img) =>
|
|
||||||
img.src.substring(
|
|
||||||
"https://fly.storage.tigris.dev/joemonk-photos/".length,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
const renderedTree = renderTree(tree);
|
const renderedTree = renderTree(tree);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className="relative">
|
||||||
<div className="flex w-full gap-4 md:gap-2 flex-col md:flex-row">
|
<div className="flex w-full gap-4 md:gap-2 flex-col md:flex-row">
|
||||||
<ul className="menu menu-xs bg-base-200 box w-full md:w-1/4">
|
<ul className="menu menu-xs bg-base-200 box w-full md:w-1/4">
|
||||||
{listQuery.hasNextPage || listQuery.isLoading ? (
|
{countQuery.data ? <p className="w-full pb-2 text-center">Photos in DB: {countQuery.data}</p> : <p>Couldn't get photo count from DB</p>}
|
||||||
<progress className="progress w-full"></progress>
|
<UpdatePhotos />
|
||||||
) : null}
|
<span className="divider m-2" />
|
||||||
|
{!images || images?.length === 0 ? (
|
||||||
|
<p>No images to load</p>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{listQuery.hasNextPage || listQuery.isLoading ? <progress className="progress w-full"></progress> : null}
|
||||||
{RenderLeaf(renderedTree, selectImage, selectedImage)}
|
{RenderLeaf(renderedTree, selectImage, selectedImage)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
<div className="md:w-3/4 box border border-base-300 p-2 w-full">
|
<div className="md:w-3/4 box border border-base-300 p-2 w-full">
|
||||||
{selectedImage?.src ? (
|
{selectedImage?.src ? (
|
||||||
@@ -201,26 +203,11 @@ export function PhotoTab(): React.JSX.Element {
|
|||||||
}),
|
}),
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<label
|
<label className={`floating-label input text-lg mb-2 w-full ${errors.title ? "input-error" : null}`}>
|
||||||
className={`floating-label input text-lg mb-2 w-full ${errors.title ? "input-error" : null}`}
|
|
||||||
>
|
|
||||||
<span>{`Title ${errors.title ? ` - ${errors.title.message}` : ""}`}</span>
|
<span>{`Title ${errors.title ? ` - ${errors.title.message}` : ""}`}</span>
|
||||||
<input
|
<input {...register("title", { value: selectedImage?.title })} ref={titleRef} type="text" placeholder="Title" />
|
||||||
{...register("title", { value: selectedImage?.title })}
|
|
||||||
ref={titleRef}
|
|
||||||
type="text"
|
|
||||||
placeholder="Title"
|
|
||||||
/>
|
|
||||||
</label>
|
</label>
|
||||||
<Image
|
<Image src={selectedImage.src} title={selectedImage?.title} alt={selectedImage?.title ?? "Image to modify data for"} width={selectedImage.width} height={selectedImage.height} blurDataURL={selectedImage.blur} placeholder="blur" />
|
||||||
src={selectedImage.src}
|
|
||||||
title={selectedImage?.title}
|
|
||||||
alt={selectedImage?.title ?? "Image to modify data for"}
|
|
||||||
width={selectedImage.width}
|
|
||||||
height={selectedImage.height}
|
|
||||||
blurDataURL={selectedImage.blur}
|
|
||||||
placeholder="blur"
|
|
||||||
/>
|
|
||||||
<div className="mt-2 grid grid-cols-3">
|
<div className="mt-2 grid grid-cols-3">
|
||||||
{[
|
{[
|
||||||
{
|
{
|
||||||
@@ -238,9 +225,7 @@ export function PhotoTab(): React.JSX.Element {
|
|||||||
].map((setting) => {
|
].map((setting) => {
|
||||||
return (
|
return (
|
||||||
<div key={setting.title} className="w-full border">
|
<div key={setting.title} className="w-full border">
|
||||||
<span className="px-2 w-20 inline-block">
|
<span className="px-2 w-20 inline-block">{setting.title}</span>
|
||||||
{setting.title}
|
|
||||||
</span>
|
|
||||||
<span className="px-2">{setting.value}</span>
|
<span className="px-2">{setting.value}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -263,9 +248,7 @@ export function PhotoTab(): React.JSX.Element {
|
|||||||
].map((setting) => {
|
].map((setting) => {
|
||||||
return (
|
return (
|
||||||
<div key={setting.title} className="w-full border">
|
<div key={setting.title} className="w-full border">
|
||||||
<span className="px-2 w-20 inline-block">
|
<span className="px-2 w-20 inline-block">{setting.title}</span>
|
||||||
{setting.title}
|
|
||||||
</span>
|
|
||||||
<span className="px-2">{setting.value}</span>
|
<span className="px-2">{setting.value}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -284,9 +267,7 @@ export function PhotoTab(): React.JSX.Element {
|
|||||||
].map((setting) => {
|
].map((setting) => {
|
||||||
return (
|
return (
|
||||||
<div key={setting.title} className="w-full border">
|
<div key={setting.title} className="w-full border">
|
||||||
<span className="px-2 w-20 inline-block">
|
<span className="px-2 w-20 inline-block">{setting.title}</span>
|
||||||
{setting.title}
|
|
||||||
</span>
|
|
||||||
<span className="px-2">{setting.value}</span>
|
<span className="px-2">{setting.value}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -295,38 +276,18 @@ export function PhotoTab(): React.JSX.Element {
|
|||||||
<div className="mt-2 px-2 pb-2 border">
|
<div className="mt-2 px-2 pb-2 border">
|
||||||
<span>Description</span>
|
<span>Description</span>
|
||||||
|
|
||||||
<Controller
|
<Controller control={control} name="description" render={({ field: { onChange } }) => <Tiptap onChange={onChange} initContent={selectedImage.description} editorRef={editorRef} />} />
|
||||||
control={control}
|
|
||||||
name="description"
|
|
||||||
render={({ field: { onChange } }) => (
|
|
||||||
<Tiptap
|
|
||||||
onChange={onChange}
|
|
||||||
initContent={selectedImage.description}
|
|
||||||
editorRef={editorRef}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row items-center">
|
<div className="flex flex-row items-center">
|
||||||
<button
|
<button className="btn btn-primary flex self-center m-4" type="submit">
|
||||||
className="btn btn-primary flex self-center m-4"
|
|
||||||
type="submit"
|
|
||||||
>
|
|
||||||
Save
|
Save
|
||||||
</button>
|
</button>
|
||||||
{modifyMutate.isSuccess ? (
|
{modifyMutate.isSuccess ? <p className="badge badge-success">Updated</p> : modifyMutate.isError ? <p className="badge badge-error">Error: {modifyMutate.error.message}</p> : modifyMutate.isPending ? <p className="badge badge-info">Updating</p> : null}
|
||||||
<p className="badge badge-success">Updated</p>
|
|
||||||
) : modifyMutate.isError ? (
|
|
||||||
<p className="badge badge-error">
|
|
||||||
Error: {modifyMutate.error.message}
|
|
||||||
</p>
|
|
||||||
) : modifyMutate.isPending ? (
|
|
||||||
<p className="badge badge-info">Updating</p>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,21 +4,10 @@ export default async function Photos(): Promise<React.JSX.Element> {
|
|||||||
return (
|
return (
|
||||||
<div className="mx-auto">
|
<div className="mx-auto">
|
||||||
<div role="tablist" className="tabs tabs-lift">
|
<div role="tablist" className="tabs tabs-lift">
|
||||||
<input
|
<input type="radio" name="admin_tabs" className="tab" aria-label="Posts" />
|
||||||
type="radio"
|
|
||||||
name="admin_tabs"
|
|
||||||
className="tab"
|
|
||||||
aria-label="Posts"
|
|
||||||
/>
|
|
||||||
<div className="tab-content bg-base-100 border-base-300 p-4"></div>
|
<div className="tab-content bg-base-100 border-base-300 p-4"></div>
|
||||||
|
|
||||||
<input
|
<input type="radio" name="admin_tabs" className="tab" aria-label="Photos" defaultChecked />
|
||||||
type="radio"
|
|
||||||
name="admin_tabs"
|
|
||||||
className="tab"
|
|
||||||
aria-label="Photos"
|
|
||||||
defaultChecked
|
|
||||||
/>
|
|
||||||
<div className="tab-content bg-base-100 border-base-300 p-4">
|
<div className="tab-content bg-base-100 border-base-300 p-4">
|
||||||
<PhotoTab />
|
<PhotoTab />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,18 +9,7 @@ export default async function Photos(): Promise<React.JSX.Element> {
|
|||||||
<div className="mx-auto">
|
<div className="mx-auto">
|
||||||
<FilteredLightbox photoData={images}>
|
<FilteredLightbox photoData={images}>
|
||||||
{images.map((image) => (
|
{images.map((image) => (
|
||||||
<Image
|
<Image key={image.src} alt={image.src} src={image.src} className="h-60 w-80 object-contain" sizes="100vw" loading="lazy" width={image.width} height={image.height} blurDataURL={image.blur} placeholder="blur" />
|
||||||
key={image.src}
|
|
||||||
alt={image.src}
|
|
||||||
src={image.src}
|
|
||||||
className="h-60 w-80 object-contain"
|
|
||||||
sizes="100vw"
|
|
||||||
loading="lazy"
|
|
||||||
width={image.width}
|
|
||||||
height={image.height}
|
|
||||||
blurDataURL={image.blur}
|
|
||||||
placeholder="blur"
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</FilteredLightbox>
|
</FilteredLightbox>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,12 +9,9 @@ export async function generateStaticParams(): Promise<
|
|||||||
slug: string[];
|
slug: string[];
|
||||||
}[]
|
}[]
|
||||||
> {
|
> {
|
||||||
const posts = await glob(
|
const posts = await glob(`${process.cwd()}/src/markdown/posts/[[]...slug[]]/**/*.mdx`, {
|
||||||
`${process.cwd()}/src/markdown/posts/[[]...slug[]]/**/*.mdx`,
|
|
||||||
{
|
|
||||||
nodir: true,
|
nodir: true,
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const postData = posts.map((post) => ({
|
const postData = posts.map((post) => ({
|
||||||
slug: [post.split("/").at(-1)?.slice(0, -4) ?? ""],
|
slug: [post.split("/").at(-1)?.slice(0, -4) ?? ""],
|
||||||
@@ -30,11 +27,6 @@ export default async function Post({
|
|||||||
slug: string[];
|
slug: string[];
|
||||||
};
|
};
|
||||||
}): Promise<React.JSX.Element> {
|
}): Promise<React.JSX.Element> {
|
||||||
const Post = dynamic(
|
const Post = dynamic(async () => import(`../../../../markdown/posts/[...slug]/${(await params).slug.join("/")}.mdx`));
|
||||||
async () =>
|
|
||||||
import(
|
|
||||||
`../../../../markdown/posts/[...slug]/${(await params).slug.join("/")}.mdx`
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return <Post />;
|
return <Post />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,18 +13,13 @@ type postDetails = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
async function loadPostDetails(): Promise<postDetails[]> {
|
async function loadPostDetails(): Promise<postDetails[]> {
|
||||||
const posts = await glob(
|
const posts = await glob(`${process.cwd()}/src/markdown/posts/[[]...slug[]]/**/*.mdx`, {
|
||||||
`${process.cwd()}/src/markdown/posts/[[]...slug[]]/**/*.mdx`,
|
|
||||||
{
|
|
||||||
nodir: true,
|
nodir: true,
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const loadPostData = posts.map(async (post: string) => {
|
const loadPostData = posts.map(async (post: string) => {
|
||||||
const slug = [post.split("/").at(-1)?.slice(0, -4)];
|
const slug = [post.split("/").at(-1)?.slice(0, -4)];
|
||||||
const mdxFile = (await import(
|
const mdxFile = (await import(`../../../../src/markdown/posts/[...slug]/${slug.join("/")}.mdx`)) as postDetails;
|
||||||
`../../../../src/markdown/posts/[...slug]/${slug.join("/")}.mdx`
|
|
||||||
)) as postDetails;
|
|
||||||
return {
|
return {
|
||||||
link: `/posts/${slug.join("/")}`,
|
link: `/posts/${slug.join("/")}`,
|
||||||
metadata: mdxFile.metadata,
|
metadata: mdxFile.metadata,
|
||||||
@@ -32,10 +27,7 @@ async function loadPostDetails(): Promise<postDetails[]> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const postData = await Promise.all(loadPostData);
|
const postData = await Promise.all(loadPostData);
|
||||||
return postData.sort(
|
return postData.sort((postA, postB) => Date.parse(postB.metadata.date) - Date.parse(postA.metadata.date));
|
||||||
(postA, postB) =>
|
|
||||||
Date.parse(postB.metadata.date) - Date.parse(postA.metadata.date),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPosts = unstable_cache(loadPostDetails, ["posts"], {
|
const getPosts = unstable_cache(loadPostDetails, ["posts"], {
|
||||||
|
|||||||
@@ -19,15 +19,8 @@ export default async function LogIn(): Promise<React.JSX.Element | undefined> {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<button
|
<button type="submit" className="btn btn-outline btn-circle hover:bg-primary/25 group border-2 border-primary/75 p-1 transition-colors duration-100">
|
||||||
type="submit"
|
<UserIcon className={`h-8 w-auto transition-colors ${session?.user ? "stroke-warning" : ""}`} />
|
||||||
className="btn btn-outline btn-circle hover:bg-primary/25 group border-2 border-primary/75 p-1 transition-colors duration-100"
|
|
||||||
>
|
|
||||||
<UserIcon
|
|
||||||
className={`h-8 w-auto transition-colors ${
|
|
||||||
session?.user ? "stroke-warning" : ""
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
<span className="sr-only">{session?.user ? "Log out" : "Log in"}</span>
|
<span className="sr-only">{session?.user ? "Log out" : "Log in"}</span>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -30,24 +30,11 @@ const content: ExperienceContent[] = [
|
|||||||
title: "Technical Lead",
|
title: "Technical Lead",
|
||||||
content: (
|
content: (
|
||||||
<>
|
<>
|
||||||
As a technical lead, my role moved mostly into communicating with the
|
As a technical lead, my role moved mostly into communicating with the wider business to ensure my team has clear, achievable objectives, then helping them release those objectives. I have been particularly involved with cross-team collaboration to continue pushing improvements to our development process, being
|
||||||
wider business to ensure my team has clear, achievable objectives, then
|
part of frontend and backend guilds as well as having a constant input into our service architecture to set development-wide architectural decisions. During this time, I have worked in multiple tech stacks, swiftly becoming proficient with frameworks and languages in order to upskill my team.
|
||||||
helping them release those objectives. I have been particularly involved
|
|
||||||
with cross-team collaboration to continue pushing improvements to our
|
|
||||||
development process, being part of frontend and backend guilds as well
|
|
||||||
as having a constant input into our service architecture to set
|
|
||||||
development-wide architectural decisions. During this time, I have
|
|
||||||
worked in multiple tech stacks, swiftly becoming proficient with
|
|
||||||
frameworks and languages in order to upskill my team.
|
|
||||||
<br />
|
<br />
|
||||||
Projects I have led include: rebuilding of some of the most used pages
|
Projects I have led include: rebuilding of some of the most used pages we have, releasing new web apps to millions of monthly users which included complex searching with filters and via a map, and user specific context while retaining high SEO scores and high levels of accessibility compliance; creating a new
|
||||||
we have, releasing new web apps to millions of monthly users which
|
email workflow, that can utilizes multiple accounts and services to protect reputation; and creating a genetic algorithm built to scale with which users can track their progress when crunching complex sets of data.
|
||||||
included complex searching with filters and via a map, and user specific
|
|
||||||
context while retaining high SEO scores and high levels of accessibility
|
|
||||||
compliance; creating a new email workflow, that can utilizes multiple
|
|
||||||
accounts and services to protect reputation; and creating a genetic
|
|
||||||
algorithm built to scale with which users can track their progress when
|
|
||||||
crunching complex sets of data.
|
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -68,29 +55,13 @@ const content: ExperienceContent[] = [
|
|||||||
title: "Development Manager",
|
title: "Development Manager",
|
||||||
content: (
|
content: (
|
||||||
<>
|
<>
|
||||||
As development manager, I oversaw all of the developers at Live 5 and
|
As development manager, I oversaw all of the developers at Live 5 and had a responsibility to oversee production of over 20 games a year from my teams. I kept each stage of game development on track to meet both internal and external deadlines, able to work with my teams to either change the scope of the
|
||||||
had a responsibility to oversee production of over 20 games a year from
|
project or move developers to get the games back on target. <PrintBreak count={4} /> By implementing a proper code review process, frequent stand ups and additional tooling for developers, qa and artists, we produced far more complex games in less time with fewer bugs. In addition, I mentored both junior and
|
||||||
my teams. I kept each stage of game development on track to meet both
|
senior members of my team to develop their technical skills, knowledge and soft skills.
|
||||||
internal and external deadlines, able to work with my teams to either
|
|
||||||
change the scope of the project or move developers to get the games back
|
|
||||||
on target. <PrintBreak count={4} /> By implementing a proper code review
|
|
||||||
process, frequent stand ups and additional tooling for developers, qa
|
|
||||||
and artists, we produced far more complex games in less time with fewer
|
|
||||||
bugs. In addition, I mentored both junior and senior members of my team
|
|
||||||
to develop their technical skills, knowledge and soft skills.
|
|
||||||
<br />
|
<br />
|
||||||
While managing the team was my foremost responsibility, I was still
|
While managing the team was my foremost responsibility, I was still heavily involved with development. I tackled any particularly difficult coding problems for the team and architecture large-scale changes within the codebase. For example, I integrated new business vital services and rebuilt our base renderer
|
||||||
heavily involved with development. I tackled any particularly difficult
|
and loading core in TypeScript. One of the more interesting projects I directed was to rebuild our backend, focusing on providing local and remote interfaces to the data generation that allowed for faster development of more reliable game backends. The deployment process was also rebuilt to allow deploying into
|
||||||
coding problems for the team and architecture large-scale changes within
|
AWS for browser game access, as a package for a separate serverless game build and to run a statistical analysis on a bare metal local kubernetes cluster which I also administered.
|
||||||
the codebase. For example, I integrated new business vital services and
|
|
||||||
rebuilt our base renderer and loading core in TypeScript. One of the
|
|
||||||
more interesting projects I directed was to rebuild our backend,
|
|
||||||
focusing on providing local and remote interfaces to the data generation
|
|
||||||
that allowed for faster development of more reliable game backends. The
|
|
||||||
deployment process was also rebuilt to allow deploying into AWS for
|
|
||||||
browser game access, as a package for a separate serverless game build
|
|
||||||
and to run a statistical analysis on a bare metal local kubernetes
|
|
||||||
cluster which I also administered.
|
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -114,11 +85,7 @@ const content: ExperienceContent[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
function Experience({
|
function Experience({ content }: { content: ExperienceContent }): React.JSX.Element {
|
||||||
content,
|
|
||||||
}: {
|
|
||||||
content: ExperienceContent;
|
|
||||||
}): React.JSX.Element {
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row gap-4 border-b-2 border-b-accent last:border-b-0">
|
<div className="flex flex-row gap-4 border-b-2 border-b-accent last:border-b-0">
|
||||||
<div className="w-20 justify-center text-center">
|
<div className="w-20 justify-center text-center">
|
||||||
@@ -132,9 +99,7 @@ function Experience({
|
|||||||
<div className="mb-2 flex w-full flex-row border-b pb-1">
|
<div className="mb-2 flex w-full flex-row border-b pb-1">
|
||||||
<div className="self-start text-left">{content.title}</div>
|
<div className="self-start text-left">{content.title}</div>
|
||||||
<div className="flex-grow self-start text-right">{content.tech}</div>
|
<div className="flex-grow self-start text-right">{content.tech}</div>
|
||||||
<div className="ml-3 w-20 border-dracula-bg-light border-l pr-2 text-right">
|
<div className="ml-3 w-20 border-dracula-bg-light border-l pr-2 text-right">{content.company}</div>
|
||||||
{content.company}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="pr-2 pb-2 text-justify">{content.content}</div>
|
<div className="pr-2 pb-2 text-justify">{content.content}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -147,9 +112,7 @@ export default function Cv(): React.JSX.Element {
|
|||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-[20cm] print:w-[20cm] print:pt-[0.5cm] border-accent">
|
<div className="mx-auto max-w-[20cm] print:w-[20cm] print:pt-[0.5cm] border-accent">
|
||||||
<div className="flex flex-col justify-center">
|
<div className="flex flex-col justify-center">
|
||||||
<h1 className="py-1 text-center font-medium text-2xl uppercase">
|
<h1 className="py-1 text-center font-medium text-2xl uppercase">Joe Lewis Monk</h1>
|
||||||
Joe Lewis Monk
|
|
||||||
</h1>
|
|
||||||
<div className="flex flex-col gap-2 p-2">
|
<div className="flex flex-col gap-2 p-2">
|
||||||
<div className="grid grid-cols-3 border-b-2 pb-2">
|
<div className="grid grid-cols-3 border-b-2 pb-2">
|
||||||
<span className="border-r text-left">joemonk.co.uk</span>
|
<span className="border-r text-left">joemonk.co.uk</span>
|
||||||
@@ -157,12 +120,8 @@ export default function Cv(): React.JSX.Element {
|
|||||||
<span className="border-l text-right">joemonk@hotmail.co.uk</span>
|
<span className="border-l text-right">joemonk@hotmail.co.uk</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-justify">
|
<p className="text-justify">
|
||||||
As a highly motivated and adaptive developer, my enthusiasm for
|
As a highly motivated and adaptive developer, my enthusiasm for learning new technologies, along with years of rapid game and web development, has driven my proficiency with many languages and tools. This allows me to be flexible when tackling problems. Over the last few years I have enjoyed expanding my
|
||||||
learning new technologies, along with years of rapid game and web
|
role to include management of multiple teams, large scale architecture.
|
||||||
development, has driven my proficiency with many languages and
|
|
||||||
tools. This allows me to be flexible when tackling problems. Over
|
|
||||||
the last few years I have enjoyed expanding my role to include
|
|
||||||
management of multiple teams, large scale architecture.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row align-middle gap-2 px-2 py-1">
|
<div className="flex flex-row align-middle gap-2 px-2 py-1">
|
||||||
@@ -171,10 +130,7 @@ export default function Cv(): React.JSX.Element {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4 py-2">
|
<div className="flex flex-col gap-4 py-2">
|
||||||
{content.map((expContent) => (
|
{content.map((expContent) => (
|
||||||
<Experience
|
<Experience content={expContent} key={`${expContent.company}_${expContent.title}`} />
|
||||||
content={expContent}
|
|
||||||
key={`${expContent.company}_${expContent.title}`}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,29 +2,14 @@
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
|
|
||||||
import {
|
import { isImageFitCover, isImageSlide, useLightboxProps, useLightboxState } from "yet-another-react-lightbox";
|
||||||
isImageFitCover,
|
|
||||||
isImageSlide,
|
|
||||||
useLightboxProps,
|
|
||||||
useLightboxState,
|
|
||||||
} from "yet-another-react-lightbox";
|
|
||||||
import "yet-another-react-lightbox/styles.css";
|
import "yet-another-react-lightbox/styles.css";
|
||||||
import "yet-another-react-lightbox/plugins/thumbnails.css";
|
import "yet-another-react-lightbox/plugins/thumbnails.css";
|
||||||
import "yet-another-react-lightbox/plugins/captions.css";
|
import "yet-another-react-lightbox/plugins/captions.css";
|
||||||
import type { RouterOutputs } from "@/trpc/react";
|
import type { RouterOutputs } from "@/trpc/react";
|
||||||
|
|
||||||
type PhotoData = RouterOutputs["photos"]["list"]["data"][number];
|
type PhotoData = RouterOutputs["photos"]["list"]["data"][number];
|
||||||
export default function LightboxImage({
|
export default function LightboxImage({ slide, offset, rect, unoptimized = false }: { slide: PhotoData; offset: number; rect: { width: number; height: number }; unoptimized: boolean }): React.JSX.Element {
|
||||||
slide,
|
|
||||||
offset,
|
|
||||||
rect,
|
|
||||||
unoptimized = false,
|
|
||||||
}: {
|
|
||||||
slide: PhotoData;
|
|
||||||
offset: number;
|
|
||||||
rect: { width: number; height: number };
|
|
||||||
unoptimized: boolean;
|
|
||||||
}): React.JSX.Element {
|
|
||||||
const {
|
const {
|
||||||
on: { click },
|
on: { click },
|
||||||
carousel: { imageFit },
|
carousel: { imageFit },
|
||||||
@@ -34,17 +19,9 @@ export default function LightboxImage({
|
|||||||
|
|
||||||
const cover = isImageSlide(slide) && isImageFitCover(slide, imageFit);
|
const cover = isImageSlide(slide) && isImageFitCover(slide, imageFit);
|
||||||
|
|
||||||
const width = !cover
|
const width = !cover ? Math.round(Math.min(rect.width, (rect.height / slide.height) * slide.width)) : rect.width;
|
||||||
? Math.round(
|
|
||||||
Math.min(rect.width, (rect.height / slide.height) * slide.width),
|
|
||||||
)
|
|
||||||
: rect.width;
|
|
||||||
|
|
||||||
const height = !cover
|
const height = !cover ? Math.round(Math.min(rect.height, (rect.width / slide.width) * slide.height)) : rect.height;
|
||||||
? Math.round(
|
|
||||||
Math.min(rect.height, (rect.width / slide.width) * slide.height),
|
|
||||||
)
|
|
||||||
: rect.height;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ position: "relative", width, height }}>
|
<div style={{ position: "relative", width, height }}>
|
||||||
@@ -62,11 +39,7 @@ export default function LightboxImage({
|
|||||||
cursor: click ? "pointer" : undefined,
|
cursor: click ? "pointer" : undefined,
|
||||||
}}
|
}}
|
||||||
sizes={`${Math.ceil((width / window.innerWidth) * 100)}vw`}
|
sizes={`${Math.ceil((width / window.innerWidth) * 100)}vw`}
|
||||||
onClick={
|
onClick={offset === 0 ? (): void => click?.({ index: currentIndex }) : undefined}
|
||||||
offset === 0
|
|
||||||
? (): void => click?.({ index: currentIndex })
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,13 +14,7 @@ import { api, type RouterOutputs } from "@/trpc/react";
|
|||||||
|
|
||||||
type PhotoData = RouterOutputs["photos"]["list"]["data"][number];
|
type PhotoData = RouterOutputs["photos"]["list"]["data"][number];
|
||||||
|
|
||||||
export function Lightbox({
|
export function Lightbox({ photoData, children }: { photoData: PhotoData[]; children: React.JSX.Element[] }): React.JSX.Element {
|
||||||
photoData,
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
photoData: PhotoData[];
|
|
||||||
children: React.JSX.Element[];
|
|
||||||
}): React.JSX.Element {
|
|
||||||
const [active, setActive] = useState<number | null>(null);
|
const [active, setActive] = useState<number | null>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -58,10 +52,7 @@ export function Lightbox({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function FilteredLightbox(props: {
|
export default function FilteredLightbox(props: { photoData: PhotoData[]; children: React.JSX.Element[] }): React.JSX.Element {
|
||||||
photoData: PhotoData[];
|
|
||||||
children: React.JSX.Element[];
|
|
||||||
}): React.JSX.Element {
|
|
||||||
const photoQuery = api.photos.list.useInfiniteQuery(
|
const photoQuery = api.photos.list.useInfiniteQuery(
|
||||||
{
|
{
|
||||||
limit: 1,
|
limit: 1,
|
||||||
@@ -76,7 +67,10 @@ export default function FilteredLightbox(props: {
|
|||||||
],
|
],
|
||||||
pageParams: [0],
|
pageParams: [0],
|
||||||
},
|
},
|
||||||
getNextPageParam: (lastPage) => lastPage.next,
|
getNextPageParam: (lastPage) => {
|
||||||
|
console.log(lastPage);
|
||||||
|
return lastPage.next ? lastPage.next > 0 : null;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -88,33 +82,13 @@ export default function FilteredLightbox(props: {
|
|||||||
|
|
||||||
const photoData = photoQuery.data.pages.flatMap((data) => data.data);
|
const photoData = photoQuery.data.pages.flatMap((data) => data.data);
|
||||||
|
|
||||||
const children = photoData.map((data) => (
|
const children = photoData.map((data) => <Image key={data.src} alt={data.src} src={data.src} className="h-60 w-80" loading="lazy" width={data.width} height={data.height} blurDataURL={data.blur} placeholder="blur" />);
|
||||||
<Image
|
|
||||||
key={data.src}
|
|
||||||
alt={data.src}
|
|
||||||
src={data.src}
|
|
||||||
className="h-60 w-80"
|
|
||||||
loading="lazy"
|
|
||||||
width={data.width}
|
|
||||||
height={data.height}
|
|
||||||
blurDataURL={data.blur}
|
|
||||||
placeholder="blur"
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Lightbox photoData={photoData}>{...children}</Lightbox>
|
<Lightbox photoData={photoData}>{...children}</Lightbox>
|
||||||
<button
|
<button type="button" className="btn btn-primary mx-auto p-4 mt-8 flex" onClick={handleNextPage}>
|
||||||
type="button"
|
{photoQuery.isLoading ? "Loading" : photoQuery.hasNextPage ? "Load next page" : "No more photos"}
|
||||||
className="btn btn-primary mx-auto p-4 mt-8 flex"
|
|
||||||
onClick={handleNextPage}
|
|
||||||
>
|
|
||||||
{photoQuery.isLoading
|
|
||||||
? "Loading"
|
|
||||||
: photoQuery.hasNextPage
|
|
||||||
? "Load next page"
|
|
||||||
: "No more photos"}
|
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,15 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import {
|
import { Bars3Icon, HomeModernIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||||
Bars3Icon,
|
import { AnimatePresence, domAnimation, LazyMotion, motion } from "framer-motion";
|
||||||
HomeModernIcon,
|
|
||||||
XMarkIcon,
|
|
||||||
} from "@heroicons/react/24/outline";
|
|
||||||
import {
|
|
||||||
AnimatePresence,
|
|
||||||
domAnimation,
|
|
||||||
LazyMotion,
|
|
||||||
motion,
|
|
||||||
} from "framer-motion";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
@@ -25,10 +16,7 @@ type NavBarClientProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
export default function NavBarClient({
|
export default function NavBarClient({ LogIn, navigation }: NavBarClientProps): React.JSX.Element {
|
||||||
LogIn,
|
|
||||||
navigation,
|
|
||||||
}: NavBarClientProps): React.JSX.Element {
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
@@ -54,16 +42,9 @@ export default function NavBarClient({
|
|||||||
border-2 border-primary/75 p-1 transition-colors duration-100 sm:hidden"
|
border-2 border-primary/75 p-1 transition-colors duration-100 sm:hidden"
|
||||||
onClick={() => setOpen(!open)}
|
onClick={() => setOpen(!open)}
|
||||||
>
|
>
|
||||||
{open ? (
|
{open ? <XMarkIcon className="h-8 w-auto rounded-sm" /> : <Bars3Icon className="h-8 w-auto rounded-sm" />}
|
||||||
<XMarkIcon className="h-8 w-auto rounded-sm" />
|
|
||||||
) : (
|
|
||||||
<Bars3Icon className="h-8 w-auto rounded-sm" />
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
<Link
|
<Link className="btn btn-outline hidden items-center rounded border-2 border-primary/75 p-1 transition-colors hover:bg-primary/25 sm:flex" href="/">
|
||||||
className="btn btn-outline hidden items-center rounded border-2 border-primary/75 p-1 transition-colors hover:bg-primary/25 sm:flex"
|
|
||||||
href="/"
|
|
||||||
>
|
|
||||||
<HomeModernIcon className="h-8 w-auto rounded-sm" />
|
<HomeModernIcon className="h-8 w-auto rounded-sm" />
|
||||||
</Link>
|
</Link>
|
||||||
<div className="ml-12 hidden gap-4 sm:flex">
|
<div className="ml-12 hidden gap-4 sm:flex">
|
||||||
@@ -71,9 +52,7 @@ export default function NavBarClient({
|
|||||||
<Link
|
<Link
|
||||||
key={item.name}
|
key={item.name}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
className={`btn btn-outline min-w-20 rounded-lg rounded-b-none border-transparent border-b-2 px-2 py-1 pt-1.5 text-center text-lg font-medium hover:border-primary hover:bg-primary/25 ${
|
className={`btn btn-outline min-w-20 rounded-lg rounded-b-none border-transparent border-b-2 px-2 py-1 pt-1.5 text-center text-lg font-medium hover:border-primary hover:bg-primary/25 ${item.current ? "border-b-accent/75" : ""}`}
|
||||||
item.current ? "border-b-accent/75" : ""
|
|
||||||
}`}
|
|
||||||
aria-current={item.current ? "page" : undefined}
|
aria-current={item.current ? "page" : undefined}
|
||||||
>
|
>
|
||||||
{item.name}
|
{item.name}
|
||||||
@@ -89,23 +68,10 @@ export default function NavBarClient({
|
|||||||
</div>
|
</div>
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{open ? (
|
{open ? (
|
||||||
<motion.div
|
<motion.div initial={{ height: 0 }} animate={{ height: "auto" }} transition={{ duration: 0.15, ease: "linear" }} exit={{ height: 0 }} className="overflow-hidden sm:hidden">
|
||||||
initial={{ height: 0 }}
|
|
||||||
animate={{ height: "auto" }}
|
|
||||||
transition={{ duration: 0.15, ease: "linear" }}
|
|
||||||
exit={{ height: 0 }}
|
|
||||||
className="overflow-hidden sm:hidden"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col space-y-1 py-1">
|
<div className="flex flex-col space-y-1 py-1">
|
||||||
{activeNavigation.map((item) => (
|
{activeNavigation.map((item) => (
|
||||||
<Link
|
<Link key={item.name} href={item.href} className={`btn btn-outline border-transparent border-l-4 px-4 py-2 transition-colors duration-100 hover:border-primary hover:bg-primary/25 ${item.current ? "" : "border-primary"}`} aria-current={item.current ? "page" : undefined}>
|
||||||
key={item.name}
|
|
||||||
href={item.href}
|
|
||||||
className={`btn btn-outline border-transparent border-l-4 px-4 py-2 transition-colors duration-100 hover:border-primary hover:bg-primary/25 ${
|
|
||||||
item.current ? "" : "border-primary"
|
|
||||||
}`}
|
|
||||||
aria-current={item.current ? "page" : undefined}
|
|
||||||
>
|
|
||||||
{item.name}
|
{item.name}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ export default async function NavBar(): Promise<React.JSX.Element> {
|
|||||||
let nav = structuredClone(defaultNavigation);
|
let nav = structuredClone(defaultNavigation);
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
|
|
||||||
console.log(session);
|
|
||||||
if (session?.user) {
|
if (session?.user) {
|
||||||
nav = nav.concat(structuredClone(authedNavigation));
|
nav = nav.concat(structuredClone(authedNavigation));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,9 +11,7 @@ type PostHeaderProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
export default function PostHeader({
|
export default function PostHeader({ metadata }: PostHeaderProps): React.JSX.Element {
|
||||||
metadata,
|
|
||||||
}: PostHeaderProps): React.JSX.Element {
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h1 className="mb-2">{metadata.title}</h1>
|
<h1 className="mb-2">{metadata.title}</h1>
|
||||||
|
|||||||
@@ -16,11 +16,7 @@ export default function ThemeSwitcher(): React.JSX.Element {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button type="button" className="btn btn-outline w-9 h-9 btn-circle border-2 hover:bg-primary/25 border-primary/75 p-1 transition-colors duration-100" onClick={toggleTheme}>
|
||||||
type="button"
|
|
||||||
className="btn btn-outline w-9 h-9 btn-circle border-2 hover:bg-primary/25 border-primary/75 p-1 transition-colors duration-100"
|
|
||||||
onClick={toggleTheme}
|
|
||||||
>
|
|
||||||
<MoonIcon className="block dark:hidden" />
|
<MoonIcon className="block dark:hidden" />
|
||||||
<SunIcon className="hidden dark:block" />
|
<SunIcon className="hidden dark:block" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -23,9 +23,7 @@ const handler = (req: NextRequest) =>
|
|||||||
onError:
|
onError:
|
||||||
process.env.NODE_ENV === "development"
|
process.env.NODE_ENV === "development"
|
||||||
? ({ path, error }) => {
|
? ({ path, error }) => {
|
||||||
console.error(
|
console.error(`❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`);
|
||||||
`❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
});
|
});
|
||||||
|
|||||||
25
src/app/error.tsx
Normal file
25
src/app/error.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
"use client"; // Error boundaries must be Client Components
|
||||||
|
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
export default function Error({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) {
|
||||||
|
useEffect(() => {
|
||||||
|
// Log the error to an error reporting service
|
||||||
|
console.error(error);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Something went wrong!</h1>
|
||||||
|
<button
|
||||||
|
className="button "
|
||||||
|
onClick={
|
||||||
|
// Attempt to recover by trying to re-render the segment
|
||||||
|
() => reset()
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Try again
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -18,15 +18,9 @@ const inter = Inter({
|
|||||||
weight: ["300", "400", "500", "600"],
|
weight: ["300", "400", "500", "600"],
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
|
||||||
children,
|
|
||||||
}: Readonly<{ children: React.ReactNode }>) {
|
|
||||||
return (
|
return (
|
||||||
<html
|
<html lang="en" className={`${inter.variable} w-screen overflow-x-hidden`} suppressHydrationWarning>
|
||||||
lang="en"
|
|
||||||
className={`${inter.variable} w-screen overflow-x-hidden`}
|
|
||||||
suppressHydrationWarning
|
|
||||||
>
|
|
||||||
<head>
|
<head>
|
||||||
<script
|
<script
|
||||||
id="SetTheme"
|
id="SetTheme"
|
||||||
@@ -37,9 +31,9 @@ export default function RootLayout({
|
|||||||
document.documentElement.setAttribute('data-theme', localStorage.theme)
|
document.documentElement.setAttribute('data-theme', localStorage.theme)
|
||||||
} else {
|
} else {
|
||||||
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||||
document.documentElement.setAttribute('data-theme', 'dark')
|
document.documentElement.setAttribute('data-theme', 'dracula-soft')
|
||||||
} else {
|
} else {
|
||||||
document.documentElement.setAttribute('data-theme', 'light')
|
document.documentElement.setAttribute('data-theme', 'alucard')
|
||||||
}
|
}
|
||||||
}`,
|
}`,
|
||||||
}}
|
}}
|
||||||
|
|||||||
17
src/env.js
17
src/env.js
@@ -7,19 +7,16 @@ export const env = createEnv({
|
|||||||
* isn't built with invalid env vars.
|
* isn't built with invalid env vars.
|
||||||
*/
|
*/
|
||||||
server: {
|
server: {
|
||||||
AUTH_SECRET:
|
AUTH_SECRET: process.env.NODE_ENV === "production" ? z.string() : z.string().optional(),
|
||||||
process.env.NODE_ENV === "production"
|
|
||||||
? z.string()
|
|
||||||
: z.string().optional(),
|
|
||||||
AUTH_CLIENT_ID: z.string(),
|
AUTH_CLIENT_ID: z.string(),
|
||||||
AUTH_CLIENT_SECRET: z.string(),
|
AUTH_CLIENT_SECRET: z.string(),
|
||||||
DATABASE_URL: z.string().url(),
|
DATABASE_URL: z.url(),
|
||||||
NODE_ENV: z
|
NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
|
||||||
.enum(["development", "test", "production"])
|
|
||||||
.default("development"),
|
|
||||||
S3_ACCESS_KEY_ID: z.string(),
|
S3_ACCESS_KEY_ID: z.string(),
|
||||||
S3_SECRET_ACCESS_KEY: z.string(),
|
S3_SECRET_ACCESS_KEY: z.string(),
|
||||||
PORT: z.number({ coerce: true }).int().default(3000),
|
S3_ENDPOINT: z.string(),
|
||||||
|
S3_BUCKET: z.string(),
|
||||||
|
PORT: z.coerce.number().int().default(3000),
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -43,6 +40,8 @@ export const env = createEnv({
|
|||||||
NODE_ENV: process.env.NODE_ENV,
|
NODE_ENV: process.env.NODE_ENV,
|
||||||
S3_ACCESS_KEY_ID: process.env.S3_ACCESS_KEY_ID,
|
S3_ACCESS_KEY_ID: process.env.S3_ACCESS_KEY_ID,
|
||||||
S3_SECRET_ACCESS_KEY: process.env.S3_SECRET_ACCESS_KEY,
|
S3_SECRET_ACCESS_KEY: process.env.S3_SECRET_ACCESS_KEY,
|
||||||
|
S3_ENDPOINT: process.env.S3_ENDPOINT,
|
||||||
|
S3_BUCKET: process.env.S3_BUCKET,
|
||||||
PORT: process.env.PORT,
|
PORT: process.env.PORT,
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,6 +2,6 @@ export function getBaseUrl(): string {
|
|||||||
if (process.env.NODE_ENV === "production") {
|
if (process.env.NODE_ENV === "production") {
|
||||||
return "https://joemonk.co.uk";
|
return "https://joemonk.co.uk";
|
||||||
} else {
|
} else {
|
||||||
return "https://3000.vscode.home.joemonk.co.uk";
|
return "http://3000.vscode.localhost";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,16 +3,8 @@ import type React from "react";
|
|||||||
|
|
||||||
export function useMDXComponents(components: MDXComponents): MDXComponents {
|
export function useMDXComponents(components: MDXComponents): MDXComponents {
|
||||||
return {
|
return {
|
||||||
wrapper: ({
|
wrapper: ({ children }: { children: React.JSX.Element[] }): React.JSX.Element => {
|
||||||
children,
|
return <article className="prose mx-auto first:prose-h2:mt-8">{children}</article>;
|
||||||
}: {
|
|
||||||
children: React.JSX.Element[];
|
|
||||||
}): React.JSX.Element => {
|
|
||||||
return (
|
|
||||||
<article className="prose mx-auto first:prose-h2:mt-8">
|
|
||||||
{children}
|
|
||||||
</article>
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
...components,
|
...components,
|
||||||
};
|
};
|
||||||
|
|||||||
6
src/server/api/routers/photos/count/index.ts
Normal file
6
src/server/api/routers/photos/count/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { db } from "@/server/db";
|
||||||
|
import { photos } from "@/server/db/schema";
|
||||||
|
|
||||||
|
export async function count(): Promise<number> {
|
||||||
|
return await db.$count(photos);
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { count } from "drizzle-orm";
|
||||||
import { shake } from "radash";
|
import { shake } from "radash";
|
||||||
import { db } from "@/server/db";
|
import { db } from "@/server/db";
|
||||||
import { photos } from "@/server/db/schema";
|
import { photos } from "@/server/db/schema";
|
||||||
@@ -25,12 +26,12 @@ export type ListOptions = {
|
|||||||
limit: number;
|
limit: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function list(options: ListOptions): Promise<PhotoData[]> {
|
export async function list(options: ListOptions): Promise<{
|
||||||
const currentSources = await db
|
images: PhotoData[];
|
||||||
.select()
|
count: number;
|
||||||
.from(photos)
|
}> {
|
||||||
.limit(options.limit)
|
const currentSources = await db.select().from(photos).limit(options.limit).offset(options.cursor);
|
||||||
.offset(options.cursor);
|
const photosCount = await db.$count(photos);
|
||||||
|
|
||||||
const images = currentSources.map((photo) => {
|
const images = currentSources.map((photo) => {
|
||||||
return {
|
return {
|
||||||
@@ -48,9 +49,12 @@ export async function list(options: ListOptions): Promise<PhotoData[]> {
|
|||||||
lensModel: photo.lensModel,
|
lensModel: photo.lensModel,
|
||||||
}),
|
}),
|
||||||
title: photo.title ?? undefined,
|
title: photo.title ?? undefined,
|
||||||
description: photo.description ?? undefined,
|
description: (photo.description as string) ?? undefined,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return images;
|
return {
|
||||||
|
images,
|
||||||
|
count: photosCount,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,5 @@ export async function modify(mod: {
|
|||||||
content: unknown[];
|
content: unknown[];
|
||||||
};
|
};
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
await db
|
await db.update(photos).set({ title: mod.title, description: mod.description }).where(eq(photos.src, mod.src));
|
||||||
.update(photos)
|
|
||||||
.set({ title: mod.title, description: mod.description })
|
|
||||||
.where(eq(photos.src, mod.src));
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,50 +1,47 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import {
|
import { createTRPCRouter, protectedProcedure, publicProcedure } from "@/server/api/trpc";
|
||||||
createTRPCRouter,
|
import { count } from "./count";
|
||||||
publicProcedure,
|
|
||||||
protectedProcedure,
|
|
||||||
} from "@/server/api/trpc";
|
|
||||||
import { list } from "./list";
|
import { list } from "./list";
|
||||||
import { update } from "./update";
|
|
||||||
import { modify } from "./modify";
|
import { modify } from "./modify";
|
||||||
|
import { update } from "./update";
|
||||||
|
|
||||||
export const photosRouter = createTRPCRouter({
|
export const photosRouter = createTRPCRouter({
|
||||||
list: publicProcedure
|
list: publicProcedure
|
||||||
.input(
|
.input(
|
||||||
z
|
z
|
||||||
.object({
|
.object({
|
||||||
limit: z.number().nonnegative().default(1),
|
limit: z.number().nonnegative().default(3),
|
||||||
cursor: z.number().nonnegative().default(0),
|
cursor: z.number().nonnegative().default(0),
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.default({}),
|
.default({
|
||||||
|
limit: 3,
|
||||||
|
cursor: 0,
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
const ret = await list({
|
const ret = await list({
|
||||||
limit: input.limit + 1,
|
limit: input.limit,
|
||||||
cursor: input.cursor,
|
cursor: input.cursor,
|
||||||
});
|
});
|
||||||
|
|
||||||
let next: number | undefined;
|
let next: number | undefined;
|
||||||
if (ret.length > input.limit) {
|
if (ret.count > input.limit + input.cursor) {
|
||||||
next = input.limit;
|
next = input.limit + input.cursor;
|
||||||
ret.pop();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: ret,
|
data: ret.images,
|
||||||
next,
|
next,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
count: publicProcedure.query(count),
|
||||||
update: publicProcedure.query(update),
|
update: publicProcedure.query(update),
|
||||||
modify: protectedProcedure
|
modify: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
title: z
|
title: z.string().min(3, "Title should be over 3 characters").max(128, "Title cannot be over 128 characters"),
|
||||||
.string()
|
|
||||||
.min(3, "Title should be over 3 characters")
|
|
||||||
.max(128, "Title cannot be over 128 characters"),
|
|
||||||
src: z.string(),
|
src: z.string(),
|
||||||
description: z.object({
|
description: z.object({
|
||||||
type: z.string(),
|
type: z.string(),
|
||||||
|
|||||||
@@ -1,26 +1,27 @@
|
|||||||
import {
|
import { GetObjectCommand, ListObjectsV2Command, S3Client } from "@aws-sdk/client-s3";
|
||||||
GetObjectCommand,
|
|
||||||
ListObjectsV2Command,
|
|
||||||
S3Client,
|
|
||||||
} from "@aws-sdk/client-s3";
|
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import exifReader from "exif-reader";
|
import exifReader from "exif-reader";
|
||||||
import { diff, sift } from "radash";
|
import { diff, sift } from "radash";
|
||||||
import sharp from "sharp";
|
import sharp from "sharp";
|
||||||
|
import { env } from "@/env";
|
||||||
import { db } from "@/server/db";
|
import { db } from "@/server/db";
|
||||||
import { photos } from "@/server/db/schema";
|
import { photos } from "@/server/db/schema";
|
||||||
|
|
||||||
export async function update(): Promise<string[]> {
|
export async function update(): Promise<string[]> {
|
||||||
const allPhotos = await db.select().from(photos);
|
const allPhotos = await db.select({ src: photos.src }).from(photos);
|
||||||
const currentSources = allPhotos.map((photo) => photo.src);
|
const currentSources = allPhotos.map((photo) => photo.src);
|
||||||
|
|
||||||
const s3Client = new S3Client({
|
const s3Client = new S3Client({
|
||||||
region: "auto",
|
region: "auto",
|
||||||
endpoint: "https://fly.storage.tigris.dev",
|
endpoint: env.S3_ENDPOINT,
|
||||||
|
credentials: {
|
||||||
|
accessKeyId: env.S3_ACCESS_KEY_ID,
|
||||||
|
secretAccessKey: env.S3_SECRET_ACCESS_KEY,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const listObjCmd = new ListObjectsV2Command({
|
const listObjCmd = new ListObjectsV2Command({
|
||||||
Bucket: "joemonk-photos",
|
Bucket: env.S3_BUCKET,
|
||||||
});
|
});
|
||||||
|
|
||||||
const s3Res = await s3Client.send(listObjCmd);
|
const s3Res = await s3Client.send(listObjCmd);
|
||||||
@@ -49,18 +50,13 @@ export async function update(): Promise<string[]> {
|
|||||||
const photoData = newPhotos.map(async (fileName: string) => {
|
const photoData = newPhotos.map(async (fileName: string) => {
|
||||||
const getImageCmd = new GetObjectCommand({
|
const getImageCmd = new GetObjectCommand({
|
||||||
Bucket: "joemonk-photos",
|
Bucket: "joemonk-photos",
|
||||||
Key: fileName.replace(
|
Key: fileName.replace("https://fly.storage.tigris.dev/joemonk-photos/", ""),
|
||||||
"https://fly.storage.tigris.dev/joemonk-photos/",
|
|
||||||
"",
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
const imgRes = await s3Client.send(getImageCmd);
|
const imgRes = await s3Client.send(getImageCmd);
|
||||||
const image = await imgRes.Body?.transformToByteArray();
|
const image = await imgRes.Body?.transformToByteArray();
|
||||||
|
|
||||||
const { width, height, exif } = await sharp(image).metadata();
|
const { width, height, exif } = await sharp(image).metadata();
|
||||||
const blur = await sharp(image)
|
const blur = await sharp(image).resize({ width: 12, height: 12, fit: "inside" }).toBuffer();
|
||||||
.resize({ width: 12, height: 12, fit: "inside" })
|
|
||||||
.toBuffer();
|
|
||||||
const exifData = exif ? exifReader(exif) : undefined;
|
const exifData = exif ? exifReader(exif) : undefined;
|
||||||
|
|
||||||
const photo: typeof photos.$inferInsert = {
|
const photo: typeof photos.$inferInsert = {
|
||||||
|
|||||||
@@ -50,8 +50,7 @@ const t = initTRPC.context<typeof createTRPCContext>().create({
|
|||||||
...shape,
|
...shape,
|
||||||
data: {
|
data: {
|
||||||
...shape.data,
|
...shape.data,
|
||||||
zodError:
|
zodError: error.cause instanceof ZodError ? error.cause.flatten() : null,
|
||||||
error.cause instanceof ZodError ? error.cause.flatten() : null,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -118,9 +117,7 @@ export const publicProcedure = t.procedure.use(timingMiddleware);
|
|||||||
*
|
*
|
||||||
* @see https://trpc.io/docs/procedures
|
* @see https://trpc.io/docs/procedures
|
||||||
*/
|
*/
|
||||||
export const protectedProcedure = t.procedure
|
export const protectedProcedure = t.procedure.use(timingMiddleware).use(({ ctx, next }) => {
|
||||||
.use(timingMiddleware)
|
|
||||||
.use(({ ctx, next }) => {
|
|
||||||
if (!ctx.session?.user) {
|
if (!ctx.session?.user) {
|
||||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,8 +36,7 @@ export const authConfig = {
|
|||||||
issuer: "https://auth.home.joemonk.co.uk",
|
issuer: "https://auth.home.joemonk.co.uk",
|
||||||
clientId: process.env.AUTH_CLIENT_ID,
|
clientId: process.env.AUTH_CLIENT_ID,
|
||||||
clientSecret: process.env.AUTH_CLIENT_SECRET,
|
clientSecret: process.env.AUTH_CLIENT_SECRET,
|
||||||
wellKnown:
|
wellKnown: "https://auth.home.joemonk.co.uk/.well-known/openid-configuration",
|
||||||
"https://auth.home.joemonk.co.uk/.well-known/openid-configuration",
|
|
||||||
idToken: true,
|
idToken: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ const globalForDb = globalThis as unknown as {
|
|||||||
client: Client | undefined;
|
client: Client | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const client =
|
export const client = globalForDb.client ?? createClient({ url: env.DATABASE_URL });
|
||||||
globalForDb.client ?? createClient({ url: env.DATABASE_URL });
|
|
||||||
if (process.env.NODE_ENV !== "production") globalForDb.client = client;
|
if (process.env.NODE_ENV !== "production") globalForDb.client = client;
|
||||||
|
|
||||||
export const db = drizzle(client, { schema });
|
export const db = drizzle(client, { schema });
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const photos = sqliteTable("photo", (d) => ({
|
|||||||
|
|
||||||
camera: d.text({ length: 128 }),
|
camera: d.text({ length: 128 }),
|
||||||
title: d.text({ length: 128 }),
|
title: d.text({ length: 128 }),
|
||||||
description: d.blob({ mode: "json" }),
|
description: d.text({ mode: "json" }),
|
||||||
exposureBiasValue: d.integer({ mode: "number" }),
|
exposureBiasValue: d.integer({ mode: "number" }),
|
||||||
fNumber: d.real(),
|
fNumber: d.real(),
|
||||||
isoSpeedRatings: d.integer({ mode: "number" }),
|
isoSpeedRatings: d.integer({ mode: "number" }),
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
@plugin "daisyui/theme" {
|
@plugin "daisyui/theme" {
|
||||||
/* Nicked from the vscode soft theme https://github.com/dracula/visual-studio-code/blob/master/src/dracula.yml */
|
/* Nicked from the vscode soft theme https://github.com/dracula/visual-studio-code/blob/master/src/dracula.yml */
|
||||||
name: "dracula-soft";
|
name: "dracula-soft";
|
||||||
default: false;
|
default: true;
|
||||||
prefersdark: true;
|
prefersdark: true;
|
||||||
color-scheme: "dark";
|
color-scheme: "dark";
|
||||||
|
|
||||||
@@ -90,9 +90,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
--font-sans:
|
--font-sans: var(--font-inter), ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||||
var(--font-inter), ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
|
--animate-spin: spin 2s linear infinite;
|
||||||
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@custom-variant dark (&:where([data-theme=dracula-soft], [data-theme=dracula-soft] *));
|
@custom-variant dark (&:where([data-theme=dracula-soft], [data-theme=dracula-soft] *));
|
||||||
@@ -100,7 +99,13 @@
|
|||||||
@utility btn {
|
@utility btn {
|
||||||
@apply shadow-none;
|
@apply shadow-none;
|
||||||
}
|
}
|
||||||
|
@utility pause {
|
||||||
|
animation-play-state: paused;
|
||||||
|
}
|
||||||
|
@utility play {
|
||||||
|
animation-play-state: running;
|
||||||
|
}
|
||||||
|
|
||||||
:root .prose {
|
:root .prose {
|
||||||
--tw-prose-body: color-mix(in oklab, var(--color-base-content) 92%, #0000) !important;
|
--tw-prose-body: color-mix(in oklab, var(--color-base-content) 92%, #0000);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import {
|
import { defaultShouldDehydrateQuery, QueryClient } from "@tanstack/react-query";
|
||||||
defaultShouldDehydrateQuery,
|
|
||||||
QueryClient,
|
|
||||||
} from "@tanstack/react-query";
|
|
||||||
import SuperJSON from "superjson";
|
import SuperJSON from "superjson";
|
||||||
|
|
||||||
export const createQueryClient = () =>
|
export const createQueryClient = () =>
|
||||||
@@ -14,9 +11,7 @@ export const createQueryClient = () =>
|
|||||||
},
|
},
|
||||||
dehydrate: {
|
dehydrate: {
|
||||||
serializeData: SuperJSON.serialize,
|
serializeData: SuperJSON.serialize,
|
||||||
shouldDehydrateQuery: (query) =>
|
shouldDehydrateQuery: (query) => defaultShouldDehydrateQuery(query) || query.state.status === "pending",
|
||||||
defaultShouldDehydrateQuery(query) ||
|
|
||||||
query.state.status === "pending",
|
|
||||||
},
|
},
|
||||||
hydrate: {
|
hydrate: {
|
||||||
deserializeData: SuperJSON.deserialize,
|
deserializeData: SuperJSON.deserialize,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { createTRPCReact } from "@trpc/react-query";
|
|||||||
import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server";
|
import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import SuperJSON from "superjson";
|
import SuperJSON from "superjson";
|
||||||
|
import { getBaseUrl } from "@/lib/base-url";
|
||||||
import type { AppRouter } from "@/server/api/root";
|
import type { AppRouter } from "@/server/api/root";
|
||||||
import { createQueryClient } from "./query-client";
|
import { createQueryClient } from "./query-client";
|
||||||
|
|
||||||
@@ -44,9 +45,7 @@ export function TRPCReactProvider(props: { children: React.ReactNode }) {
|
|||||||
api.createClient({
|
api.createClient({
|
||||||
links: [
|
links: [
|
||||||
loggerLink({
|
loggerLink({
|
||||||
enabled: (op) =>
|
enabled: (op) => process.env.NODE_ENV === "development" || (op.direction === "down" && op.result instanceof Error),
|
||||||
process.env.NODE_ENV === "development" ||
|
|
||||||
(op.direction === "down" && op.result instanceof Error),
|
|
||||||
}),
|
}),
|
||||||
httpBatchStreamLink({
|
httpBatchStreamLink({
|
||||||
transformer: SuperJSON,
|
transformer: SuperJSON,
|
||||||
@@ -69,9 +68,3 @@ export function TRPCReactProvider(props: { children: React.ReactNode }) {
|
|||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBaseUrl() {
|
|
||||||
if (typeof window !== "undefined") return window.location.origin;
|
|
||||||
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
|
|
||||||
return "https://3000.vscode.home.joemonk.co.uk";
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -24,7 +24,4 @@ const createContext = cache(async () => {
|
|||||||
const getQueryClient = cache(createQueryClient);
|
const getQueryClient = cache(createQueryClient);
|
||||||
const caller = createCaller(createContext);
|
const caller = createCaller(createContext);
|
||||||
|
|
||||||
export const { trpc: api, HydrateClient } = createHydrationHelpers<AppRouter>(
|
export const { trpc: api, HydrateClient } = createHydrationHelpers<AppRouter>(caller, getQueryClient);
|
||||||
caller,
|
|
||||||
getQueryClient,
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -30,13 +30,6 @@
|
|||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.cjs", "**/*.js", ".next/types/**/*.ts"],
|
||||||
"next-env.d.ts",
|
|
||||||
"**/*.ts",
|
|
||||||
"**/*.tsx",
|
|
||||||
"**/*.cjs",
|
|
||||||
"**/*.js",
|
|
||||||
".next/types/**/*.ts"
|
|
||||||
],
|
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user