Huge number of changes, upgrade to next 15, add loads of pages, auth, add ci, loads of clean up, a db for images etc
This commit is contained in:
23
src/app/(root)/auth/page.tsx
Normal file
23
src/app/(root)/auth/page.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { signIn } from "@/lib/auth"
|
||||
|
||||
export default function Auth(props: {
|
||||
searchParams: { callbackUrl: string | undefined }
|
||||
}) {
|
||||
return (
|
||||
<form
|
||||
className="w-40 mx-auto"
|
||||
action={async () => {
|
||||
"use server"
|
||||
await signIn("authelia", {
|
||||
redirectTo: props.searchParams?.callbackUrl ?? "",
|
||||
})
|
||||
}}
|
||||
>
|
||||
<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`}
|
||||
>
|
||||
<span>Sign in with Authelia</span>
|
||||
</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
import "../globals.css";
|
||||
import { SessionProvider } from "next-auth/react";
|
||||
|
||||
import NavBar from '@/components/navbar';
|
||||
import Footer from '@/components/footer';
|
||||
import LogIn from "@/components/auth/login";
|
||||
|
||||
import "../globals.css";
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
@@ -11,9 +13,11 @@ export default function RootLayout({
|
||||
}>): React.JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<NavBar LogIn={<LogIn/>}/>
|
||||
<main className="px-6 py-4 w-full mx-auto flex-1 align-middle lg:max-w-5xl">
|
||||
{children}
|
||||
<NavBar/>
|
||||
<main className="px-6 py-4 w-full flex-1 align-middle overflow-y-scroll scrollbar scrollbar-thumb-dracula-purple scrollbar-track-dracula-bg-light">
|
||||
<div className="mx-auto w-full align-middle lg:max-w-5xl ">
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
<Footer/>
|
||||
</>
|
||||
|
||||
@@ -4,7 +4,6 @@ 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'] } });
|
||||
console.log(res);
|
||||
return res.json() as Promise<GetPhotos>;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,32 +1,24 @@
|
||||
import { glob } from "glob";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
// type postMdx = {
|
||||
// metadata: {
|
||||
// title: string,
|
||||
// date: string,
|
||||
// coverImage: string,
|
||||
// blurb: string,
|
||||
// shortBlurb: string,
|
||||
// tags: string[]
|
||||
// }
|
||||
// }
|
||||
export const dynamicParams = false;
|
||||
|
||||
export async function generateStaticParams(): Promise<{slug: string[]}[]> {
|
||||
const posts = await glob(`src/markdown/posts/[...slug]/**/*.mdx`, {
|
||||
const posts = await glob(`${process.cwd()}/src/markdown/posts/[[]...slug[]]/**/*.mdx`, {
|
||||
nodir: true,
|
||||
});
|
||||
|
||||
const slugs = posts.map((post) => ({
|
||||
slug: post.replace('src/markdown/posts/[...slug]/', '').replace(/\.mdx$/, '').split('/')
|
||||
slug: [post.split('/').at(-1)!.slice(0, -4)]
|
||||
}));
|
||||
|
||||
return slugs;
|
||||
}
|
||||
|
||||
export default function Post({params}: {params: { slug: string[] }}): React.JSX.Element {
|
||||
export default async function Post({params}: {params: { slug: string[] }}): Promise<React.JSX.Element> {
|
||||
const mdxFile = await import(`../../../../markdown/posts/[...slug]/${params.slug.join('/')}.mdx`)
|
||||
const Post = dynamic(async () => mdxFile);
|
||||
return (
|
||||
<>
|
||||
{params.slug}
|
||||
</>
|
||||
<Post/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,73 @@
|
||||
export default function Posts(): React.JSX.Element {
|
||||
import { glob } from "glob";
|
||||
import { getCurrentUrl } from "@/lib/current-url";
|
||||
import { unstable_cache } from "next/cache";
|
||||
import Link from "next/link";
|
||||
|
||||
type postDetails = {
|
||||
link: string,
|
||||
metadata: {
|
||||
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`, {
|
||||
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`)
|
||||
return {
|
||||
metadata: mdxFile.metadata,
|
||||
link: getCurrentUrl() + '/posts/' + slug.join('/')
|
||||
}
|
||||
});
|
||||
|
||||
const postData = await Promise.all(loadPostData);
|
||||
return postData;
|
||||
}
|
||||
|
||||
const getPosts = unstable_cache(
|
||||
loadPostDetails,
|
||||
['posts'],
|
||||
{
|
||||
revalidate: false
|
||||
}
|
||||
)
|
||||
|
||||
export default async function Posts(): Promise<React.JSX.Element> {
|
||||
const postDetails = await getPosts();
|
||||
return (
|
||||
<>
|
||||
Actually this should be custom
|
||||
</>
|
||||
<div className="flex flex-col gap-6">
|
||||
{postDetails.map((post) => {
|
||||
return (
|
||||
<div key={post.link}>
|
||||
<div className="prose dark:prose-invert mx-auto">
|
||||
<h2>
|
||||
<Link href={post.link}>{post.metadata.title}</Link>
|
||||
</h2>
|
||||
<div className="flex flex-row">
|
||||
{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>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<p>
|
||||
{post.metadata.blurb}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,22 @@
|
||||
import NextAuth from "next-auth";
|
||||
import { authConfig } from "@/lib/auth";
|
||||
import { NextRequest } from "next/server";
|
||||
import { handlers } from "@/lib/auth";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const handler = NextAuth(authConfig);
|
||||
const reqWithTrustedOrigin = (req: NextRequest): NextRequest => {
|
||||
const proto = req.headers.get('x-forwarded-proto')
|
||||
const host = req.headers.get('x-forwarded-host')
|
||||
if (!proto || !host) {
|
||||
console.warn("Missing x-forwarded-proto or x-forwarded-host headers.")
|
||||
return req
|
||||
}
|
||||
const envOrigin = `${proto}://${host}`
|
||||
const { href, origin } = req.nextUrl
|
||||
return new NextRequest(href.replace(origin, envOrigin), req)
|
||||
}
|
||||
|
||||
export { handler as GET, handler as POST };
|
||||
export const GET = (req: NextRequest) => {
|
||||
return handlers.GET(reqWithTrustedOrigin(req))
|
||||
}
|
||||
|
||||
export const POST = (req: NextRequest) => {
|
||||
return handlers.POST(reqWithTrustedOrigin(req))
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
import exifReader from "exif-reader";
|
||||
import { glob } from "glob";
|
||||
import { NextResponse } from "next/server";
|
||||
import { pick } from "radash";
|
||||
import sharp from "sharp";
|
||||
import { shake } from "radash";
|
||||
import PhotoDataSource from "@/data-source";
|
||||
import { Photo } from "@/entity/photo";
|
||||
|
||||
export type ImageData = {
|
||||
width: number,
|
||||
@@ -17,7 +16,9 @@ export type ImageData = {
|
||||
FocalLength: number,
|
||||
DateTimeOriginal: Date,
|
||||
LensModel: string
|
||||
}>
|
||||
}>,
|
||||
title?: string,
|
||||
description?: string
|
||||
}
|
||||
|
||||
export type GetPhotos = {
|
||||
@@ -28,28 +29,28 @@ export type GetPhotos = {
|
||||
}
|
||||
|
||||
export async function GET(): Promise<Response> {
|
||||
const photosGlob = await glob(`public/photos/**/*.{png,jpeg,jpg}`, {
|
||||
nodir: true,
|
||||
});
|
||||
|
||||
const imageData = photosGlob.map(async (fileName: string) => {
|
||||
const { width, height, exif } = await sharp(fileName).metadata();
|
||||
const blur = await sharp(fileName)
|
||||
.resize({ width: 12, height: 12, fit: 'inside' })
|
||||
.toBuffer();
|
||||
const exifData = exif ? exifReader(exif) : undefined;
|
||||
|
||||
const dataSource = await PhotoDataSource.dataSource;
|
||||
const photoRepository = dataSource.getRepository(Photo);
|
||||
const currentSources = await photoRepository.find();
|
||||
const images = currentSources.map((photo) => {
|
||||
return {
|
||||
width: width ?? 10,
|
||||
height: height ?? 10,
|
||||
blur: `data:image/jpeg;base64,${blur.toString('base64')}` as `data:image/${string}`,
|
||||
src: fileName.slice(6),
|
||||
camera: exifData?.Image?.Model,
|
||||
exif: pick(exifData?.Photo ?? {}, ['ExposureBiasValue', 'FNumber', 'ISOSpeedRatings', 'FocalLength', 'DateTimeOriginal', 'LensModel'])
|
||||
};
|
||||
});
|
||||
|
||||
const images = await Promise.all(imageData);
|
||||
width: photo.width,
|
||||
height: photo.height,
|
||||
blur: photo.blur as `data:image/${string}`,
|
||||
src: photo.src,
|
||||
camera: photo.camera ?? undefined,
|
||||
exif: shake({
|
||||
ExposureBiasValue: photo.exposureBiasValue,
|
||||
FNumber: photo.fNumber,
|
||||
ISOSpeedRatings: photo.isoSpeedRatings,
|
||||
FocalLength: photo.focalLength,
|
||||
DateTimeOriginal: photo.dateTimeOriginal,
|
||||
LensModel: photo.lensModel
|
||||
}),
|
||||
title: photo.title ?? undefined,
|
||||
description: photo.description ?? undefined
|
||||
}
|
||||
})
|
||||
|
||||
return NextResponse.json<GetPhotos>({ status: 200, data: { images } });
|
||||
}
|
||||
86
src/app/api/photos/update/route.ts
Normal file
86
src/app/api/photos/update/route.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
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 PhotoDataSource from "@/data-source";
|
||||
import { Photo } from "@/entity/photo";
|
||||
import { auth } from "@/lib/auth";
|
||||
|
||||
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 })
|
||||
}
|
||||
|
||||
const dataSource = await PhotoDataSource.dataSource;
|
||||
const photoRepository = dataSource.getRepository(Photo);
|
||||
const currentSources = (await photoRepository.find({
|
||||
select: {
|
||||
src: true
|
||||
}
|
||||
})).map((photo) => photo.src);
|
||||
|
||||
const s3Client = new S3Client();
|
||||
|
||||
const listObjCmd = new ListObjectsV2Command({
|
||||
Bucket: "joemonk-photos"
|
||||
});
|
||||
|
||||
const s3Res = await s3Client.send(listObjCmd);
|
||||
|
||||
if (!s3Res.Contents) {
|
||||
return NextResponse.json({ status: 500 })
|
||||
}
|
||||
const s3Photos = sift(s3Res.Contents.map((obj) => {
|
||||
if (!obj.Key?.endsWith('/')) {
|
||||
return `https://fly.storage.tigris.dev/joemonk-photos/${obj.Key}`;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}));
|
||||
|
||||
const newPhotos = diff(s3Photos, currentSources);
|
||||
|
||||
const imageData = newPhotos.map(async (fileName: string) => {
|
||||
const getImageCmd = new GetObjectCommand({
|
||||
Bucket: "joemonk-photos",
|
||||
Key: fileName.replace("https://fly.storage.tigris.dev/joemonk-photos/", "")
|
||||
})
|
||||
const imgRes = await s3Client.send(getImageCmd);
|
||||
const image = await imgRes.Body?.transformToByteArray();
|
||||
|
||||
const { width, height, exif } = await sharp(image).metadata();
|
||||
const blur = await sharp(image)
|
||||
.resize({ width: 12, height: 12, fit: 'inside' })
|
||||
.toBuffer();
|
||||
const exifData = exif ? exifReader(exif) : undefined;
|
||||
|
||||
const photo = new Photo();
|
||||
photo.src = fileName;
|
||||
photo.width = width ?? 10;
|
||||
photo.height = height ?? 10;
|
||||
photo.blur = `data:image/jpeg;base64,${blur.toString('base64')}` as `data:image/${string}`;
|
||||
photo.camera = exifData?.Image?.Model ?? null;
|
||||
|
||||
photo.exposureBiasValue = exifData?.Photo?.ExposureBiasValue ?? null;
|
||||
photo.fNumber = exifData?.Photo?.FNumber ?? null;
|
||||
photo.isoSpeedRatings = exifData?.Photo?.ISOSpeedRatings ?? null;
|
||||
photo.focalLength = exifData?.Photo?.FocalLength ?? null;
|
||||
photo.dateTimeOriginal = exifData?.Photo?.DateTimeOriginal ?? null;
|
||||
photo.lensModel = exifData?.Photo?.LensModel ?? null;
|
||||
|
||||
return photo;
|
||||
});
|
||||
|
||||
const images = await Promise.all(imageData);
|
||||
|
||||
await photoRepository.save(images);
|
||||
|
||||
return NextResponse.json<GetPhotosUpdate>({ status: 200, s3Photos: newPhotos });
|
||||
});
|
||||
@@ -1,15 +0,0 @@
|
||||
import { auth } from '@/lib/auth';
|
||||
import { revalidateTag } from 'next/cache';
|
||||
import { draftMode } from 'next/headers';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
export async function GET(): Promise<Response> {
|
||||
const session = await auth();
|
||||
if (session) {
|
||||
draftMode().enable();
|
||||
revalidateTag('datocms');
|
||||
} else if (draftMode().isEnabled) {
|
||||
draftMode().disable();
|
||||
}
|
||||
redirect('/');
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import "reflect-metadata";
|
||||
import type { Metadata } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
import "./globals.css";
|
||||
@@ -8,8 +9,8 @@ const inter = Inter({
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
title: "Joe Monk",
|
||||
description: "A portfolio page showing some of the things I've done",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
@@ -24,15 +25,15 @@ export default function RootLayout({
|
||||
<script id="SetTheme"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
if (localStorage.theme !== 'dark' || (!('theme' in localStorage) && !window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
document.documentElement.classList.remove('dark');
|
||||
} else {
|
||||
document.documentElement.classList.add('dark');
|
||||
}`,
|
||||
}}>
|
||||
</script>
|
||||
</head>
|
||||
<body className="min-h-screen flex flex-col bg-dracula-bg-lightest dark:bg-dracula-bg print:white">
|
||||
<body className="min-h-screen flex flex-col bg-dracula-bg-lightest dark:bg-dracula-bg print:white max-h-screen">
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user