Set up photo editor and clean up styling

This commit is contained in:
2025-09-21 19:08:08 +01:00
parent 42caeb8834
commit 784f7320a1
19 changed files with 9067 additions and 1435 deletions

1222
bun.lock

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,10 @@
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
);

View File

@@ -0,0 +1,187 @@
{
"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": {}
}
}

View File

@@ -8,6 +8,13 @@
"when": 1746743224792,
"tag": "0000_harsh_toad_men",
"breakpoints": true
},
{
"idx": 1,
"version": "6",
"when": 1756863378002,
"tag": "0001_familiar_gambit",
"breakpoints": true
}
]
}

8588
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -61,6 +61,7 @@
"devDependencies": {
"@biomejs/biome": "2.0.6",
"@tailwindcss/postcss": "^4.1.11",
"@types/node": "24.5.2",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"drizzle-kit": "^0.31.4",

View File

@@ -1,19 +1,26 @@
"use client";
import { useEditor, EditorContent, useEditorState } from "@tiptap/react";
import Bold from "@tiptap/extension-bold";
import Document from "@tiptap/extension-document";
import Italic from "@tiptap/extension-italic";
import Paragraph from "@tiptap/extension-paragraph";
import Text from "@tiptap/extension-text";
import Bold from "@tiptap/extension-bold";
import Italic from "@tiptap/extension-italic";
import Typography from "@tiptap/extension-typography";
import { UndoRedo, Placeholder } from "@tiptap/extensions";
import { Placeholder, UndoRedo } from "@tiptap/extensions";
import {
EditorContent,
useEditor,
useEditorState,
type Content,
} from "@tiptap/react";
import { useEffect } from "react";
export default function Tiptap({
onChange,
initContent,
}: {
onChange: (args: unknown) => void;
initContent?: Content;
}) {
const editor = useEditor({
extensions: [
@@ -35,6 +42,7 @@ export default function Tiptap({
class: "py-1 px-2",
},
},
content: initContent,
});
const editorState = useEditorState({
@@ -53,7 +61,6 @@ export default function Tiptap({
});
useEffect(() => {
console.log(editorState?.currentContent);
onChange(editorState?.currentContent);
}, [editorState?.currentContent, onChange]);

View File

@@ -1,17 +1,18 @@
"use client";
import type { PhotoData } from "@/server/api/routers/photos/list";
import { api } from "@/trpc/react";
import { zodResolver } from "@hookform/resolvers/zod";
import Image from "next/image";
import type React from "react";
import { useState } from "react";
import ImageSvg from "./file-svg";
import DirSvg from "./dir-svg";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import z from "zod";
import type { PhotoData } from "@/server/api/routers/photos/list";
import { api } from "@/trpc/react";
import DirSvg from "./dir-svg";
import ImageSvg from "./file-svg";
import Tiptap from "./photo-editor";
// - TODO - Pull this from trpc
const FormSchema = z.object({
title: z
.string()
@@ -83,24 +84,35 @@ function renderTree(node: DirectoryTree, pathSoFar = ""): Item[] {
return items;
}
function RenderLeaf(leaf: Item[], selectImageTab: (path: string) => void) {
function RenderLeaf(
leaf: Item[],
selectImageTab: (path: string) => void,
selectedImage: PhotoData | undefined,
) {
return leaf.map((leaf) => {
const selectedLeaf =
`https://fly.storage.tigris.dev/joemonk-photos/${leaf.fullPath}` ===
selectedImage?.src;
if (leaf.children?.length) {
return (
<li>
<li key={leaf.fullPath}>
<details open>
<summary>
<DirSvg />
{leaf.name}
</summary>
<ul>{RenderLeaf(leaf.children, selectImageTab)}</ul>
<ul>{RenderLeaf(leaf.children, selectImageTab, selectedImage)}</ul>
</details>
</li>
);
}
return (
<li key={leaf.fullPath}>
<button type="button" onClick={() => selectImageTab(leaf.fullPath)}>
<button
type="button"
className={selectedLeaf ? "active" : ""}
onClick={() => selectImageTab(leaf.fullPath)}
>
<ImageSvg />
{leaf.name}
</button>
@@ -111,9 +123,16 @@ function RenderLeaf(leaf: Item[], selectImageTab: (path: string) => void) {
export function PhotoTab(): React.JSX.Element {
const [selectedImage, setSelectedImage] = useState<PhotoData>();
const query = api.photos.list.useQuery(undefined, {
refetchOnWindowFocus: false,
});
const listQuery = api.photos.list.useInfiniteQuery(
{},
{
getNextPageParam: (lastPage) => lastPage.next,
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
},
);
const modifyMutate = api.photos.modify.useMutation();
const {
register,
@@ -125,18 +144,21 @@ export function PhotoTab(): React.JSX.Element {
mode: "onSubmit",
});
if (query.isLoading) {
if (listQuery.isLoading) {
return <p>Loading</p>;
}
if (query.error) {
return <p>{query.error.message}</p>;
if (listQuery.error) {
return <p>{listQuery.error.message}</p>;
}
const images = query.data?.data;
const images = listQuery.data?.pages.flatMap((data) => data.data);
if (!images || images?.length === 0) {
return <p>No Images</p>;
}
if (listQuery.hasNextPage) {
listQuery.fetchNextPage();
}
const selectImageTab = (path: string) => {
const selectImage = (path: string) => {
const img = images.find(
(img) =>
img.src === `https://fly.storage.tigris.dev/joemonk-photos/${path}`,
@@ -153,22 +175,26 @@ export function PhotoTab(): React.JSX.Element {
);
const renderedTree = renderTree(tree);
const onSubmit = (data: IFormInput) => {
console.log(data);
};
return (
<div className="flex w-full gap-2">
<ul className="menu menu-xs bg-base-200 box w-1/4">
{RenderLeaf(renderedTree, selectImageTab)}
<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">
{RenderLeaf(renderedTree, selectImage, selectedImage)}
</ul>
<div className="w-3/4 box border border-base-300 p-2">
<div className="md:w-3/4 box border border-base-300 p-2 w-full">
{selectedImage?.src ? (
<form onSubmit={handleSubmit(onSubmit)}>
<form
onSubmit={handleSubmit((data) =>
modifyMutate.mutate({
title: data.title,
description: data.description,
src: selectedImage.src,
}),
)}
>
<label
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
{...register("title")}
type="text"
@@ -263,13 +289,30 @@ export function PhotoTab(): React.JSX.Element {
control={control}
name="description"
render={({ field: { onChange } }) => (
<Tiptap onChange={onChange} />
<Tiptap
onChange={onChange}
initContent={selectedImage.description}
/>
)}
/>
</div>
<button className="button" type="submit">
Submit
</button>
<div className="flex flex-row items-center">
<button
className="btn btn-primary flex self-center m-4"
type="submit"
>
Save
</button>
{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}
</div>
</form>
) : null}
</div>

View File

@@ -3,7 +3,7 @@ import { PhotoTab } from "./_components/photo-tab";
export default async function Photos(): Promise<React.JSX.Element> {
return (
<div className="mx-auto">
<div className="tabs tabs-lift">
<div role="tablist" className="tabs tabs-lift">
<input
type="radio"
name="admin_tabs"

View File

@@ -53,7 +53,7 @@ export default async function Posts(): Promise<React.JSX.Element> {
<div className="card-body">
<h1 className="card-title">{post.metadata.title}</h1>
<time dateTime={post.metadata.date}>{post.metadata.date}</time>
<div className="flex flex-row gap-2 pb-2">
<div className="flex flex-row flex-wrap gap-2 pb-2">
{post.metadata.tags.map((tag) => {
return (
<div key={`${post.link}_${tag}`}>
@@ -64,10 +64,7 @@ export default async function Posts(): Promise<React.JSX.Element> {
</div>
<p>{post.metadata.blurb}</p>
<div className="card-actions justify-end pt-2">
<Link
className="btn btn-primary hover:bg-primary/10"
href={post.link}
>
<Link className="btn btn-primary" href={post.link}>
Read
</Link>
</div>

View File

@@ -0,0 +1,73 @@
"use client";
import Image from "next/image";
import type React from "react";
import {
isImageFitCover,
isImageSlide,
useLightboxProps,
useLightboxState,
} from "yet-another-react-lightbox";
import "yet-another-react-lightbox/styles.css";
import "yet-another-react-lightbox/plugins/thumbnails.css";
import "yet-another-react-lightbox/plugins/captions.css";
import type { RouterOutputs } from "@/trpc/react";
type PhotoData = RouterOutputs["photos"]["list"]["data"][number];
export default function LightboxImage({
slide,
offset,
rect,
unoptimized = false,
}: {
slide: PhotoData;
offset: number;
rect: { width: number; height: number };
unoptimized: boolean;
}): React.JSX.Element {
const {
on: { click },
carousel: { imageFit },
} = useLightboxProps();
const { currentIndex } = useLightboxState();
const cover = isImageSlide(slide) && isImageFitCover(slide, imageFit);
const width = !cover
? Math.round(
Math.min(rect.width, (rect.height / slide.height) * slide.width),
)
: rect.width;
const height = !cover
? Math.round(
Math.min(rect.height, (rect.width / slide.width) * slide.height),
)
: rect.height;
return (
<div style={{ position: "relative", width, height }}>
<Image
fill
alt=""
src={slide}
loading="lazy"
unoptimized={unoptimized}
draggable={false}
blurDataURL={slide.blur}
placeholder="blur"
style={{
objectFit: cover ? "cover" : "contain",
cursor: click ? "pointer" : undefined,
}}
sizes={`${Math.ceil((width / window.innerWidth) * 100)}vw`}
onClick={
offset === 0
? (): void => click?.({ index: currentIndex })
: undefined
}
/>
</div>
);
}

View File

@@ -2,16 +2,11 @@
import Image from "next/image";
import type React from "react";
import { useState } from "react";
import YARL, {
isImageFitCover,
isImageSlide,
useLightboxProps,
useLightboxState,
} from "yet-another-react-lightbox";
import YARL from "yet-another-react-lightbox";
import Captions from "yet-another-react-lightbox/plugins/captions";
import Thumbnails from "yet-another-react-lightbox/plugins/thumbnails";
import Zoom from "yet-another-react-lightbox/plugins/zoom";
import LightboxImage from "./lightbox-image";
import "yet-another-react-lightbox/styles.css";
import "yet-another-react-lightbox/plugins/thumbnails.css";
import "yet-another-react-lightbox/plugins/captions.css";
@@ -19,66 +14,8 @@ import { api, type RouterOutputs } from "@/trpc/react";
type PhotoData = RouterOutputs["photos"]["list"]["data"][number];
function NextJsImage({
slide,
offset,
rect,
unoptimized = false,
}: {
slide: PhotoData;
offset: number;
rect: { width: number; height: number };
unoptimized: boolean;
}): React.JSX.Element {
const {
on: { click },
carousel: { imageFit },
} = useLightboxProps();
const { currentIndex } = useLightboxState();
const cover = isImageSlide(slide) && isImageFitCover(slide, imageFit);
const width = !cover
? Math.round(
Math.min(rect.width, (rect.height / slide.height) * slide.width),
)
: rect.width;
const height = !cover
? Math.round(
Math.min(rect.height, (rect.width / slide.width) * slide.height),
)
: rect.height;
return (
<div style={{ position: "relative", width, height }}>
<Image
fill
alt=""
src={slide}
loading="eager"
unoptimized={unoptimized}
draggable={false}
blurDataURL={slide.blur}
placeholder="blur"
style={{
objectFit: cover ? "cover" : "contain",
cursor: click ? "pointer" : undefined,
}}
sizes={`${Math.ceil((width / window.innerWidth) * 100)}vw`}
onClick={
offset === 0
? (): void => click?.({ index: currentIndex })
: undefined
}
/>
</div>
);
}
export function Lightbox({
photoData: photoData,
photoData,
children,
}: {
photoData: PhotoData[];
@@ -88,17 +25,18 @@ export function Lightbox({
return (
<div className="mx-auto">
<div className="flex flex-row flex-wrap justify-center">
<div className="flex flex-row flex-wrap justify-center gap-8">
{children.map((image, index) => {
return (
<button
type="button"
key={"lightbox_img"}
key={`lightbox_${image.key}`}
className="cursor-pointer"
onClick={() => {
setActive(index);
}}
>
<div className="relative">{image}</div>
{image}
</button>
);
})}
@@ -110,9 +48,9 @@ export function Lightbox({
slides={photoData}
render={{
// @ts-expect-error - Todo
slide: (args) => NextJsImage({ ...args, unoptimized: true }),
slide: (args) => LightboxImage({ ...args, unoptimized: true }),
// @ts-expect-error - Todo - This just passes the slide through, but it doesn't know the type
thumbnail: NextJsImage,
thumbnail: LightboxImage,
}}
plugins={[Thumbnails, Zoom, Captions]}
/>
@@ -120,20 +58,10 @@ export function Lightbox({
);
}
interface FormElements extends HTMLFormControlsCollection {
src: HTMLInputElement;
}
interface UsernameFormElement extends HTMLFormElement {
readonly elements: FormElements;
}
// TODO
export default function FilteredLightbox(props: {
photoData: PhotoData[];
children: React.JSX.Element[];
}): React.JSX.Element {
//const [photoData, setImageData] = useState(props.photoData);
const [photoData] = useState(props.photoData);
const photoQuery = api.photos.list.useInfiniteQuery(
{
limit: 1,
@@ -152,62 +80,42 @@ export default function FilteredLightbox(props: {
},
);
const refreshQuery = api.photos.update.useQuery(undefined, {
enabled: false,
retry: false,
});
function handleSubmit(event: React.FormEvent<UsernameFormElement>): void {
event.preventDefault();
// const photoData = props.photoData;
// setImageData(
// photoData.filter(
// (data) => data.src === event.currentTarget.elements.src.value
// )
// );
void photoQuery.fetchNextPage();
function handleNextPage(): void {
if (!photoQuery.isLoading) {
void photoQuery.fetchNextPage();
}
}
const children = photoQuery.data.pages
.flatMap((data) => data.data)
.map((data) => (
<Image
key={data.src}
alt={data.src}
src={data.src}
className="h-60 w-80 object-contain"
sizes="100vw"
loading="lazy"
width={data.width}
height={data.height}
blurDataURL={data.blur}
placeholder="blur"
/>
))
.filter((data) => !!data);
const photoData = photoQuery.data.pages.flatMap((data) => data.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"
/>
));
refreshQuery.error ? console.log(refreshQuery.error) : null;
return (
<>
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="src">Src:</label>
<input id="src" type="text" />
</div>
<button type="submit">Submit</button>
</form>
<Lightbox photoData={photoData}>{...children}</Lightbox>
<button
type="button"
onClick={() => {
console.log("refetch");
void refreshQuery.refetch();
}}
className="btn btn-primary mx-auto p-4 mt-8 flex"
onClick={handleNextPage}
>
Refresh
{photoQuery.isLoading
? "Loading"
: photoQuery.hasNextPage
? "Load next page"
: "No more photos"}
</button>
{refreshQuery.data ? JSON.stringify(refreshQuery.data) : "No data"}
{refreshQuery.error ? JSON.stringify(refreshQuery.error) : "No Error"}
<Lightbox photoData={photoData}>{...children}</Lightbox>
</>
);
}

View File

@@ -17,7 +17,7 @@ export default function PostHeader({
return (
<>
<h1 className="mb-2">{metadata.title}</h1>
<div className="mb-4 text-primary-content/80">
<div className="mb-2">
<time dateTime={metadata.date}>{metadata.date}</time>
</div>
<div className="mb-2 flex gap-2">

View File

@@ -1,5 +1,5 @@
import { handlers } from "@/server/auth";
import { NextRequest } from "next/server";
import { handlers } from "@/server/auth";
const reqWithTrustedOrigin = (req: NextRequest): NextRequest => {
const proto = req.headers.get("x-forwarded-proto");

View File

@@ -0,0 +1,17 @@
import { eq } from "drizzle-orm";
import { db } from "@/server/db";
import { photos } from "@/server/db/schema";
export async function modify(mod: {
title: string;
src: string;
description: {
type: string;
content: unknown[];
};
}): Promise<void> {
await db
.update(photos)
.set({ title: mod.title, description: mod.description })
.where(eq(photos.src, mod.src));
}

View File

@@ -2,18 +2,19 @@ import { z } from "zod";
import {
createTRPCRouter,
protectedProcedure,
publicProcedure,
protectedProcedure,
} from "@/server/api/trpc";
import { list } from "./list";
import { update } from "./update";
import { modify } from "./modify";
export const photosRouter = createTRPCRouter({
list: publicProcedure
.input(
z
.object({
limit: z.number().nonnegative().default(2),
limit: z.number().nonnegative().default(1),
cursor: z.number().nonnegative().default(0),
})
.optional()
@@ -37,4 +38,21 @@ export const photosRouter = createTRPCRouter({
};
}),
update: publicProcedure.query(update),
modify: protectedProcedure
.input(
z.object({
title: z
.string()
.min(3, "Title should be over 3 characters")
.max(128, "Title cannot be over 128 characters"),
src: z.string(),
description: z.object({
type: z.string(),
content: z.array(z.unknown()),
}),
}),
)
.mutation(async ({ input }) => {
await modify(input);
}),
});

View File

@@ -10,7 +10,7 @@ export const photos = sqliteTable("photo", (d) => ({
camera: d.text({ length: 128 }),
title: d.text({ length: 128 }),
description: d.text({ length: 1024 }),
description: d.blob({ mode: "json" }),
exposureBiasValue: d.integer({ mode: "number" }),
fNumber: d.real(),
isoSpeedRatings: d.integer({ mode: "number" }),

View File

@@ -1,56 +1,56 @@
/** biome-ignore-all lint/correctness/noUnknownProperty: Biome doesn't understand DaisyUI properties */
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@plugin "daisyui";
@plugin "daisyui/theme" {
name: "alucard";
default: true;
prefersdark: false;
color-scheme: "light";
name: "alucard";
default: true;
prefersdark: false;
color-scheme: "light";
--color-base-100: oklch(97.02% 0.000 0);
--color-base-200: oklch(95.20% 0.007 268.55);
--color-base-300: oklch(88.75% 0.015 264.49);
--color-base-content: oklch(23.93% 0.000 0);
--color-base-100: oklch(97.02% 0 0);
--color-base-200: oklch(95.20% 0.007 268.55);
--color-base-300: oklch(88.75% 0.015 264.49);
--color-base-content: oklch(23.93% 0 0);
--color-primary: oklch(64.21% 0.086 228.32);
--color-primary-content: oklch(100% 0 0);
--color-secondary: oklch(67.53% 0.129 27.41);
--color-secondary-content: oklch(23.93% 0.000 0);
--color-accent: oklch(50.93% 0.091 287.46);
--color-accent-content: oklch(100% 0 0);
--color-neutral: oklch(45.68% 0.000 0);
--color-neutral-content: oklch(100% 0 0);
--color-primary: oklch(64.21% 0.086 228.32);
--color-primary-content: oklch(100% 0 0);
--color-secondary: oklch(67.53% 0.129 27.41);
--color-secondary-content: oklch(23.93% 0 0);
--color-accent: oklch(50.93% 0.091 287.46);
--color-accent-content: oklch(100% 0 0);
--color-neutral: oklch(45.68% 0 0);
--color-neutral-content: oklch(100% 0 0);
--color-info: oklch(49.47% 0.122 243.83);
--color-info-content: oklch(100% 0 0);
--color-success: oklch(63.12% 0.124 141.91);
--color-success-content: oklch(0% 0 0);
--color-warning: oklch(76.96% 0.156 99.76);
--color-warning-content: oklch(0% 0 0);
--color-error: oklch(52.56% 0.199 5.45);
--color-error-content: oklch(100% 0 0);
--color-info: oklch(49.47% 0.122 243.83);
--color-info-content: oklch(100% 0 0);
--color-success: oklch(63.12% 0.124 141.91);
--color-success-content: oklch(0% 0 0);
--color-warning: oklch(76.96% 0.156 99.76);
--color-warning-content: oklch(0% 0 0);
--color-error: oklch(52.56% 0.199 5.45);
--color-error-content: oklch(100% 0 0);
--radius-selector: 1rem;
--radius-field: 0.5rem;
--radius-box: 1rem;
--radius-selector: 1rem;
--radius-field: 0.5rem;
--radius-box: 1rem;
--size-selector: 0.25rem;
--size-field: 0.25rem;
--border: 1px;
--size-selector: 0.25rem;
--size-field: 0.25rem;
--border: 1px;
--depth: 1;
--noise: 0;
--depth: 1;
--noise: 0;
}
@plugin "daisyui/theme" {
/* Nicked from the vscode soft theme https://github.com/dracula/visual-studio-code/blob/master/src/dracula.yml */
name: "dracula-soft";
default: false;
prefersdark: false;
prefersdark: true;
color-scheme: "dark";
/* --color-base-50: oklch(34.02% 0.027 276.05); */

View File

@@ -6,12 +6,10 @@ import { createTRPCReact } from "@trpc/react-query";
import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server";
import { useState } from "react";
import SuperJSON from "superjson";
import type { AppRouter } from "@/server/api/root";
import { createQueryClient } from "./query-client";
import { env } from "@/env";
let clientQueryClientSingleton: QueryClient | undefined = undefined;
let clientQueryClientSingleton: QueryClient | undefined;
const getQueryClient = () => {
if (typeof window === "undefined") {
// Server: always make a new query client