Commiting to switch to a different orm

This commit is contained in:
2024-11-14 01:05:53 +00:00
parent d1200eea74
commit 3c1a277b37
23 changed files with 1867 additions and 3829 deletions

38
.dockerignore Normal file
View File

@@ -0,0 +1,38 @@
# flyctl launch added from .gitignore
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
node_modules
.pnp
**/.pnp.js
**/.yarn/install-state.gz
# testing
coverage
# next.js
.next
out
# production
build
# misc
**/.DS_Store
**/*.pem
# debug
**/npm-debug.log*
**/yarn-debug.log*
**/yarn-error.log*
# local env files
**/.env*.local
# vercel
**/.vercel
# typescript
**/*.tsbuildinfo
**/next-env.d.ts
fly.toml

View File

@@ -2,35 +2,50 @@ name: Build and deploy
run-name: Build and deploy run-name: Build and deploy
on: on:
push: push:
# branches: branches:
# - main - main
jobs: jobs:
deploy: deploy:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- - name: Checkout
name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
github-server-url: 'https://gitea.home.joemonk.co.uk' github-server-url: 'https://gitea.home.joemonk.co.uk'
-
name: Set up docker - name: Set up docker
run: 'curl -fsSL https://get.docker.com | sh' run: 'curl -fsSL https://get.docker.com | sh'
-
name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
-
name: Login to private registry - name: Login to gitea registry
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: 'gitea.home.joemonk.co.uk/${{ github.repository }}' registry: 'gitea.home.joemonk.co.uk/${{ github.repository }}'
username: ${{ secrets.REGISTRY_USERNAME }} username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }} password: ${{ secrets.REGISTRY_PASSWORD }}
-
name: Build and push - name: Login to flyio registry
uses: docker/login-action@v3
with:
registry: 'registry.fly.io'
username: x
password: ${{ secrets.FLY_API_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
context: . context: .
push: true push: true
tags: 'gitea.home.joemonk.co.uk/${{ gitea.repository }}:latest' tags: |
'gitea.home.joemonk.co.uk/${{ gitea.repository }}:latest'
'gitea.home.joemonk.co.uk/${{ gitea.repository }}:${{ gitea.sha }}'
'registry.fly.io/${{ gitea.repository }}:${{ gitea.sha }}'
- uses: superfly/flyctl-actions/setup-flyctl@master
- run: flyctl deploy --remote-only -i registry.fly.io/${{ gitea.repository }}:${{ gitea.sha }}
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}

View File

@@ -39,6 +39,7 @@ RUN chown nextjs:nodejs .next
# https://nextjs.org/docs/advanced-features/output-file-tracing # https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=builder /app/db.sql ./db.sql
USER nextjs USER nextjs

30
fly.toml Normal file
View File

@@ -0,0 +1,30 @@
# fly.toml app configuration file generated for joemonk on 2024-11-13T18:49:43Z
#
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
#
app = 'joemonk'
primary_region = 'lhr'
[build]
[http_service]
internal_port = 3000
force_https = true
auto_stop_machines = 'stop'
auto_start_machines = true
min_machines_running = 0
processes = ['app']
[[http_service.checks]]
grace_period = "15s"
interval = "120s"
method = "GET"
timeout = "5s"
path = "/api/status"
protocol = "http"
[[vm]]
memory = '1gb'
cpu_kind = 'shared'
cpus = 1

View File

@@ -6,19 +6,26 @@ const nextConfig = {
pageExtensions: ["js", "jsx", "md", "mdx", "ts", "tsx"], pageExtensions: ["js", "jsx", "md", "mdx", "ts", "tsx"],
experimental: { experimental: {
reactCompiler: true, reactCompiler: true,
ppr: true, ppr: "incremental",
}, },
serverExternalPackages: ["typeorm"], serverExternalPackages: ["typeorm", "better-sqlite3"],
reactStrictMode: true, reactStrictMode: true,
output: "standalone", output: "standalone",
images: { images: {
remotePatterns: [ remotePatterns: [
{ {
protocol: "https", protocol: "https",
hostname: "fly.storage.tigris.dev" hostname: "fly.storage.tigris.dev",
}, },
], ],
}, },
typescript: {
// !! WARN !!
// Dangerously allow production builds to successfully complete even if
// your project has type errors.
// !! WARN !!
ignoreBuildErrors: true,
},
}; };
const millionConfig = { const millionConfig = {
@@ -30,4 +37,5 @@ const withMDX = createMDX({
// Add markdown plugins here, as desired // Add markdown plugins here, as desired
}); });
export default withMDX(million.next(nextConfig, millionConfig)); // export default withMDX(million.next(nextConfig, millionConfig));
export default withMDX(nextConfig);

