Looking at adding a lightbox
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
|
import million from "million/compiler";
|
||||||
|
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
swcMinify: true,
|
swcMinify: true,
|
||||||
@@ -5,4 +7,8 @@ const nextConfig = {
|
|||||||
output: "standalone",
|
output: "standalone",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
const millionConfig = {
|
||||||
|
auto: { rsc: true }, rsc: true
|
||||||
|
}
|
||||||
|
|
||||||
|
export default million.next(nextConfig, millionConfig);
|
||||||
|
|||||||
1125
package-lock.json
generated
1125
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
23
package.json
23
package.json
@@ -11,32 +11,31 @@
|
|||||||
"lint:fix": "next lint -- --fix"
|
"lint:fix": "next lint -- --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@headlessui/react": "^1.7.18",
|
|
||||||
"@heroicons/react": "^2.1.3",
|
"@heroicons/react": "^2.1.3",
|
||||||
"@next/bundle-analyzer": "^14.1.4",
|
"@next/bundle-analyzer": "^14.2.2",
|
||||||
"@tailwindcss/typography": "^0.5.12",
|
"@tailwindcss/typography": "^0.5.12",
|
||||||
"@types/node": "^20.11.30",
|
"@types/node": "^20.12.7",
|
||||||
"@types/react": "^18.2.73",
|
"@types/react": "^18.2.79",
|
||||||
"@types/react-dom": "^18.2.23",
|
"@types/react-dom": "^18.2.25",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
"@typescript-eslint/eslint-plugin": "^7.7.1",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-config-next": "14.1.4",
|
"eslint-config-next": "14.2.2",
|
||||||
"exif-reader": "^2.0.1",
|
"exif-reader": "^2.0.1",
|
||||||
"framer-motion": "^11.0.23",
|
"framer-motion": "^11.1.7",
|
||||||
"glob": "^10.3.12",
|
"glob": "^10.3.12",
|
||||||
"million": "^3.0.6",
|
"million": "^3.0.6",
|
||||||
"next": "14.1.4",
|
"next": "14.2.2",
|
||||||
"next-auth": "^4.24.7",
|
"next-auth": "^4.24.7",
|
||||||
"postcss": "^8.4.38",
|
"postcss": "^8.4.38",
|
||||||
"radash": "^12.1.0",
|
"radash": "^12.1.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-datocms": "^5.0.3",
|
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"react-zoom-pan-pinch": "^3.4.4",
|
||||||
"server-only": "^0.0.1",
|
"server-only": "^0.0.1",
|
||||||
"sharp": "^0.33.3",
|
"sharp": "^0.33.3",
|
||||||
"tailwind-scrollbar": "^3.1.0",
|
"tailwind-scrollbar": "^3.1.0",
|
||||||
"tailwindcss": "^3.4.3",
|
"tailwindcss": "^3.4.3",
|
||||||
"typescript": "^5.4.3"
|
"typescript": "^5.4.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export default function RootLayout({
|
|||||||
children,
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>): JSX.Element {
|
}>): React.JSX.Element {
|
||||||
return (
|
return (
|
||||||
<html className={`${inter.variable} font-sans`} lang="en">
|
<html className={`${inter.variable} font-sans`} lang="en">
|
||||||
<body className="min-h-screen flex flex-col bg-dracula-bg">
|
<body className="min-h-screen flex flex-col bg-dracula-bg">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import Sim from '@/components/sim';
|
import Sim from '@/components/sim';
|
||||||
|
|
||||||
export default function Home(): JSX.Element {
|
export default function Home(): React.JSX.Element {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Sim/>
|
<Sim/>
|
||||||
|
|||||||
@@ -3,14 +3,15 @@ import { glob } from "glob";
|
|||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
import exifReader from 'exif-reader';
|
import exifReader from 'exif-reader';
|
||||||
import { pick } from 'radash';
|
import { pick } from 'radash';
|
||||||
|
import Lightbox from "@/components/lightbox";
|
||||||
|
|
||||||
type ImageData = {
|
type ImageData = {
|
||||||
width: number,
|
width: number,
|
||||||
height: number,
|
height: number,
|
||||||
blur: string,
|
blur: `data:image/${string}`,
|
||||||
src: string,
|
src: string,
|
||||||
|
camera?: string,
|
||||||
exif: Partial<{
|
exif: Partial<{
|
||||||
ExposureProgram: number,
|
|
||||||
ExposureBiasValue: number,
|
ExposureBiasValue: number,
|
||||||
FNumber: number,
|
FNumber: number,
|
||||||
ISOSpeedRatings: number,
|
ISOSpeedRatings: number,
|
||||||
@@ -24,30 +25,32 @@ export async function getImages(): Promise<{images: ImageData[]}> {
|
|||||||
const photosGlob = await glob(`public/photos/**/*.{png,jpeg,jpg}`, {
|
const photosGlob = await glob(`public/photos/**/*.{png,jpeg,jpg}`, {
|
||||||
nodir: true,
|
nodir: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const images = photosGlob.map(async (fileName) => {
|
const images = photosGlob.map(async (fileName) => {
|
||||||
const { width, height, exif } = await sharp(fileName).metadata();
|
const { width, height, exif } = await sharp(fileName).metadata();
|
||||||
const blur = await sharp(fileName)
|
const blur = await sharp(fileName)
|
||||||
.resize({ width: 10, height: 10, fit: 'inside' })
|
.resize({ width: 12, height: 12, fit: 'inside' })
|
||||||
.toBuffer();
|
.toBuffer();
|
||||||
const exifData = exif ? exifReader(exif) : undefined;
|
const exifData = exif ? exifReader(exif) : undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
width: width ?? 10,
|
width: width ?? 10,
|
||||||
height: height ?? 10,
|
height: height ?? 10,
|
||||||
blur: blur.toString('base64'),
|
blur: `data:image/jpeg;base64,${blur.toString('base64')}` as `data:image/${string}`,
|
||||||
src: fileName.slice(6),
|
src: fileName.slice(6),
|
||||||
exif: pick(exifData?.Photo ?? {}, ['ExposureProgram', 'ExposureBiasValue', 'FNumber', 'ISOSpeedRatings', 'FocalLength', 'DateTimeOriginal', 'LensModel'])
|
camera: exifData?.Image?.Model,
|
||||||
|
exif: pick(exifData?.Photo ?? {}, ['ExposureBiasValue', 'FNumber', 'ISOSpeedRatings', 'FocalLength', 'DateTimeOriginal', 'LensModel'])
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return { images: await Promise.all(images) };
|
return { images: await Promise.all(images) };
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function Home(): Promise<JSX.Element> {
|
export default async function Home(): Promise<React.JSX.Element> {
|
||||||
const { images } = await getImages();
|
const { images } = await getImages();
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-2 grid-cols-3">
|
<Lightbox imageData={images}>
|
||||||
{images.map((image) => (
|
{images.map((image) => (
|
||||||
<div className="relative" key={image.src}>
|
<div className="relative" key={image.src}>
|
||||||
<Image
|
<Image
|
||||||
@@ -58,11 +61,11 @@ export default async function Home(): Promise<JSX.Element> {
|
|||||||
loading="lazy"
|
loading="lazy"
|
||||||
width={image.width}
|
width={image.width}
|
||||||
height={image.height}
|
height={image.height}
|
||||||
blurDataURL={`data:image/jpeg;base64,${image.blur}`}
|
blurDataURL={image.blur}
|
||||||
placeholder={`data:image/jpeg;base64,${image.blur}`}
|
placeholder={image.blur}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</Lightbox>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { LogoutButton } from "./logout_button";
|
|||||||
/**
|
/**
|
||||||
* This is a server component, then the buttons are client side
|
* This is a server component, then the buttons are client side
|
||||||
*/
|
*/
|
||||||
export default async function LogIn(): Promise<JSX.Element | undefined> {
|
export default async function LogIn(): Promise<React.JSX.Element | undefined> {
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
if (session) {
|
if (session) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { UserCircleIcon } from '@heroicons/react/24/outline';
|
import { UserCircleIcon } from '@heroicons/react/24/outline';
|
||||||
import { signIn } from "next-auth/react";
|
import { signIn } from "next-auth/react";
|
||||||
|
|
||||||
export function LoginButton(): JSX.Element {
|
export function LoginButton(): React.JSX.Element {
|
||||||
return (
|
return (
|
||||||
<button className="p-1 hover:bg-dracula-bglight rounded-3xl transition-colors group" onClick={() => void signIn('cognito')}>
|
<button className="p-1 hover:bg-dracula-bglight rounded-3xl transition-colors group" onClick={() => void signIn('cognito')}>
|
||||||
<UserCircleIcon className='stroke-dracula-cyan h-8 w-auto group-hover:stroke-dracula-orange transition-colors'/>
|
<UserCircleIcon className='stroke-dracula-cyan h-8 w-auto group-hover:stroke-dracula-orange transition-colors'/>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { UserCircleIcon } from '@heroicons/react/24/outline';
|
import { UserCircleIcon } from '@heroicons/react/24/outline';
|
||||||
import { signOut } from "next-auth/react";
|
import { signOut } from "next-auth/react";
|
||||||
|
|
||||||
export function LogoutButton(): JSX.Element {
|
export function LogoutButton(): React.JSX.Element {
|
||||||
return (
|
return (
|
||||||
<button className="p-1 hover:bg-dracula-bglight rounded-3xl transition-colors group" onClick={() => void signOut()}>
|
<button className="p-1 hover:bg-dracula-bglight rounded-3xl transition-colors group" onClick={() => void signOut()}>
|
||||||
<UserCircleIcon className='stroke-dracula-cyan h-8 w-auto group-hover:stroke-dracula-red transition-colors'/>
|
<UserCircleIcon className='stroke-dracula-cyan h-8 w-auto group-hover:stroke-dracula-red transition-colors'/>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export default function NavBar(): JSX.Element {
|
export default function NavBar(): React.JSX.Element {
|
||||||
return (
|
return (
|
||||||
<footer className="bg-dracula-bg-darker border-t-2 border-dracula-purple">
|
<footer className="bg-dracula-bg-darker border-t-2 border-dracula-purple">
|
||||||
<div className="mx-auto max-w-7xl px-4">
|
<div className="mx-auto max-w-7xl px-4">
|
||||||
|
|||||||
76
src/components/lightbox.tsx
Normal file
76
src/components/lightbox.tsx
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
"use client";
|
||||||
|
import { useState } from "react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { TransformWrapper, TransformComponent } from "react-zoom-pan-pinch";
|
||||||
|
|
||||||
|
type ImageData = {
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
blur: `data:image/${string}`,
|
||||||
|
src: string,
|
||||||
|
camera?: string,
|
||||||
|
exif: Partial<{
|
||||||
|
ExposureBiasValue: number,
|
||||||
|
FNumber: number,
|
||||||
|
ISOSpeedRatings: number,
|
||||||
|
FocalLength: number,
|
||||||
|
DateTimeOriginal: Date,
|
||||||
|
LensModel: string
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
type LightboxProps = {
|
||||||
|
children: React.JSX.Element[],
|
||||||
|
imageData: ImageData[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Lightbox(props: LightboxProps): React.JSX.Element {
|
||||||
|
const [active, setActive] = useState<ImageData | null>(null);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="grid gap-2 grid-cols-3">
|
||||||
|
{props.children.map((image, index) => (
|
||||||
|
<button key={`lightbox_img_${index}`} onClick={(() => {
|
||||||
|
setActive(props.imageData[index]);
|
||||||
|
})}>
|
||||||
|
{image}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{ active ? (
|
||||||
|
<div className="fixed top-0 left-0 w-full h-full z-30">
|
||||||
|
<div className="bg-black bg-opacity-80 w-full h-full p-12">
|
||||||
|
<div className="flex flex-col w-full h-full">
|
||||||
|
<div className="p-1 mb-4">
|
||||||
|
<span className="text-white py-1.5 px-3 bg-dracula-bg-lighter border border-dracula-purple">{active.exif.DateTimeOriginal?.toLocaleDateString()}</span>
|
||||||
|
<button className="text-white py-1.5 px-3 bg-dracula-bg-lighter border border-dracula-purple" onClick={() => setActive(null)}>
|
||||||
|
<span>x</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="relative w-full h-full m-auto">
|
||||||
|
<Image
|
||||||
|
alt={active.src}
|
||||||
|
src={active.src}
|
||||||
|
className="opacity-100 object-contain m-auto"
|
||||||
|
loading="eager"
|
||||||
|
unoptimized={true}
|
||||||
|
fill
|
||||||
|
blurDataURL={active.blur}
|
||||||
|
placeholder={active.blur}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row flex-wrap self-center justify-center gap-4 max-w-3xl mt-4">
|
||||||
|
<span className="text-white py-1.5 px-3 bg-dracula-bg-lighter border border-dracula-purple">{active.camera}</span>
|
||||||
|
<span className="text-white py-1.5 px-3 bg-dracula-bg-lighter border border-dracula-purple">{active.exif.LensModel}</span>
|
||||||
|
<span className="text-white py-1.5 px-3 bg-dracula-bg-lighter border border-dracula-purple">{active.exif.FocalLength}mm</span>
|
||||||
|
<span className="text-white py-1.5 px-3 bg-dracula-bg-lighter border border-dracula-purple">f/{active.exif.FNumber}</span>
|
||||||
|
<span className="text-white py-1.5 px-3 bg-dracula-bg-lighter border border-dracula-purple">ISO {active.exif.ISOSpeedRatings}</span>
|
||||||
|
<span className="text-white py-1.5 px-3 bg-dracula-bg-lighter border border-dracula-purple">{active.exif.ExposureBiasValue}EV</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ const navigation = [
|
|||||||
{ name: 'Contact', href: '#', current: false },
|
{ name: 'Contact', href: '#', current: false },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function NavBar({LogIn}: {LogIn: JSX.Element}): JSX.Element {
|
export default function NavBar({LogIn}: {LogIn: React.JSX.Element}): React.JSX.Element {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { shuffle } from "radash";
|
import { shuffle } from "radash";
|
||||||
import { FormEvent, useState, useRef, useCallback, useEffect } from "react";
|
import { FormEvent, useState, useRef, useCallback, useEffect } from "react";
|
||||||
|
|
||||||
export default function Sim(): JSX.Element {
|
export default function Sim(): React.JSX.Element {
|
||||||
const [outputs, setOutputs] = useState<
|
const [outputs, setOutputs] = useState<
|
||||||
{ runs: number; tickets: number; totalWin: number }[]
|
{ runs: number; tickets: number; totalWin: number }[]
|
||||||
>([]);
|
>([]);
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ const config: Config = {
|
|||||||
colors: {
|
colors: {
|
||||||
// Nicked from the vs code version of the theme https://github.com/dracula/visual-studio-code/blob/master/src/dracula.yml
|
// Nicked from the vs code version of the theme https://github.com/dracula/visual-studio-code/blob/master/src/dracula.yml
|
||||||
dracula: {
|
dracula: {
|
||||||
'bglighter': '#424450',
|
'bg-lighter': '#424450',
|
||||||
'bglight': '#343746',
|
'bg-light': '#343746',
|
||||||
'bg': '#282A36',
|
'bg': '#282A36',
|
||||||
'bg-dark': '#21222C',
|
'bg-dark': '#21222C',
|
||||||
'bg-darker': '#191A21',
|
'bg-darker': '#191A21',
|
||||||
|
|||||||
Reference in New Issue
Block a user