Lint and TRPC
This commit is contained in:
2383
package-lock.json
generated
2383
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
56
package.json
56
package.json
@@ -11,43 +11,51 @@
|
||||
"lint:fix": "next lint --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.693.0",
|
||||
"@heroicons/react": "^2.1.5",
|
||||
"@mdx-js/loader": "^3.0.1",
|
||||
"@mdx-js/react": "^3.0.1",
|
||||
"@next/bundle-analyzer": "^15.0.3",
|
||||
"@next/mdx": "^15.0.3",
|
||||
"@aws-sdk/client-s3": "^3.712.0",
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"@mdx-js/loader": "^3.1.0",
|
||||
"@mdx-js/react": "^3.1.0",
|
||||
"@next/bundle-analyzer": "^15.1.0",
|
||||
"@next/mdx": "^15.1.0",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@types/better-sqlite3": "^7.6.11",
|
||||
"@tanstack/react-query": "^5.62.7",
|
||||
"@tanstack/react-virtual": "^3.11.1",
|
||||
"@trpc/client": "^11.0.0-rc.660",
|
||||
"@trpc/react-query": "^11.0.0-rc.660",
|
||||
"@trpc/server": "^11.0.0-rc.660",
|
||||
"@types/better-sqlite3": "^7.6.12",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "^22.6.1",
|
||||
"@types/react": "^18.3.9",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.14.0",
|
||||
"@types/node": "^22.10.2",
|
||||
"@types/react": "^19.0.1",
|
||||
"@types/react-dom": "^19.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^8.18.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"babel-plugin-react-compiler": "^19.0.0-beta-a7bf2bd-20241110",
|
||||
"better-sqlite3": "^11.5.0",
|
||||
"drizzle-kit": "^0.28.1",
|
||||
"drizzle-orm": "^0.36.3",
|
||||
"eslint": "^9.15.0",
|
||||
"eslint-config-next": "^15.0.4-canary.15",
|
||||
"babel-plugin-react-compiler": "beta",
|
||||
"better-sqlite3": "^11.7.0",
|
||||
"client-only": "^0.0.1",
|
||||
"drizzle-kit": "^0.30.1",
|
||||
"drizzle-orm": "^0.38.2",
|
||||
"eslint": "^9.17.0",
|
||||
"eslint-config-next": "^15.1.0",
|
||||
"exif-reader": "^2.0.1",
|
||||
"framer-motion": "^11.11.17",
|
||||
"framer-motion": "^11.14.4",
|
||||
"glob": "^11.0.0",
|
||||
"million": "^3.1.11",
|
||||
"next": "15.0.4-canary.15",
|
||||
"next": "15.1.1-canary.5",
|
||||
"next-auth": "5.0.0-beta.25",
|
||||
"postcss": "^8.4.49",
|
||||
"radash": "^12.1.0",
|
||||
"react": "19.0.0-rc-e1ef8c95-20241115",
|
||||
"react-dom": "19.0.0-rc-e1ef8c95-20241115",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react-zoom-pan-pinch": "^3.6.1",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"server-only": "^0.0.1",
|
||||
"sharp": "^0.33.5",
|
||||
"superjson": "^2.2.2",
|
||||
"tailwind-scrollbar": "^3.1.0",
|
||||
"tailwindcss": "^3.4.15",
|
||||
"typescript": "^5.6.2",
|
||||
"yet-another-react-lightbox": "^3.21.7"
|
||||
"tailwindcss": "^3.4.16",
|
||||
"typescript": "^5.7.2",
|
||||
"yet-another-react-lightbox": "^3.21.7",
|
||||
"zod": "^3.24.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
import Image from "next/image";
|
||||
import FilteredLightbox from "@/components/lightbox";
|
||||
import { type GetPhotos } from "@/app/api/photos/route";
|
||||
|
||||
async function getImageData(): Promise<GetPhotos> {
|
||||
const res = await fetch(`http://localhost:3000/api/photos`, { next: { revalidate: false, tags: ['photos'] } });
|
||||
return res.json() as Promise<GetPhotos>;
|
||||
}
|
||||
import { trpc } from "@/trpc/server";
|
||||
import { TRPCProvider } from "@/trpc/client";
|
||||
|
||||
export default async function Photos(): Promise<React.JSX.Element> {
|
||||
const {data: imageData} = await getImageData();
|
||||
const { data: images } = await trpc.photos.list();
|
||||
|
||||
return (
|
||||
<div className="mx-auto">
|
||||
<FilteredLightbox imageData={imageData.images}>
|
||||
{imageData.images.map((image) => (
|
||||
<TRPCProvider>
|
||||
<FilteredLightbox imageData={images}>
|
||||
{images.map((image) => (
|
||||
<Image
|
||||
key={image.src}
|
||||
alt={image.src}
|
||||
@@ -28,6 +25,7 @@ export default async function Photos(): Promise<React.JSX.Element> {
|
||||
/>
|
||||
))}
|
||||
</FilteredLightbox>
|
||||
</TRPCProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,24 +1,32 @@
|
||||
import { glob } from "glob";
|
||||
import dynamic from "next/dynamic";
|
||||
import dynamic, { LoaderComponent } from "next/dynamic";
|
||||
import React from "react";
|
||||
|
||||
export const dynamicParams = false;
|
||||
|
||||
export async function generateStaticParams(): Promise<{slug: string[]}[]> {
|
||||
const posts = await glob(`${process.cwd()}/src/markdown/posts/[[]...slug[]]/**/*.mdx`, {
|
||||
export async function generateStaticParams(): Promise<{ slug: string[] }[]> {
|
||||
const posts = await glob(
|
||||
`${process.cwd()}/src/markdown/posts/[[]...slug[]]/**/*.mdx`,
|
||||
{
|
||||
nodir: true,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const slugs = posts.map((post) => ({
|
||||
slug: [post.split('/').at(-1)!.slice(0, -4)]
|
||||
slug: [post.split("/").at(-1)!.slice(0, -4)],
|
||||
}));
|
||||
|
||||
return slugs;
|
||||
}
|
||||
|
||||
export default async function Post({params}: {params: Promise<{ slug: string[] }>}): Promise<React.JSX.Element> {
|
||||
const mdxFile = await import(`../../../../markdown/posts/[...slug]/${(await params).slug.join('/')}.mdx`);
|
||||
export default async function Post({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ slug: string[] }>;
|
||||
}): Promise<React.JSX.Element> {
|
||||
const mdxFile = await import(
|
||||
`../../../../markdown/posts/[...slug]/${(await params).slug.join("/")}.mdx`
|
||||
) as LoaderComponent<unknown>;
|
||||
const Post = dynamic(() => mdxFile);
|
||||
return (
|
||||
<Post/>
|
||||
);
|
||||
return <Post />;
|
||||
}
|
||||
|
||||
@@ -4,27 +4,32 @@ import { unstable_cache } from "next/cache";
|
||||
import Link from "next/link";
|
||||
|
||||
type postDetails = {
|
||||
link: string,
|
||||
link: string;
|
||||
metadata: {
|
||||
title: string,
|
||||
date: string,
|
||||
coverImage: string,
|
||||
blurb: string,
|
||||
shortBlurb: string,
|
||||
tags: string[]
|
||||
}
|
||||
}
|
||||
title: string;
|
||||
date: string;
|
||||
coverImage: string;
|
||||
blurb: string;
|
||||
shortBlurb: string;
|
||||
tags: string[];
|
||||
};
|
||||
};
|
||||
|
||||
async function loadPostDetails(): Promise<postDetails[]> {
|
||||
const posts = await glob(`${process.cwd()}/src/markdown/posts/[[]...slug[]]/**/*.mdx`, {
|
||||
const posts = await glob(
|
||||
`${process.cwd()}/src/markdown/posts/[[]...slug[]]/**/*.mdx`,
|
||||
{
|
||||
nodir: true,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const loadPostData = posts.map(async (post) => {
|
||||
const slug = [post.split('/').at(-1)!.slice(0, -4)];
|
||||
const mdxFile = await import(`../../../../src/markdown/posts/[...slug]/${slug.join('/')}.mdx`);
|
||||
const slug = [post.split("/").at(-1)!.slice(0, -4)];
|
||||
const mdxFile = await import(
|
||||
`../../../../src/markdown/posts/[...slug]/${slug.join("/")}.mdx`
|
||||
) as postDetails;
|
||||
return {
|
||||
link: getCurrentUrl() + '/posts/' + slug.join('/'),
|
||||
link: getCurrentUrl() + "/posts/" + slug.join("/"),
|
||||
metadata: mdxFile.metadata,
|
||||
};
|
||||
});
|
||||
@@ -33,13 +38,9 @@ async function loadPostDetails(): Promise<postDetails[]> {
|
||||
return postData;
|
||||
}
|
||||
|
||||
const getPosts = unstable_cache(
|
||||
loadPostDetails,
|
||||
['posts'],
|
||||
{
|
||||
revalidate: false
|
||||
}
|
||||
);
|
||||
const getPosts = unstable_cache(loadPostDetails, ["posts"], {
|
||||
revalidate: false,
|
||||
});
|
||||
|
||||
export default async function Posts(): Promise<React.JSX.Element> {
|
||||
const postDetails = await getPosts();
|
||||
@@ -56,14 +57,14 @@ export default async function Posts(): Promise<React.JSX.Element> {
|
||||
{post.metadata.tags.map((tag) => {
|
||||
return (
|
||||
<div key={`${post.link}_${tag}`}>
|
||||
<span className="select-none text-sm me-2 px-2.5 py-1 rounded border border-dracula-pink dark:bg-dracula-bg-darker dark:text-dracula-pink">{tag}</span>
|
||||
<span className="select-none text-sm me-2 px-2.5 py-1 rounded border border-dracula-pink dark:bg-dracula-bg-darker dark:text-dracula-pink">
|
||||
{tag}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<p>
|
||||
{post.metadata.blurb}
|
||||
</p>
|
||||
<p>{post.metadata.blurb}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
11
src/app/api/trpc/[trpc]/route.ts
Normal file
11
src/app/api/trpc/[trpc]/route.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
|
||||
import { createTRPCContext } from '@/trpc/init';
|
||||
import { appRouter } from '@/trpc/routers/_app';
|
||||
const handler = (req: Request): Promise<Response> =>
|
||||
fetchRequestHandler({
|
||||
endpoint: '/api/trpc',
|
||||
req,
|
||||
router: appRouter,
|
||||
createContext: createTRPCContext,
|
||||
});
|
||||
export { handler as GET, handler as POST };
|
||||
@@ -15,9 +15,22 @@ 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 ImageData } from "@/app/api/photos/route";
|
||||
import type { RouterOutput } from "@/trpc/routers/_app";
|
||||
import { trpc } from "@/trpc/client";
|
||||
|
||||
function NextJsImage({ slide, offset, rect, unoptimized = false }: {slide: ImageData, offset: number, rect: {width: number, height: number}, unoptimized: boolean}): React.JSX.Element {
|
||||
type ImageData = RouterOutput["photos"]["list"]["data"][number];
|
||||
|
||||
function NextJsImage({
|
||||
slide,
|
||||
offset,
|
||||
rect,
|
||||
unoptimized = false,
|
||||
}: {
|
||||
slide: ImageData;
|
||||
offset: number;
|
||||
rect: { width: number; height: number };
|
||||
unoptimized: boolean;
|
||||
}): React.JSX.Element {
|
||||
const {
|
||||
on: { click },
|
||||
carousel: { imageFit },
|
||||
@@ -29,13 +42,13 @@ function NextJsImage({ slide, offset, rect, unoptimized = false }: {slide: Image
|
||||
|
||||
const width = !cover
|
||||
? Math.round(
|
||||
Math.min(rect.width, (rect.height / slide.height) * slide.width),
|
||||
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),
|
||||
Math.min(rect.height, (rect.width / slide.width) * slide.height)
|
||||
)
|
||||
: rect.height;
|
||||
|
||||
@@ -56,14 +69,22 @@ function NextJsImage({ slide, offset, rect, unoptimized = false }: {slide: Image
|
||||
}}
|
||||
sizes={`${Math.ceil((width / window.innerWidth) * 100)}vw`}
|
||||
onClick={
|
||||
offset === 0 ? (): void => click?.({ index: currentIndex }) : undefined
|
||||
offset === 0
|
||||
? (): void => click?.({ index: currentIndex })
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Lightbox({imageData, children}: {imageData: ImageData[], children: React.JSX.Element[]}): React.JSX.Element {
|
||||
export function Lightbox({
|
||||
imageData,
|
||||
children,
|
||||
}: {
|
||||
imageData: ImageData[];
|
||||
children: React.JSX.Element[];
|
||||
}): React.JSX.Element {
|
||||
const [active, setActive] = useState<number | null>(null);
|
||||
|
||||
return (
|
||||
@@ -71,49 +92,98 @@ export function Lightbox({imageData, children}: {imageData: ImageData[], childre
|
||||
<div className="flex flex-row flex-wrap justify-center">
|
||||
{children.map((image, index) => {
|
||||
return (
|
||||
<button key={`lightbox_img_${index}`} onClick={(() => {
|
||||
<button
|
||||
key={`lightbox_img_${index}`}
|
||||
onClick={() => {
|
||||
setActive(index);
|
||||
})}>
|
||||
<div className="relative">
|
||||
{image}
|
||||
</div>
|
||||
}}
|
||||
>
|
||||
<div className="relative">{image}</div>
|
||||
</button>
|
||||
); }
|
||||
)}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<YARL
|
||||
open={typeof active === 'number'}
|
||||
open={typeof active === "number"}
|
||||
close={() => setActive(null)}
|
||||
index={active ?? undefined}
|
||||
slides={imageData}
|
||||
render={{
|
||||
// @ts-expect-error - Todo
|
||||
slide: (args) => NextJsImage({ ...args, unoptimized: true }),
|
||||
// @ts-expect-error - Todo - This just passes the slide through, but it doesn't know the type
|
||||
render={{ slide: (args) => NextJsImage({...args, unoptimized: true }), thumbnail: NextJsImage }}
|
||||
thumbnail: NextJsImage,
|
||||
}}
|
||||
plugins={[Thumbnails, Zoom, Captions]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
interface FormElements extends HTMLFormControlsCollection {
|
||||
src: HTMLInputElement
|
||||
src: HTMLInputElement;
|
||||
}
|
||||
interface UsernameFormElement extends HTMLFormElement {
|
||||
readonly elements: FormElements
|
||||
readonly elements: FormElements;
|
||||
}
|
||||
|
||||
export default function FilteredLightbox(props: {imageData: ImageData[], children: React.JSX.Element[]}): React.JSX.Element {
|
||||
const [imageData, setImageData] = useState(props.imageData);
|
||||
export default function FilteredLightbox(props: {
|
||||
imageData: ImageData[];
|
||||
children: React.JSX.Element[];
|
||||
}): React.JSX.Element {
|
||||
//const [imageData, setImageData] = useState(props.imageData);
|
||||
const [imageData] = useState(props.imageData);
|
||||
const photoQuery = trpc.photos.list.useInfiniteQuery(
|
||||
{
|
||||
limit: 1,
|
||||
},
|
||||
{
|
||||
initialData: {
|
||||
pages: [
|
||||
{
|
||||
data: props.imageData,
|
||||
next: props.imageData.length,
|
||||
},
|
||||
],
|
||||
pageParams: [0],
|
||||
},
|
||||
getNextPageParam: (lastPage) => lastPage.next,
|
||||
}
|
||||
);
|
||||
|
||||
const refreshQuery = trpc.photos.update.useQuery(undefined, {
|
||||
enabled: false,
|
||||
retry: false,
|
||||
});
|
||||
|
||||
function handleSubmit(event: React.FormEvent<UsernameFormElement>): void {
|
||||
event.preventDefault();
|
||||
const imageData = props.imageData;
|
||||
setImageData(imageData.filter((data) => data.src === event.currentTarget.elements.src.value));
|
||||
// const imageData = props.imageData;
|
||||
// setImageData(
|
||||
// imageData.filter(
|
||||
// (data) => data.src === event.currentTarget.elements.src.value
|
||||
// )
|
||||
// );
|
||||
void photoQuery.fetchNextPage();
|
||||
}
|
||||
|
||||
const children = imageData.map((data) => props.children.find((child) => {
|
||||
return data.src === child.key ? child : null;
|
||||
})).filter(((data) => !!data));
|
||||
const children = photoQuery.data.pages
|
||||
.flatMap((data) => data.data)
|
||||
.map((data) => (
|
||||
<Image
|
||||
key={data.src}
|
||||
alt={data.src}
|
||||
src={data.src}
|
||||
className="object-contain h-60 w-80"
|
||||
sizes="100vw"
|
||||
loading="lazy"
|
||||
width={data.width}
|
||||
height={data.height}
|
||||
blurDataURL={data.blur}
|
||||
placeholder="blur"
|
||||
/>
|
||||
))
|
||||
.filter((data) => !!data);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -124,9 +194,16 @@ export default function FilteredLightbox(props: {imageData: ImageData[], childre
|
||||
</div>
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
<Lightbox imageData={imageData}>
|
||||
{...children}
|
||||
</Lightbox>
|
||||
<button
|
||||
onClick={() => {
|
||||
void refreshQuery.refetch();
|
||||
}}
|
||||
>
|
||||
Refresh
|
||||
</button>
|
||||
{refreshQuery.data ? JSON.stringify(refreshQuery.data) : "\nNot"}
|
||||
{refreshQuery.error ? JSON.stringify(refreshQuery.error) : "\nNo Error"}
|
||||
<Lightbox imageData={imageData}>{...children}</Lightbox>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,21 +1,33 @@
|
||||
'use client';
|
||||
import { useMemo, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { HomeModernIcon, Bars3Icon, XMarkIcon } from '@heroicons/react/24/outline';
|
||||
import { AnimatePresence, m, LazyMotion, domAnimation } from "framer-motion";
|
||||
import { usePathname } from 'next/navigation';
|
||||
import ThemeSwitcher from './theme-switcher';
|
||||
"use client";
|
||||
import { useMemo, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
HomeModernIcon,
|
||||
Bars3Icon,
|
||||
XMarkIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import {
|
||||
AnimatePresence,
|
||||
motion,
|
||||
LazyMotion,
|
||||
domAnimation,
|
||||
} from "framer-motion";
|
||||
import { usePathname } from "next/navigation";
|
||||
import ThemeSwitcher from "./theme-switcher";
|
||||
|
||||
type NavBarClientProps = {
|
||||
LogIn: React.JSX.Element,
|
||||
LogIn: React.JSX.Element;
|
||||
navigation: {
|
||||
name: string;
|
||||
href: string;
|
||||
current: boolean;
|
||||
}[]
|
||||
}
|
||||
}[];
|
||||
};
|
||||
|
||||
export default function NavBarClient({LogIn, navigation}: NavBarClientProps): React.JSX.Element {
|
||||
export default function NavBarClient({
|
||||
LogIn,
|
||||
navigation,
|
||||
}: NavBarClientProps): React.JSX.Element {
|
||||
const [open, setOpen] = useState(false);
|
||||
const pathname = usePathname();
|
||||
|
||||
@@ -35,61 +47,67 @@ export default function NavBarClient({LogIn, navigation}: NavBarClientProps): Re
|
||||
<div className="mx-auto max-w-7xl px-4">
|
||||
<div className="relative flex h-16 items-center justify-between">
|
||||
<div className="flex">
|
||||
<button className='sm:hidden dark:hover:bg-dracula-bg-light transition-colors duration-100 rounded-sm p-1' onClick={() => setOpen(!open)}>
|
||||
<button
|
||||
className="sm:hidden dark:hover:bg-dracula-bg-light transition-colors duration-100 rounded-sm p-1"
|
||||
onClick={() => setOpen(!open)}
|
||||
>
|
||||
{open ? (
|
||||
<XMarkIcon className='rounded-sm dark:stroke-dracula-cyan h-8 w-auto'/>
|
||||
<XMarkIcon className="rounded-sm dark:stroke-dracula-cyan h-8 w-auto" />
|
||||
) : (
|
||||
<Bars3Icon className='rounded-sm dark:stroke-dracula-cyan h-8 w-auto'/>
|
||||
<Bars3Icon className="rounded-sm dark:stroke-dracula-cyan h-8 w-auto" />
|
||||
)}
|
||||
</button>
|
||||
<Link className='hidden sm:flex items-center p-1 dark:hover:bg-dracula-bg-light transition-colors' href='/'>
|
||||
<HomeModernIcon className='dark:stroke-dracula-cyan rounded-sm h-8 w-auto'/>
|
||||
<Link
|
||||
className="hidden sm:flex items-center p-1 dark:hover:bg-dracula-bg-light transition-colors"
|
||||
href="/"
|
||||
>
|
||||
<HomeModernIcon className="dark:stroke-dracula-cyan rounded-sm h-8 w-auto" />
|
||||
</Link>
|
||||
<div className='space-x-5 hidden sm:flex ml-10'>
|
||||
<div className="space-x-5 hidden sm:flex ml-10">
|
||||
{activeNavigation.map((item) => (
|
||||
<Link
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
className={`dark:hover:bg-dracula-bg-light transition-colors duration-100 dark:text-white rounded-sm px-3 pt-2 pb-1.5 font-normal border-b-2 border-transparent ${
|
||||
item.current ? 'dark:border-b-dracula-pink' : ''
|
||||
item.current ? "dark:border-b-dracula-pink" : ""
|
||||
}`}
|
||||
aria-current={item.current ? 'page' : undefined}
|
||||
aria-current={item.current ? "page" : undefined}
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className='space-x-4 flex'>
|
||||
<ThemeSwitcher/>
|
||||
<div className="space-x-4 flex">
|
||||
<ThemeSwitcher />
|
||||
{LogIn}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<AnimatePresence>
|
||||
{ open ? (
|
||||
<m.div
|
||||
{open ? (
|
||||
<motion.div
|
||||
initial={{ height: 0 }}
|
||||
animate={{ height: "auto" }}
|
||||
transition={{ duration: 0.15, ease: 'linear' }}
|
||||
transition={{ duration: 0.15, ease: "linear" }}
|
||||
exit={{ height: 0 }}
|
||||
className='sm:hidden overflow-hidden'
|
||||
className="sm:hidden overflow-hidden"
|
||||
>
|
||||
<div className='flex flex-col space-y-1 py-1'>
|
||||
<div className="flex flex-col space-y-1 py-1">
|
||||
{activeNavigation.map((item) => (
|
||||
<Link
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
className={`dark:hover:bg-dracula-bg-light transition-colors duration-100 dark:text-white px-2 py-2 font-normal border-l-4 border-transparent ${
|
||||
item.current ? 'dark:border-l-dracula-pink' : ''
|
||||
item.current ? "dark:border-l-dracula-pink" : ""
|
||||
}`}
|
||||
aria-current={item.current ? 'page' : undefined}
|
||||
aria-current={item.current ? "page" : undefined}
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</m.div>
|
||||
</motion.div>
|
||||
) : null}
|
||||
</AnimatePresence>
|
||||
</LazyMotion>
|
||||
|
||||
60
src/trpc/client.tsx
Normal file
60
src/trpc/client.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import superjson from "superjson";
|
||||
import { httpBatchLink } from "@trpc/client";
|
||||
import { createTRPCReact } from "@trpc/react-query";
|
||||
import { getCurrentUrl } from "@/lib/current-url";
|
||||
import { makeQueryClient } from "./query-client";
|
||||
|
||||
import { QueryClientProvider, type QueryClient } from "@tanstack/react-query";
|
||||
import type { appRouter } from "./routers/_app";
|
||||
|
||||
export const trpc = createTRPCReact<typeof appRouter>();
|
||||
|
||||
let clientQueryClientSingleton: QueryClient;
|
||||
|
||||
function getQueryClient(): QueryClient {
|
||||
if (typeof window === "undefined") {
|
||||
// Server: always make a new query client
|
||||
return makeQueryClient();
|
||||
}
|
||||
// Browser: use singleton pattern to keep the same query client
|
||||
return (clientQueryClientSingleton ??= makeQueryClient());
|
||||
}
|
||||
function getUrl(): string {
|
||||
const base = ((): string => {
|
||||
if (typeof window !== "undefined") return "";
|
||||
return getCurrentUrl();
|
||||
})();
|
||||
return `${base}/api/trpc`;
|
||||
}
|
||||
export function TRPCProvider(
|
||||
props: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>
|
||||
): React.JSX.Element {
|
||||
// NOTE: Avoid useState when initializing the query client if you don't
|
||||
// have a suspense boundary between this and the code that may
|
||||
// suspend because React will throw away the client on the initial
|
||||
// render if it suspends and there is no boundary
|
||||
const queryClient = getQueryClient();
|
||||
const [trpcClient] = useState(() =>
|
||||
trpc.createClient({
|
||||
links: [
|
||||
httpBatchLink({
|
||||
transformer: superjson,
|
||||
url: getUrl(),
|
||||
}),
|
||||
]
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<trpc.Provider client={trpcClient} queryClient={queryClient}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{props.children}
|
||||
</QueryClientProvider>
|
||||
</trpc.Provider>
|
||||
);
|
||||
}
|
||||
49
src/trpc/init.ts
Normal file
49
src/trpc/init.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { cache } from 'react';
|
||||
import superjson from 'superjson';
|
||||
import { initTRPC, TRPCError } from '@trpc/server';
|
||||
import { auth } from '@/lib/auth';
|
||||
|
||||
interface Context {
|
||||
user?: {
|
||||
id?: string
|
||||
name?: string | null
|
||||
email?: string | null
|
||||
image?: string | null
|
||||
};
|
||||
}
|
||||
|
||||
export const createTRPCContext = cache(async (): Promise<Context> => {
|
||||
/**
|
||||
* @see: https://trpc.io/docs/server/context
|
||||
*/
|
||||
const session = await auth();
|
||||
return { user: session?.user };
|
||||
});
|
||||
|
||||
// Avoid exporting the entire t-object
|
||||
// since it's not very descriptive.
|
||||
// For instance, the use of a t variable
|
||||
// is common in i18n libraries.
|
||||
const t = initTRPC.context<Context>().create({
|
||||
/**
|
||||
* @see https://trpc.io/docs/server/data-transformers
|
||||
*/
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
const authMiddleware = t.middleware(({ ctx, next }) => {
|
||||
if (ctx.user?.name !== 'Joe') {
|
||||
throw new TRPCError({ code: 'UNAUTHORIZED' });
|
||||
}
|
||||
return next({
|
||||
ctx: {
|
||||
user: ctx.user,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// Base router and procedure helpers
|
||||
export const createTRPCRouter = t.router;
|
||||
export const createCallerFactory = t.createCallerFactory;
|
||||
export const publicProcedure = t.procedure;
|
||||
export const privateProcedure = t.procedure.use(authMiddleware);
|
||||
27
src/trpc/query-client.ts
Normal file
27
src/trpc/query-client.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import {
|
||||
defaultShouldDehydrateQuery,
|
||||
QueryClient,
|
||||
} from '@tanstack/react-query';
|
||||
import { serialize, deserialize } from 'superjson';
|
||||
|
||||
export function makeQueryClient(): QueryClient {
|
||||
return new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
staleTime: 30 * 1000,
|
||||
},
|
||||
dehydrate: {
|
||||
serializeData: serialize,
|
||||
shouldDehydrateQuery: (query) =>
|
||||
defaultShouldDehydrateQuery(query) ||
|
||||
query.state.status === 'pending',
|
||||
},
|
||||
hydrate: {
|
||||
deserializeData: deserialize,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
11
src/trpc/routers/_app.ts
Normal file
11
src/trpc/routers/_app.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
// eslint-disable-next-line import/named
|
||||
import { inferRouterInputs, inferRouterOutputs } from '@trpc/server';
|
||||
import { createTRPCRouter } from '../init';
|
||||
import { photosRouter } from './photos';
|
||||
|
||||
export const appRouter = createTRPCRouter({
|
||||
photos: photosRouter,
|
||||
});
|
||||
|
||||
export type RouterInput = inferRouterInputs<typeof appRouter>;
|
||||
export type RouterOutput = inferRouterOutputs<typeof appRouter>;
|
||||
35
src/trpc/routers/photos.ts
Normal file
35
src/trpc/routers/photos.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { z } from 'zod';
|
||||
import { createTRPCRouter, privateProcedure, publicProcedure } from '../init';
|
||||
|
||||
import { list } from './photos/list';
|
||||
import { update } from './photos/update';
|
||||
|
||||
export const photosRouter = createTRPCRouter({
|
||||
list: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
limit: z.number().nonnegative().default(1),
|
||||
cursor: z.number().nonnegative().default(0),
|
||||
})
|
||||
.optional()
|
||||
.default({}),
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
const ret = await list({
|
||||
limit: input.limit + 1,
|
||||
cursor: input.cursor,
|
||||
});
|
||||
|
||||
let next;
|
||||
if (ret.length > input.limit) {
|
||||
next = input.limit;
|
||||
ret.pop();
|
||||
}
|
||||
|
||||
return {
|
||||
data: ret,
|
||||
next
|
||||
};
|
||||
}),
|
||||
update: privateProcedure.query(update)
|
||||
});
|
||||
@@ -1,4 +1,3 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { shake } from "radash";
|
||||
import db from "@/db/db";
|
||||
import { photosTable } from "@/db/schema/photo";
|
||||
@@ -21,15 +20,16 @@ export type ImageData = {
|
||||
description?: string
|
||||
}
|
||||
|
||||
export type GetPhotos = {
|
||||
status: number,
|
||||
data: {
|
||||
images: ImageData[]
|
||||
}
|
||||
export type ListOptions = {
|
||||
cursor: number,
|
||||
limit: number
|
||||
}
|
||||
|
||||
export async function GET(): Promise<Response> {
|
||||
const currentSources = await db.select().from(photosTable);
|
||||
export async function list(options: ListOptions): Promise<ImageData[]> {
|
||||
const currentSources = await db.select().from(photosTable)
|
||||
.limit(options.limit)
|
||||
.offset(options.cursor);
|
||||
|
||||
const images = currentSources.map((photo) => {
|
||||
return {
|
||||
width: photo.width,
|
||||
@@ -50,5 +50,5 @@ export async function GET(): Promise<Response> {
|
||||
};
|
||||
});
|
||||
|
||||
return NextResponse.json<GetPhotos>({ status: 200, data: { images } });
|
||||
return images;
|
||||
}
|
||||
@@ -1,22 +1,13 @@
|
||||
import { S3Client, ListObjectsV2Command, GetObjectCommand } from "@aws-sdk/client-s3";
|
||||
import exifReader from "exif-reader";
|
||||
import { NextResponse } from "next/server";
|
||||
import { diff, sift } from "radash";
|
||||
import sharp from "sharp";
|
||||
|
||||
import db from "@/db/db";
|
||||
import { photosTable } from "@/db/schema/photo";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
export type GetPhotosUpdate = {
|
||||
status: number,
|
||||
s3Photos: string[]
|
||||
}
|
||||
|
||||
export const GET = auth(async function GET(req): Promise<Response> {
|
||||
if (!req.auth) {
|
||||
return NextResponse.json({ message: "Not authenticated" }, { status: 401 });
|
||||
}
|
||||
export async function update(): Promise<string[]> {
|
||||
const photos = await db.select().from(photosTable);
|
||||
const currentSources = photos.map((photo) => photo.src);
|
||||
|
||||
@@ -32,7 +23,10 @@ export const GET = auth(async function GET(req): Promise<Response> {
|
||||
const s3Res = await s3Client.send(listObjCmd);
|
||||
|
||||
if (!s3Res.Contents) {
|
||||
return NextResponse.json({ status: 500 });
|
||||
throw new TRPCError({
|
||||
code: "GATEWAY_TIMEOUT",
|
||||
message: "Could not get contents from Tigris"
|
||||
});
|
||||
}
|
||||
const s3Photos = sift(s3Res.Contents.map((obj) => {
|
||||
if (!obj.Key?.endsWith('/')) {
|
||||
@@ -45,7 +39,7 @@ export const GET = auth(async function GET(req): Promise<Response> {
|
||||
const newPhotos = diff(s3Photos, currentSources);
|
||||
|
||||
if (newPhotos.length === 0) {
|
||||
return NextResponse.json<GetPhotosUpdate>({ status: 200, s3Photos: newPhotos });
|
||||
return [];
|
||||
}
|
||||
|
||||
const imageData = newPhotos.map(async (fileName: string) => {
|
||||
@@ -84,5 +78,5 @@ export const GET = auth(async function GET(req): Promise<Response> {
|
||||
|
||||
await db.insert(photosTable).values(images);
|
||||
|
||||
return NextResponse.json<GetPhotosUpdate>({ status: 200, s3Photos: newPhotos });
|
||||
});
|
||||
return newPhotos;
|
||||
};
|
||||
14
src/trpc/server.ts
Normal file
14
src/trpc/server.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import 'server-only'; // <-- ensure this file cannot be imported from the client
|
||||
import { createHydrationHelpers } from '@trpc/react-query/rsc';
|
||||
import { cache } from 'react';
|
||||
import { createCallerFactory, createTRPCContext } from './init';
|
||||
import { makeQueryClient } from './query-client';
|
||||
import { appRouter } from './routers/_app';
|
||||
// IMPORTANT: Create a stable getter for the query client that
|
||||
// will return the same client during the same request.
|
||||
export const getQueryClient = cache(makeQueryClient);
|
||||
const caller = createCallerFactory(appRouter)(createTRPCContext);
|
||||
export const { trpc, HydrateClient } = createHydrationHelpers<typeof appRouter>(
|
||||
caller,
|
||||
getQueryClient,
|
||||
);
|
||||
Reference in New Issue
Block a user