5331
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@
"build:analyse": "ANALYZE=true npm run build", "build:analyse": "ANALYZE=true npm run build",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "next lint",
"lint:fix": "next lint -- --fix" "lint:fix": "next lint --fix"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.663.0", "@aws-sdk/client-s3": "^3.663.0",
@@ -32,12 +32,12 @@
"framer-motion": "^11.5.6", "framer-motion": "^11.5.6",
"glob": "^11.0.0", "glob": "^11.0.0",
"million": "^3.1.11", "million": "^3.1.11",
"next": "^15.0.0-rc.0", "next": "15.0.4-canary.2",
"next-auth": "^5.0.0-beta", "next-auth": "beta",
"postcss": "^8.4.47", "postcss": "^8.4.47",
"radash": "^12.1.0", "radash": "^12.1.0",
"react": "^19.0.0-rc-04bd67a4-20240924", "react": "19.0.0-rc-5c56b873-20241107",
"react-dom": "^19.0.0-rc-04bd67a4-20240924", "react-dom": "19.0.0-rc-5c56b873-20241107",
"react-zoom-pan-pinch": "^3.6.1", "react-zoom-pan-pinch": "^3.6.1",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"server-only": "^0.0.1", "server-only": "^0.0.1",

View File

@@ -1,23 +1,24 @@
import { signIn } from "@/lib/auth" import { signIn } from "@/lib/auth";
import type React from "react";
export default function Auth(props: { export default function Auth(props: {
searchParams: { callbackUrl: string | undefined } searchParams: Promise<{ callbackUrl: string | undefined }>
}) { }): React.JSX.Element {
return ( return (
<form <form
className="w-40 mx-auto" className="w-40 mx-auto"
action={async () => { action={async () => {
"use server" "use server";
await signIn("authelia", { await signIn("authelia", {
redirectTo: props.searchParams?.callbackUrl ?? "", redirectTo: (await props.searchParams)?.callbackUrl ?? "",
}) });
}} }}
> >
<button type="submit" <button type="submit"
className={`rounded-lg dark:bg-dracula-bg-light transition-colors duration-100 dark:text-white px-2 py-2 font-normal border-transparent`} className={`rounded-lg dark:bg-dracula-bg-light transition-colors duration-100 dark:text-white px-2 py-2 font-normal border-transparent`}
> >
<span>Sign in with Authelia</span> <span>Sign in with Authelia</span>
</button> </button>
</form> </form>
) );
} }

View File

@@ -1,8 +1,5 @@
import { SessionProvider } from "next-auth/react";
import NavBar from '@/components/navbar'; import NavBar from '@/components/navbar';
import Footer from '@/components/footer'; import Footer from '@/components/footer';
import LogIn from "@/components/auth/login";
import "../globals.css"; import "../globals.css";

View File

@@ -11,21 +11,23 @@ export default async function Photos(): Promise<React.JSX.Element> {
const {data: imageData} = await getImageData(); const {data: imageData} = await getImageData();
return ( return (
<Lightbox imageData={imageData.images}> <div className="mx-auto">
{imageData.images.map((image) => ( <Lightbox imageData={imageData.images}>
<Image {imageData.images.map((image) => (
key={image.src} <Image
alt={image.src} key={image.src}
src={image.src} alt={image.src}
className="object-contain h-60 w-80" src={image.src}
sizes="100vw" className="object-contain h-60 w-80"
loading="lazy" sizes="100vw"
width={image.width} loading="lazy"
height={image.height} width={image.width}
blurDataURL={image.blur} height={image.height}
placeholder="blur" blurDataURL={image.blur}
/> placeholder="blur"
))} />
</Lightbox> ))}
</Lightbox>
</div>
); );
} }

