279 lines
6.6 KiB
TypeScript
279 lines
6.6 KiB
TypeScript
"use client";
|
|
import type { PhotoData } from "@/server/api/routers/photos/list";
|
|
import { api } from "@/trpc/react";
|
|
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 Tiptap from "./photo-editor";
|
|
|
|
const FormSchema = z.object({
|
|
title: z
|
|
.string()
|
|
.min(3, "Title should be over 3 characters")
|
|
.max(128, "Title cannot be over 128 characters"),
|
|
description: z.object({
|
|
type: z.string(),
|
|
content: z.array(z.unknown()),
|
|
}),
|
|
});
|
|
|
|
type IFormInput = z.infer<typeof FormSchema>;
|
|
|
|
interface DirectoryTree {
|
|
[key: string]: DirectoryTree;
|
|
}
|
|
|
|
function buildDirectoryTree(filePaths: string[]): DirectoryTree {
|
|
const root: DirectoryTree = {};
|
|
|
|
filePaths.forEach((path) => {
|
|
const parts = path.split("/").filter((p) => p.length > 0);
|
|
let current = root;
|
|
|
|
// Traverse or create nodes for each part of the path
|
|
for (let i = 0; i < parts.length; i++) {
|
|
const part = parts[i];
|
|
if (part) {
|
|
if (!current[part]) {
|
|
current[part] = {};
|
|
}
|
|
current = current[part];
|
|
}
|
|
}
|
|
});
|
|
|
|
return root;
|
|
}
|
|
|
|
type Item = {
|
|
type: "directory" | "file";
|
|
name: string;
|
|
fullPath: string;
|
|
children?: Item[];
|
|
};
|
|
|
|
function renderTree(node: DirectoryTree, pathSoFar = ""): Item[] {
|
|
const entries = Object.entries(node);
|
|
const items: Item[] = [];
|
|
|
|
for (const [name, children] of entries) {
|
|
const fullPath = pathSoFar ? `${pathSoFar}/${name}` : name;
|
|
const isLeaf = Object.keys(children).length === 0;
|
|
|
|
if (isLeaf) {
|
|
// It's a file
|
|
items.push({ type: "file", name, fullPath });
|
|
} else {
|
|
// It's a directory
|
|
items.push({
|
|
type: "directory",
|
|
name,
|
|
fullPath,
|
|
children: renderTree(children, fullPath),
|
|
});
|
|
}
|
|
}
|
|
|
|
return items;
|
|
}
|
|
|
|
function RenderLeaf(leaf: Item[], selectImageTab: (path: string) => void) {
|
|
return leaf.map((leaf) => {
|
|
if (leaf.children?.length) {
|
|
return (
|
|
<li>
|
|
<details open>
|
|
<summary>
|
|
<DirSvg />
|
|
{leaf.name}
|
|
</summary>
|
|
<ul>{RenderLeaf(leaf.children, selectImageTab)}</ul>
|
|
</details>
|
|
</li>
|
|
);
|
|
}
|
|
return (
|
|
<li key={leaf.fullPath}>
|
|
<button type="button" onClick={() => selectImageTab(leaf.fullPath)}>
|
|
<ImageSvg />
|
|
{leaf.name}
|
|
</button>
|
|
</li>
|
|
);
|
|
});
|
|
}
|
|
|
|
export function PhotoTab(): React.JSX.Element {
|
|
const [selectedImage, setSelectedImage] = useState<PhotoData>();
|
|
const query = api.photos.list.useQuery(undefined, {
|
|
refetchOnWindowFocus: false,
|
|
});
|
|
|
|
const {
|
|
register,
|
|
handleSubmit,
|
|
control,
|
|
formState: { errors },
|
|
} = useForm<IFormInput>({
|
|
resolver: zodResolver(FormSchema),
|
|
mode: "onSubmit",
|
|
});
|
|
|
|
if (query.isLoading) {
|
|
return <p>Loading</p>;
|
|
}
|
|
if (query.error) {
|
|
return <p>{query.error.message}</p>;
|
|
}
|
|
const images = query.data?.data;
|
|
if (!images || images?.length === 0) {
|
|
return <p>No Images</p>;
|
|
}
|
|
|
|
const selectImageTab = (path: string) => {
|
|
const img = images.find(
|
|
(img) =>
|
|
img.src === `https://fly.storage.tigris.dev/joemonk-photos/${path}`,
|
|
);
|
|
setSelectedImage(img);
|
|
};
|
|
|
|
const tree = buildDirectoryTree(
|
|
images.map((img) =>
|
|
img.src.substring(
|
|
"https://fly.storage.tigris.dev/joemonk-photos/".length,
|
|
),
|
|
),
|
|
);
|
|
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)}
|
|
</ul>
|
|
<div className="w-3/4 box border border-base-300 p-2">
|
|
{selectedImage?.src ? (
|
|
<form onSubmit={handleSubmit(onSubmit)}>
|
|
<label
|
|
className={`floating-label input text-lg mb-2 w-full ${errors.title ? "input-error" : null}`}
|
|
>
|
|
<span>{`Title ${errors.title ? " - " + errors.title.message : ""}`}</span>
|
|
<input
|
|
{...register("title")}
|
|
type="text"
|
|
placeholder="Title"
|
|
defaultValue={selectedImage?.title}
|
|
/>
|
|
</label>
|
|
<Image
|
|
src={selectedImage.src}
|
|
title={selectedImage?.title}
|
|
alt={selectedImage?.title ?? "Image to modify data for"}
|
|
width={selectedImage.width}
|
|
height={selectedImage.height}
|
|
blurDataURL={selectedImage.blur}
|
|
placeholder="blur"
|
|
/>
|
|
<div className="mt-2 grid grid-cols-3">
|
|
{[
|
|
{
|
|
title: "F-Stop",
|
|
value: selectedImage.exif.fNumber?.toString(),
|
|
},
|
|
{
|
|
title: "ISO",
|
|
value: selectedImage.exif.isoSpeedRatings?.toString(),
|
|
},
|
|
{
|
|
title: "Exposure",
|
|
value: selectedImage.exif.exposureBiasValue?.toString(),
|
|
},
|
|
].map((setting) => {
|
|
return (
|
|
<div key={setting.title} className="w-full border">
|
|
<span className="px-2 w-20 inline-block">
|
|
{setting.title}
|
|
</span>
|
|
<span className="px-2">{setting.value}</span>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
<div className="mt-2 grid grid-cols-3">
|
|
{[
|
|
{
|
|
title: "Taken",
|
|
value: selectedImage.exif.takenAt?.toLocaleDateString(),
|
|
},
|
|
{
|
|
title: "Lens",
|
|
value: selectedImage.exif.LensModel,
|
|
},
|
|
{
|
|
title: "Camera",
|
|
value: selectedImage.camera,
|
|
},
|
|
].map((setting) => {
|
|
return (
|
|
<div key={setting.title} className="w-full border">
|
|
<span className="px-2 w-20 inline-block">
|
|
{setting.title}
|
|
</span>
|
|
<span className="px-2">{setting.value}</span>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
<div className="mt-2 grid grid-cols-2">
|
|
{[
|
|
{
|
|
title: "Height",
|
|
value: selectedImage.height.toString(),
|
|
},
|
|
{
|
|
title: "Width",
|
|
value: selectedImage.width.toString(),
|
|
},
|
|
].map((setting) => {
|
|
return (
|
|
<div key={setting.title} className="w-full border">
|
|
<span className="px-2 w-20 inline-block">
|
|
{setting.title}
|
|
</span>
|
|
<span className="px-2">{setting.value}</span>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
<div className="mt-2 px-2 pb-2 border">
|
|
<span>Description</span>
|
|
|
|
<Controller
|
|
control={control}
|
|
name="description"
|
|
render={({ field: { onChange } }) => (
|
|
<Tiptap onChange={onChange} />
|
|
)}
|
|
/>
|
|
</div>
|
|
<button className="button" type="submit">
|
|
Submit
|
|
</button>
|
|
</form>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|