2025-09-01 10:15:15 +02:00
|
|
|
import type { NextPage } from "next";
|
2025-10-16 11:16:50 +02:00
|
|
|
|
|
|
|
|
/*
|
2025-10-14 16:42:19 +02:00
|
|
|
import { useState } from "react";
|
|
|
|
|
import { useDebounce } from "react-use";
|
|
|
|
|
import z from "zod";
|
|
|
|
|
import Input from "~/components/custom_ui/input";
|
|
|
|
|
import { LoadingPage } from "~/components/loading";
|
2025-09-01 10:15:15 +02:00
|
|
|
import { Button } from "~/components/ui/button";
|
|
|
|
|
|
2025-10-16 11:16:50 +02:00
|
|
|
|
2025-10-14 16:42:19 +02:00
|
|
|
type NominatimResponse = z.infer<typeof responseSchema>;
|
|
|
|
|
export const responseSchema = z.object({
|
|
|
|
|
type: z.string(),
|
|
|
|
|
geocoding: z.object({
|
|
|
|
|
version: z.string(),
|
|
|
|
|
attribution: z.string(),
|
|
|
|
|
licence: z.string(),
|
|
|
|
|
query: z.string(),
|
|
|
|
|
}),
|
|
|
|
|
features: z.array(
|
|
|
|
|
z.object({
|
|
|
|
|
type: z.string(),
|
|
|
|
|
properties: z.object({
|
|
|
|
|
geocoding: z.object({
|
|
|
|
|
place_id: z.number(),
|
|
|
|
|
osm_type: z.string(),
|
|
|
|
|
osm_id: z.number(),
|
|
|
|
|
osm_key: z.string(),
|
|
|
|
|
osm_value: z.string(),
|
|
|
|
|
type: z.string(),
|
|
|
|
|
label: z.string(),
|
|
|
|
|
name: z.string(),
|
|
|
|
|
postcode: z.string().optional(),
|
|
|
|
|
county: z.string().optional(),
|
|
|
|
|
state: z.string().optional(),
|
|
|
|
|
country: z.string(),
|
|
|
|
|
country_code: z.string(),
|
|
|
|
|
admin: z.any(),
|
|
|
|
|
district: z.string().optional(),
|
|
|
|
|
city: z.string().optional(),
|
|
|
|
|
}),
|
|
|
|
|
}),
|
|
|
|
|
geometry: z.object({
|
|
|
|
|
type: z.string(),
|
|
|
|
|
coordinates: z.array(z.number()),
|
|
|
|
|
}),
|
|
|
|
|
}),
|
|
|
|
|
),
|
|
|
|
|
});
|
|
|
|
|
|
2025-09-01 10:15:15 +02:00
|
|
|
const Test: NextPage = () => {
|
2025-10-14 16:42:19 +02:00
|
|
|
const [value, setValue] = useState("");
|
|
|
|
|
const [isLoading, setLoading] = useState(false);
|
|
|
|
|
const [options, setOptions] = useState<NominatimResponse["features"]>([]);
|
|
|
|
|
|
|
|
|
|
useDebounce(
|
|
|
|
|
() => {
|
|
|
|
|
if (value !== "") {
|
|
|
|
|
handleFetch();
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
1000,
|
|
|
|
|
[value],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const handleFetch = () => {
|
|
|
|
|
setLoading(true);
|
|
|
|
|
const api = `https://nominatim.openstreetmap.org/search?format=geocodejson&limit=5&addressdetails=1&q=${encodeURI(
|
|
|
|
|
value,
|
|
|
|
|
)}`;
|
|
|
|
|
fetch(api)
|
|
|
|
|
.then((response) => response.json())
|
|
|
|
|
.then((data) => {
|
|
|
|
|
const parsed = responseSchema.safeParse(data);
|
|
|
|
|
if (parsed.success) {
|
|
|
|
|
const filtered = parsed.data.features.filter(
|
|
|
|
|
(f) =>
|
|
|
|
|
f.properties.geocoding.osm_type === "way" &&
|
|
|
|
|
f.properties.geocoding.type === "street" &&
|
|
|
|
|
f.properties.geocoding.postcode,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Then deduplicate by city
|
|
|
|
|
const uniqueResults = filtered.reduce(
|
|
|
|
|
(acc, current) => {
|
|
|
|
|
const city = current.properties.geocoding.city;
|
|
|
|
|
// If this city doesn't exist or we haven't seen it yet
|
|
|
|
|
if (
|
|
|
|
|
!city ||
|
|
|
|
|
!acc.some((item) => item.properties.geocoding.city === city)
|
|
|
|
|
) {
|
|
|
|
|
acc.push(current);
|
|
|
|
|
}
|
|
|
|
|
return acc;
|
|
|
|
|
},
|
|
|
|
|
[] as typeof filtered,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
setOptions(uniqueResults);
|
|
|
|
|
} else {
|
|
|
|
|
console.error("Failed to fetch location data", parsed.error);
|
|
|
|
|
}
|
|
|
|
|
setLoading(false);
|
|
|
|
|
})
|
|
|
|
|
.catch((error) => {
|
|
|
|
|
console.error(error);
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2025-09-01 10:15:15 +02:00
|
|
|
return (
|
2025-10-14 16:42:19 +02:00
|
|
|
<div className="flex flex-col gap-2 p-4">
|
|
|
|
|
<Input
|
|
|
|
|
onChange={({ currentTarget }) => {
|
|
|
|
|
setValue(currentTarget.value);
|
|
|
|
|
}}
|
|
|
|
|
value={value}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{isLoading ? (
|
|
|
|
|
<LoadingPage />
|
|
|
|
|
) : (
|
|
|
|
|
options.map((option, idx) => (
|
|
|
|
|
<Button
|
|
|
|
|
className="w-full justify-start truncate"
|
|
|
|
|
key={idx}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
console.log(option.properties.geocoding);
|
|
|
|
|
}}
|
|
|
|
|
variant="outline"
|
|
|
|
|
>
|
|
|
|
|
<p>
|
|
|
|
|
{option.properties.geocoding.name} -{" "}
|
|
|
|
|
{option.properties.geocoding.city} -{" "}
|
|
|
|
|
{option.properties.geocoding.postcode} -{" "}
|
|
|
|
|
{option.properties.geocoding.county} -{" "}
|
|
|
|
|
{option.properties.geocoding.country}{" "}
|
|
|
|
|
{option.properties.geocoding.country_code}
|
|
|
|
|
</p>
|
|
|
|
|
</Button>
|
|
|
|
|
))
|
|
|
|
|
)}
|
2025-09-01 10:15:15 +02:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
export default Test;
|
2025-10-16 11:16:50 +02:00
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
const Test: NextPage = () => {
|
2025-10-22 15:41:53 +02:00
|
|
|
return <FileManagementPage />;
|
2025-10-16 11:16:50 +02:00
|
|
|
};
|
|
|
|
|
export default Test;
|
2025-10-22 15:41:53 +02:00
|
|
|
|
|
|
|
|
import { useEffect, useState } from "react";
|
|
|
|
|
import { LoadingPage } from "~/components/loading";
|
|
|
|
|
import { Button } from "~/components/ui/button";
|
|
|
|
|
import { Input } from "~/components/ui/input";
|
|
|
|
|
import {
|
|
|
|
|
deleteFile,
|
|
|
|
|
type FileMetadata,
|
|
|
|
|
fetchFiles,
|
|
|
|
|
uploadFile,
|
|
|
|
|
} from "~/server/storage";
|
|
|
|
|
|
|
|
|
|
function FileManagementPage() {
|
|
|
|
|
const [files, setFiles] = useState<FileMetadata[]>([]);
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
|
|
|
|
|
const loadData = async () => {
|
|
|
|
|
setLoading(true);
|
|
|
|
|
const fileList = await fetchFiles();
|
|
|
|
|
setFiles(fileList);
|
|
|
|
|
setLoading(false);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
loadData();
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const [inputFile, setInputFile] = useState<File | null>(null);
|
|
|
|
|
const [inputExpiresAt, setInputExpiresAt] = useState<string | undefined>(
|
|
|
|
|
undefined,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const handleFileUpload = async () => {
|
|
|
|
|
if (inputFile) {
|
|
|
|
|
const success = await uploadFile(inputFile, inputExpiresAt);
|
|
|
|
|
if (success) {
|
|
|
|
|
// Reload list after successful upload
|
|
|
|
|
await loadData();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleDelete = async (fileID: string) => {
|
|
|
|
|
const confirmDelete = window.confirm("Are you sure?");
|
|
|
|
|
if (confirmDelete) {
|
|
|
|
|
const success = await deleteFile(fileID);
|
|
|
|
|
if (success) {
|
|
|
|
|
await loadData();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex flex-col gap-4 p-4">
|
|
|
|
|
<div className="flex items-center gap-4">
|
|
|
|
|
<h1 className="font-bold text-2xl">File Storage</h1>
|
|
|
|
|
<Button
|
|
|
|
|
onClick={async () => {
|
|
|
|
|
await loadData();
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
Refresh
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<Input
|
|
|
|
|
className=""
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
setInputFile(e.currentTarget.files?.[0] || null);
|
|
|
|
|
}}
|
|
|
|
|
type="file"
|
|
|
|
|
/>
|
|
|
|
|
<Input
|
|
|
|
|
className=""
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
setInputExpiresAt(new Date(e.currentTarget.value).toISOString());
|
|
|
|
|
}}
|
|
|
|
|
type="datetime-local"
|
|
|
|
|
/>
|
|
|
|
|
<Button onClick={handleFileUpload}>Upload</Button>
|
|
|
|
|
{loading ? (
|
|
|
|
|
<LoadingPage />
|
|
|
|
|
) : (
|
|
|
|
|
<ul className="space-y-2">
|
|
|
|
|
{files.map((file) => (
|
|
|
|
|
<li
|
|
|
|
|
className="flex items-center justify-between gap-3 rounded-lg border p-3"
|
|
|
|
|
key={file.id}
|
|
|
|
|
>
|
|
|
|
|
{file.mimeType.includes("image") && (
|
|
|
|
|
<img
|
|
|
|
|
alt={file.originalName}
|
|
|
|
|
className="size-20"
|
2025-10-23 15:16:42 +02:00
|
|
|
src={`/storage-api/file/${file.id}`}
|
2025-10-22 15:41:53 +02:00
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
<div className="flex-1">
|
|
|
|
|
<p className="font-semibold">{file.originalName}</p>
|
|
|
|
|
<p className="text-gray-500 text-sm">
|
|
|
|
|
{file.mimeType} | Size: {(file.size / 1024 / 1024).toFixed(2)}{" "}
|
|
|
|
|
MB
|
|
|
|
|
</p>
|
|
|
|
|
<p>Uploaded At: {new Date(file.uploadedAt).toLocaleString()}</p>
|
|
|
|
|
{file.expiresAt && (
|
|
|
|
|
<p>Expires At: {new Date(file.expiresAt).toLocaleString()}</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex space-x-2">
|
|
|
|
|
<a
|
|
|
|
|
className="rounded bg-blue-500 px-3 py-1 text-sm text-white"
|
2025-10-23 15:16:42 +02:00
|
|
|
href={`/storage-api/file/${file.id}`}
|
2025-10-22 15:41:53 +02:00
|
|
|
>
|
|
|
|
|
Download
|
|
|
|
|
</a>
|
|
|
|
|
<button
|
|
|
|
|
className="rounded bg-red-500 px-3 py-1 text-sm text-white"
|
|
|
|
|
onClick={() => handleDelete(file.id)}
|
|
|
|
|
type="button"
|
|
|
|
|
>
|
|
|
|
|
Delete
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</li>
|
|
|
|
|
))}
|
|
|
|
|
</ul>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|