View File

@@ -15,9 +15,12 @@ export async function generateStaticParams(): Promise<{slug: string[]}[]> {
return slugs; return slugs;
} }
export default async function Post({params}: {params: { slug: string[] }}): Promise<React.JSX.Element> { export default async function Post({params}: {params: Promise<{ slug: string[] }>}): Promise<React.JSX.Element> {
const mdxFile = await import(`../../../../markdown/posts/[...slug]/${params.slug.join('/')}.mdx`) // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const Post = dynamic(async () => mdxFile); const mdxFile = await import(`../../../../markdown/posts/[...slug]/${(await params).slug.join('/')}.mdx`);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-return
const Post = dynamic(() => mdxFile);
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return ( return (
<Post/> <Post/>
); );

View File

@@ -21,12 +21,15 @@ async function loadPostDetails(): Promise<postDetails[]> {
}); });
const loadPostData = posts.map(async (post) => { const loadPostData = posts.map(async (post) => {
const slug = [post.split('/').at(-1)!.slice(0, -4)] const slug = [post.split('/').at(-1)!.slice(0, -4)];
const mdxFile = await import(`../../../../src/markdown/posts/[...slug]/${slug.join('/')}.mdx`) // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const mdxFile = await import(`../../../../src/markdown/posts/[...slug]/${slug.join('/')}.mdx`);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
return { return {
link: getCurrentUrl() + '/posts/' + slug.join('/'),
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment
metadata: mdxFile.metadata, metadata: mdxFile.metadata,
link: getCurrentUrl() + '/posts/' + slug.join('/') };
}
}); });
const postData = await Promise.all(loadPostData); const postData = await Promise.all(loadPostData);
@@ -39,7 +42,7 @@ const getPosts = unstable_cache(
{ {
revalidate: false revalidate: false
} }
) );
export default async function Posts(): Promise<React.JSX.Element> { export default async function Posts(): Promise<React.JSX.Element> {
const postDetails = await getPosts(); const postDetails = await getPosts();
@@ -58,7 +61,7 @@ export default async function Posts(): Promise<React.JSX.Element> {
<div key={`${post.link}_${tag}`}> <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>
) );
})} })}
</div> </div>
<p> <p>
@@ -66,7 +69,7 @@ export default async function Posts(): Promise<React.JSX.Element> {
</p> </p>
</div> </div>
</div> </div>
) );
})} })}
</div> </div>
); );

View File

@@ -2,21 +2,21 @@ import { NextRequest } from "next/server";
import { handlers } from "@/lib/auth"; import { handlers } from "@/lib/auth";
const reqWithTrustedOrigin = (req: NextRequest): NextRequest => { const reqWithTrustedOrigin = (req: NextRequest): NextRequest => {
const proto = req.headers.get('x-forwarded-proto') const proto = req.headers.get('x-forwarded-proto');
const host = req.headers.get('x-forwarded-host') const host = req.headers.get('x-forwarded-host');
if (!proto || !host) { if (!proto || !host) {
console.warn("Missing x-forwarded-proto or x-forwarded-host headers.") console.warn("Missing x-forwarded-proto or x-forwarded-host headers.");
return req return req;
} }
const envOrigin = `${proto}://${host}` const envOrigin = `${proto}://${host}`;
const { href, origin } = req.nextUrl const { href, origin } = req.nextUrl;
return new NextRequest(href.replace(origin, envOrigin), req) return new NextRequest(href.replace(origin, envOrigin), req);
} };
export const GET = (req: NextRequest) => { export const GET = (req: NextRequest): Promise<Response> => {
return handlers.GET(reqWithTrustedOrigin(req)) return handlers.GET(reqWithTrustedOrigin(req));
} };
export const POST = (req: NextRequest) => { export const POST = (req: NextRequest): Promise<Response> => {
return handlers.POST(reqWithTrustedOrigin(req)) return handlers.POST(reqWithTrustedOrigin(req));
} };

View File

@@ -49,8 +49,8 @@ export async function GET(): Promise<Response> {
}), }),
title: photo.title ?? undefined, title: photo.title ?? undefined,
description: photo.description ?? undefined description: photo.description ?? undefined
} };
}) });
return NextResponse.json<GetPhotos>({ status: 200, data: { images } }); return NextResponse.json<GetPhotos>({ status: 200, data: { images } });
} }

View File

@@ -15,7 +15,7 @@ export type GetPhotosUpdate = {
export const GET = auth(async function GET(req): Promise<Response> { export const GET = auth(async function GET(req): Promise<Response> {
if (!req.auth) { if (!req.auth) {
return NextResponse.json({ message: "Not authenticated" }, { status: 401 }) return NextResponse.json({ message: "Not authenticated" }, { status: 401 });
} }
const dataSource = await PhotoDataSource.dataSource; const dataSource = await PhotoDataSource.dataSource;
@@ -35,7 +35,7 @@ export const GET = auth(async function GET(req): Promise<Response> {
const s3Res = await s3Client.send(listObjCmd); const s3Res = await s3Client.send(listObjCmd);
if (!s3Res.Contents) { if (!s3Res.Contents) {
return NextResponse.json({ status: 500 }) return NextResponse.json({ status: 500 });
} }
const s3Photos = sift(s3Res.Contents.map((obj) => { const s3Photos = sift(s3Res.Contents.map((obj) => {
if (!obj.Key?.endsWith('/')) { if (!obj.Key?.endsWith('/')) {
@@ -51,7 +51,7 @@ export const GET = auth(async function GET(req): Promise<Response> {
const getImageCmd = new GetObjectCommand({ const getImageCmd = new GetObjectCommand({
Bucket: "joemonk-photos", Bucket: "joemonk-photos",
Key: fileName.replace("https://fly.storage.tigris.dev/joemonk-photos/", "") Key: fileName.replace("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();

View File

@@ -8,13 +8,13 @@ export default async function LogIn(): Promise<React.JSX.Element | undefined> {
return ( return (
<form <form
action={async () => { action={async () => {
"use server" "use server";
if (session?.user) { if (session?.user) {
await signOut({ await signOut({
redirectTo: `${getCurrentUrl()}/` redirectTo: `${getCurrentUrl()}/`
}) });
} else { } else {
await signIn("authelia") await signIn("authelia");
} }
}} }}
> >

View File

@@ -63,21 +63,23 @@ function NextJsImage({ slide, offset, rect, unoptimized = false }: {slide: Image
); );
} }
export default function MyLightbox({imageData, children}: {imageData: ImageData[], children: React.JSX.Element[]}): React.JSX.Element { export function MyLightbox({imageData, children}: {imageData: ImageData[], children: React.JSX.Element[]}): React.JSX.Element {
const [active, setActive] = useState<number | null>(null); const [active, setActive] = useState<number | null>(null);
return ( return (
<div className="mx-auto"> <div className="mx-auto">
<div className="flex flex-row flex-wrap justify-center"> <div className="flex flex-row flex-wrap justify-center">
{children.map((image, index) => ( {children.map((image, index) => {
<button key={`lightbox_img_${index}`} onClick={(() => { return (
setActive(index); <button key={`lightbox_img_${index}`} onClick={(() => {
})}> setActive(index);
<div className="relative"> })}>
{image} <div className="relative">
</div> {image}
</button> </div>
))} </button>
); }
)}
</div> </div>
<YARL <YARL
open={typeof active === 'number'} open={typeof active === 'number'}
@@ -90,4 +92,41 @@ export default function MyLightbox({imageData, children}: {imageData: ImageData[
/> />
</div> </div>
); );
}
interface FormElements extends HTMLFormControlsCollection {
src: HTMLInputElement
}
interface UsernameFormElement extends HTMLFormElement {
readonly elements: FormElements
}
export default function Test(props: {imageData: ImageData[], children: React.JSX.Element[]}): React.JSX.Element {
const [imageData, setImageData] = useState(props.imageData);
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 children = imageData.map((data) => props.children.find((child) => {
return data.src === child.key ? child : null;
})).filter(((data) => !!data));
return (
<>
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="src">Src:</label>
<input id="src" type="text" />
</div>
<button type="submit">Submit</button>
</form>
<MyLightbox imageData={imageData}>
{...children}
</MyLightbox>
</>
);
} }

View File

@@ -27,7 +27,7 @@ export default function NavBarClient({LogIn, navigation}: NavBarClientProps): Re
current.current = true; current.current = true;
} }
return nav; return nav;
}, [pathname]); }, [pathname, navigation]);
return ( return (
<nav className="dark:bg-dracula-bg-darker border-b-2 dark:border-dracula-purple"> <nav className="dark:bg-dracula-bg-darker border-b-2 dark:border-dracula-purple">

View File

@@ -12,7 +12,7 @@ const defaultNavigation = [
const authedNavigation = [ const authedNavigation = [
{ name: 'Manage', href: '/manage', current: false }, { name: 'Manage', href: '/manage', current: false },
] ];
export default async function NavBar(): Promise<React.JSX.Element> { export default async function NavBar(): Promise<React.JSX.Element> {
const session = await auth(); const session = await auth();

View File

@@ -22,7 +22,7 @@ export default function PostHeader({metadata}: PostHeaderProps): React.JSX.Eleme
<> <>
<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>
</> </>

View File

@@ -3,10 +3,10 @@ import { Photo } from "./entity/photo";
const dataSource = new DataSource({ const dataSource = new DataSource({
type: "better-sqlite3", type: "better-sqlite3",
database: "db.sql", database: `${process.cwd()}/db.sql`,
entities: [Photo], entities: [Photo],
migrations: ["./migrations"], migrations: ["./migrations"],
}) });
export default class PhotoDataSource { export default class PhotoDataSource {
private static _dataSource: DataSource | null = null; private static _dataSource: DataSource | null = null;
@@ -22,11 +22,11 @@ export default class PhotoDataSource {
static async initDataSource(): Promise<DataSource> { static async initDataSource(): Promise<DataSource> {
if (!PhotoDataSource._dataSource || !PhotoDataSource._dataSource.isInitialized) { if (!PhotoDataSource._dataSource || !PhotoDataSource._dataSource.isInitialized) {
const ds = await dataSource.initialize(); const ds = await dataSource.initialize();
console.log('Photo data source initialized') console.log('Photo data source initialized');
PhotoDataSource._dataSource = ds; PhotoDataSource._dataSource = ds;
} }
return PhotoDataSource._dataSource; return PhotoDataSource._dataSource;
} }
} }
PhotoDataSource.initDataSource(); await PhotoDataSource.initDataSource();

View File

@@ -1,48 +1,48 @@
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm" import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity() @Entity()
export class Photo { export class Photo {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id!: number id!: number;
@Column("text", { unique: true }) @Column("text", { unique: true })
src!: string; src!: string;
@Column() @Column()
width!: number width!: number;
@Column() @Column()
height!: number height!: number;
@Column("blob") @Column("blob")
blur!: string blur!: string;
@Column("text", { nullable: true }) @Column("text", { nullable: true })
camera: string | null = null; camera: string | null = null;
// Manually input data // Manually input data
@Column("text", { nullable: true }) @Column("text", { nullable: true })
title: string | null = null; title: string | null = null;
@Column("text", { nullable: true }) @Column("text", { nullable: true })
description: string | null = null; description: string | null = null;
// Exif data // Exif data
@Column("int", { nullable: true }) @Column("int", { nullable: true })
exposureBiasValue: number | null = null exposureBiasValue: number | null = null;
@Column("float", { nullable: true }) @Column("float", { nullable: true })
fNumber: number | null = null fNumber: number | null = null;
@Column("int", { nullable: true }) @Column("int", { nullable: true })
isoSpeedRatings: number | null = null isoSpeedRatings: number | null = null;
@Column("int", { nullable: true }) @Column("int", { nullable: true })
focalLength: number | null = null focalLength: number | null = null;
@Column("date", { nullable: true }) @Column("date", { nullable: true })
dateTimeOriginal: Date | null = null dateTimeOriginal: Date | null = null;
@Column("text", { nullable: true }) @Column("text", { nullable: true })
lensModel: string | null = null lensModel: string | null = null;
} }

View File

@@ -13,4 +13,4 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
}], }],
trustHost: true, trustHost: true,
redirectProxyUrl: `${getCurrentUrl()}/api/auth`, redirectProxyUrl: `${getCurrentUrl()}/api/auth`,
}) });