translation done
This commit is contained in:
parent
ff93e8c5be
commit
5aeb5a0db2
@ -15,11 +15,11 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!user || !user.published) {
|
if (!user || !user.published) {
|
||||||
return { title: "Memorial Not Found" };
|
return { title: "Спомен страницата не е пронајдена" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const title = user.title || "Memorial";
|
const title = user.title || "Спомен";
|
||||||
const description = user.description?.slice(0, 160) || `In Loving Memory of ${title}`;
|
const description = user.description?.slice(0, 160) || `Во спомен на ${title}`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { auth } from "@clerk/nextjs/server";
|
||||||
|
|
||||||
export async function GET(req: NextRequest) {
|
export async function GET(req: NextRequest) {
|
||||||
const slug = req.nextUrl.searchParams.get("slug");
|
const slug = req.nextUrl.searchParams.get("slug");
|
||||||
@ -8,6 +8,7 @@ export async function GET(req: NextRequest) {
|
|||||||
return NextResponse.json({ available: false });
|
return NextResponse.json({ available: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { prisma } = await import("@/lib/prisma");
|
||||||
const existing = await prisma.user.findUnique({ where: { subdomain: slug } });
|
const existing = await prisma.user.findUnique({ where: { subdomain: slug } });
|
||||||
return NextResponse.json({ available: !existing });
|
return NextResponse.json({ available: !existing });
|
||||||
}
|
}
|
||||||
@ -7,7 +7,7 @@ import { getPublicUrl } from "@/lib/upload";
|
|||||||
export async function POST(req: NextRequest) {
|
export async function POST(req: NextRequest) {
|
||||||
const { userId } = await auth();
|
const { userId } = await auth();
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
return NextResponse.json({ error: "Неавторизирано" }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -15,24 +15,24 @@ export async function POST(req: NextRequest) {
|
|||||||
const { title, description, bornDate, passedDate, subdomain, templateId, images } = body;
|
const { title, description, bornDate, passedDate, subdomain, templateId, images } = body;
|
||||||
|
|
||||||
if (!title?.trim()) {
|
if (!title?.trim()) {
|
||||||
return NextResponse.json({ error: "Title is required" }, { status: 400 });
|
return NextResponse.json({ error: "Името е задолжително" }, { status: 400 });
|
||||||
}
|
}
|
||||||
if (!subdomain || subdomain.length < 3) {
|
if (!subdomain || subdomain.length < 3) {
|
||||||
return NextResponse.json({ error: "Subdomain must be at least 3 characters" }, { status: 400 });
|
return NextResponse.json({ error: "Поддоменот мора да има најмалку 3 карактери" }, { status: 400 });
|
||||||
}
|
}
|
||||||
if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$/.test(subdomain)) {
|
if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$/.test(subdomain)) {
|
||||||
return NextResponse.json({ error: "Subdomain can only contain lowercase letters, numbers, and hyphens" }, { status: 400 });
|
return NextResponse.json({ error: "Поддоменот може да содржи само мали букви, цифри и цртички" }, { status: 400 });
|
||||||
}
|
}
|
||||||
if (templateId < 1 || templateId > 3) {
|
if (templateId < 1 || templateId > 3) {
|
||||||
return NextResponse.json({ error: "Invalid template" }, { status: 400 });
|
return NextResponse.json({ error: "Невалиден шаблон" }, { status: 400 });
|
||||||
}
|
}
|
||||||
if (!images || images.length === 0 || images.length > 3) {
|
if (!images || images.length === 0 || images.length > 3) {
|
||||||
return NextResponse.json({ error: "1-3 images required" }, { status: 400 });
|
return NextResponse.json({ error: "Потребни се 1-3 фотографии" }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const existing = await prisma.user.findUnique({ where: { subdomain } });
|
const existing = await prisma.user.findUnique({ where: { subdomain } });
|
||||||
if (existing && existing.clerkId !== userId) {
|
if (existing && existing.clerkId !== userId) {
|
||||||
return NextResponse.json({ error: "Subdomain already taken" }, { status: 409 });
|
return NextResponse.json({ error: "Поддоменот е веќе зафатен" }, { status: 409 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingUser = await prisma.user.findUnique({ where: { clerkId: userId } });
|
const existingUser = await prisma.user.findUnique({ where: { clerkId: userId } });
|
||||||
@ -93,6 +93,6 @@ export async function POST(req: NextRequest) {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Publish error:", error);
|
console.error("Publish error:", error);
|
||||||
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
return NextResponse.json({ error: "Внатрешна грешка на серверот" }, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -24,7 +24,7 @@ function getPublicUrl(key: string): string {
|
|||||||
export async function POST(req: NextRequest) {
|
export async function POST(req: NextRequest) {
|
||||||
const { userId } = await auth();
|
const { userId } = await auth();
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
return NextResponse.json({ error: "Неавторизирано" }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -32,15 +32,15 @@ export async function POST(req: NextRequest) {
|
|||||||
const file = formData.get("file") as File | null;
|
const file = formData.get("file") as File | null;
|
||||||
|
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return NextResponse.json({ error: "No file provided" }, { status: 400 });
|
return NextResponse.json({ error: "Нема подадено датотека" }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ALLOWED_TYPES.includes(file.type)) {
|
if (!ALLOWED_TYPES.includes(file.type)) {
|
||||||
return NextResponse.json({ error: "Invalid file type" }, { status: 400 });
|
return NextResponse.json({ error: "Невалиден тип на датотека" }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file.size > MAX_FILE_SIZE) {
|
if (file.size > MAX_FILE_SIZE) {
|
||||||
return NextResponse.json({ error: "File too large (max 5MB)" }, { status: 400 });
|
return NextResponse.json({ error: "Датотеката е премногу голема (макс. 5MB)" }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const ext = file.type.split("/")[1];
|
const ext = file.type.split("/")[1];
|
||||||
@ -61,6 +61,6 @@ export async function POST(req: NextRequest) {
|
|||||||
return NextResponse.json({ key, url: publicUrl });
|
return NextResponse.json({ key, url: publicUrl });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Upload error:", error);
|
console.error("Upload error:", error);
|
||||||
return NextResponse.json({ error: "Upload failed" }, { status: 500 });
|
return NextResponse.json({ error: "Не успеа качувањето" }, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -9,7 +9,7 @@ export async function DELETE(
|
|||||||
) {
|
) {
|
||||||
const { userId } = await auth();
|
const { userId } = await auth();
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
return NextResponse.json({ error: "Неавторизирано" }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { imageId } = await params;
|
const { imageId } = await params;
|
||||||
@ -21,13 +21,13 @@ export async function DELETE(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!image || image.user.clerkId !== userId) {
|
if (!image || image.user.clerkId !== userId) {
|
||||||
return NextResponse.json({ error: "Image not found" }, { status: 404 });
|
return NextResponse.json({ error: "Фотографијата не е пронајдена" }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await deleteS3Object(image.key);
|
await deleteS3Object(image.key);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to delete S3 object:", image.key, e);
|
console.error("Не успеа бришењето на S3 објект:", image.key, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
await prisma.image.delete({ where: { id: imageId } });
|
await prisma.image.delete({ where: { id: imageId } });
|
||||||
@ -47,6 +47,6 @@ export async function DELETE(
|
|||||||
return NextResponse.json({ success: true });
|
return NextResponse.json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Delete image error:", error);
|
console.error("Delete image error:", error);
|
||||||
return NextResponse.json({ error: "Failed to delete image" }, { status: 500 });
|
return NextResponse.json({ error: "Не успеа бришењето на фотографијата" }, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -6,7 +6,7 @@ import { deleteS3Object } from "@/lib/s3";
|
|||||||
export async function DELETE(req: NextRequest) {
|
export async function DELETE(req: NextRequest) {
|
||||||
const { userId } = await auth();
|
const { userId } = await auth();
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
return NextResponse.json({ error: "Неавторизирано" }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -16,14 +16,14 @@ export async function DELETE(req: NextRequest) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return NextResponse.json({ error: "Monument not found" }, { status: 404 });
|
return NextResponse.json({ error: "Споменот не е пронајден" }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const image of user.images) {
|
for (const image of user.images) {
|
||||||
try {
|
try {
|
||||||
await deleteS3Object(image.key);
|
await deleteS3Object(image.key);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to delete S3 object:", image.key, e);
|
console.error("Не успеа бришењето на S3 објект:", image.key, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,14 +32,14 @@ export async function DELETE(req: NextRequest) {
|
|||||||
return NextResponse.json({ success: true });
|
return NextResponse.json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Delete monument error:", error);
|
console.error("Delete monument error:", error);
|
||||||
return NextResponse.json({ error: "Failed to delete monument" }, { status: 500 });
|
return NextResponse.json({ error: "Не успеа бришењето на споменот" }, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
const { userId } = await auth();
|
const { userId } = await auth();
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
return NextResponse.json({ error: "Неавторизирано" }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
@ -48,7 +48,7 @@ export async function GET() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return NextResponse.json({ error: "Monument not found" }, { status: 404 });
|
return NextResponse.json({ error: "Споменот не е пронајден" }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json({ user });
|
return NextResponse.json({ user });
|
||||||
@ -57,7 +57,7 @@ export async function GET() {
|
|||||||
export async function PUT(req: NextRequest) {
|
export async function PUT(req: NextRequest) {
|
||||||
const { userId } = await auth();
|
const { userId } = await auth();
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
return NextResponse.json({ error: "Неавторизирано" }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -80,6 +80,6 @@ export async function PUT(req: NextRequest) {
|
|||||||
return NextResponse.json({ user });
|
return NextResponse.json({ user });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Update error:", error);
|
console.error("Update error:", error);
|
||||||
return NextResponse.json({ error: "Failed to update monument" }, { status: 500 });
|
return NextResponse.json({ error: "Не успеа ажурирањето на споменот" }, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -72,7 +72,7 @@ export default function EditPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <div className="flex min-h-screen items-center justify-center">Loading...</div>;
|
return <div className="flex min-h-screen items-center justify-center">Вчитување...</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const imagePreviews = images.map((img) => ({ url: img.url, order: img.order }));
|
const imagePreviews = images.map((img) => ({ url: img.url, order: img.order }));
|
||||||
@ -81,23 +81,22 @@ export default function EditPage() {
|
|||||||
<div className="min-h-screen bg-stone-50">
|
<div className="min-h-screen bg-stone-50">
|
||||||
<header className="border-b border-stone-200 bg-white">
|
<header className="border-b border-stone-200 bg-white">
|
||||||
<div className="mx-auto max-w-6xl px-6 py-4 flex items-center justify-between">
|
<div className="mx-auto max-w-6xl px-6 py-4 flex items-center justify-between">
|
||||||
<h1 className="text-xl font-semibold text-stone-900">Edit Memorial</h1>
|
<h1 className="text-xl font-semibold text-stone-900">Уреди спомен</h1>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowPreview(!showPreview)}
|
onClick={() => setShowPreview(!showPreview)}
|
||||||
className="rounded-lg border border-stone-200 px-4 py-2 text-sm font-medium text-stone-700 transition-colors hover:bg-stone-50 lg:hidden"
|
className="rounded-lg border border-stone-200 px-4 py-2 text-sm font-medium text-stone-700 transition-colors hover:bg-stone-50 lg:hidden"
|
||||||
>
|
>
|
||||||
{showPreview ? "Edit" : "Preview"}
|
{showPreview ? "Уреди" : "Преглед"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="mx-auto max-w-6xl px-6 py-8">
|
<div className="mx-auto max-w-6xl px-6 py-8">
|
||||||
<div className="flex gap-8">
|
<div className="flex gap-8">
|
||||||
{/* Form - hidden on mobile when preview is shown */}
|
|
||||||
<div className={`flex-1 min-w-0 ${showPreview ? "hidden lg:block" : ""}`}>
|
<div className={`flex-1 min-w-0 ${showPreview ? "hidden lg:block" : ""}`}>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="title" className="block text-sm font-medium text-stone-700">Name</label>
|
<label htmlFor="title" className="block text-sm font-medium text-stone-700">Име</label>
|
||||||
<input
|
<input
|
||||||
id="title"
|
id="title"
|
||||||
type="text"
|
type="text"
|
||||||
@ -110,25 +109,25 @@ export default function EditPage() {
|
|||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="bornDate" className="block text-sm font-medium text-stone-700">Born</label>
|
<label htmlFor="bornDate" className="block text-sm font-medium text-stone-700">Роден/а</label>
|
||||||
<input
|
<input
|
||||||
id="bornDate"
|
id="bornDate"
|
||||||
type="text"
|
type="text"
|
||||||
value={data.bornDate}
|
value={data.bornDate}
|
||||||
onChange={(e) => setData({ ...data, bornDate: e.target.value })}
|
onChange={(e) => setData({ ...data, bornDate: e.target.value })}
|
||||||
placeholder="e.g. 1960"
|
placeholder="нпр. 1960"
|
||||||
className="mt-1 block w-full rounded-lg border border-stone-200 px-3 py-2 text-stone-900 placeholder:text-stone-400 focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"
|
className="mt-1 block w-full rounded-lg border border-stone-200 px-3 py-2 text-stone-900 placeholder:text-stone-400 focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"
|
||||||
maxLength={50}
|
maxLength={50}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="passedDate" className="block text-sm font-medium text-stone-700">Passed Away</label>
|
<label htmlFor="passedDate" className="block text-sm font-medium text-stone-700">Починат/а</label>
|
||||||
<input
|
<input
|
||||||
id="passedDate"
|
id="passedDate"
|
||||||
type="text"
|
type="text"
|
||||||
value={data.passedDate}
|
value={data.passedDate}
|
||||||
onChange={(e) => setData({ ...data, passedDate: e.target.value })}
|
onChange={(e) => setData({ ...data, passedDate: e.target.value })}
|
||||||
placeholder="e.g. 2024"
|
placeholder="нпр. 2024"
|
||||||
className="mt-1 block w-full rounded-lg border border-stone-200 px-3 py-2 text-stone-900 placeholder:text-stone-400 focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"
|
className="mt-1 block w-full rounded-lg border border-stone-200 px-3 py-2 text-stone-900 placeholder:text-stone-400 focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"
|
||||||
maxLength={50}
|
maxLength={50}
|
||||||
/>
|
/>
|
||||||
@ -136,7 +135,7 @@ export default function EditPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="description" className="block text-sm font-medium text-stone-700">Epitaph / Life Story</label>
|
<label htmlFor="description" className="block text-sm font-medium text-stone-700">Епитафија / Животна приказна</label>
|
||||||
<textarea
|
<textarea
|
||||||
id="description"
|
id="description"
|
||||||
value={data.description}
|
value={data.description}
|
||||||
@ -148,12 +147,12 @@ export default function EditPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-stone-700 mb-2">Template</label>
|
<label className="block text-sm font-medium text-stone-700 mb-2">Шаблон</label>
|
||||||
<div className="grid grid-cols-3 gap-3">
|
<div className="grid grid-cols-3 gap-3">
|
||||||
{[
|
{[
|
||||||
{ id: 1, name: "Elegance" },
|
{ id: 1, name: "Елеганција" },
|
||||||
{ id: 2, name: "Cinematic" },
|
{ id: 2, name: "Кинематски" },
|
||||||
{ id: 3, name: "Serene" },
|
{ id: 3, name: "Спокој" },
|
||||||
].map((t) => (
|
].map((t) => (
|
||||||
<button
|
<button
|
||||||
key={t.id}
|
key={t.id}
|
||||||
@ -171,7 +170,7 @@ export default function EditPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-stone-700 mb-2">Photos</label>
|
<label className="block text-sm font-medium text-stone-700 mb-2">Фотографии</label>
|
||||||
{images.length > 0 ? (
|
{images.length > 0 ? (
|
||||||
<div className="grid grid-cols-3 gap-3">
|
<div className="grid grid-cols-3 gap-3">
|
||||||
{images.map((img) => (
|
{images.map((img) => (
|
||||||
@ -187,7 +186,7 @@ export default function EditPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-sm text-stone-400">No photos yet.</p>
|
<p className="text-sm text-stone-400">Нема фотографии.</p>
|
||||||
)}
|
)}
|
||||||
{images.length < 3 && (
|
{images.length < 3 && (
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
@ -201,19 +200,18 @@ export default function EditPage() {
|
|||||||
|
|
||||||
<div className="flex gap-3 pt-4">
|
<div className="flex gap-3 pt-4">
|
||||||
<button onClick={handleSave} disabled={saving} className="rounded-lg bg-primary px-6 py-2 text-sm font-medium text-white transition-colors hover:bg-primary-light disabled:opacity-50">
|
<button onClick={handleSave} disabled={saving} className="rounded-lg bg-primary px-6 py-2 text-sm font-medium text-white transition-colors hover:bg-primary-light disabled:opacity-50">
|
||||||
{saving ? "Saving..." : "Save Changes"}
|
{saving ? "Зачувување..." : "Зачувај промени"}
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => router.push("/dashboard")} className="rounded-lg border border-stone-200 px-6 py-2 text-sm font-medium text-stone-700 transition-colors hover:bg-stone-50">
|
<button onClick={() => router.push("/dashboard")} className="rounded-lg border border-stone-200 px-6 py-2 text-sm font-medium text-stone-700 transition-colors hover:bg-stone-50">
|
||||||
Cancel
|
Откажи
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Live preview - always visible on desktop, toggle on mobile */}
|
|
||||||
<div className={`w-[420px] flex-shrink-0 ${showPreview ? "" : "hidden lg:block"}`}>
|
<div className={`w-[420px] flex-shrink-0 ${showPreview ? "" : "hidden lg:block"}`}>
|
||||||
<div className="sticky top-8">
|
<div className="sticky top-8">
|
||||||
<p className="mb-2 text-xs font-medium text-stone-500 uppercase tracking-wider">Live Preview</p>
|
<p className="mb-2 text-xs font-medium text-stone-500 uppercase tracking-wider">Преглед во живо</p>
|
||||||
<div className="overflow-hidden rounded-lg border border-stone-300 shadow-md">
|
<div className="overflow-hidden rounded-lg border border-stone-300 shadow-md">
|
||||||
<div className="h-[600px] overflow-y-auto">
|
<div className="h-[600px] overflow-y-auto">
|
||||||
<TemplatePicker
|
<TemplatePicker
|
||||||
|
|||||||
@ -26,21 +26,21 @@ export default async function DashboardPage() {
|
|||||||
<div className="min-h-screen bg-stone-50">
|
<div className="min-h-screen bg-stone-50">
|
||||||
<header className="border-b border-stone-200 bg-white">
|
<header className="border-b border-stone-200 bg-white">
|
||||||
<div className="mx-auto flex max-w-5xl items-center justify-between px-6 py-4">
|
<div className="mx-auto flex max-w-5xl items-center justify-between px-6 py-4">
|
||||||
<h1 className="text-xl font-semibold text-stone-900">SpomeniQR</h1>
|
<h1 className="text-xl font-semibold text-stone-900">СпоменQR</h1>
|
||||||
<UserButton />
|
<UserButton />
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main className="mx-auto max-w-5xl px-6 py-8">
|
<main className="mx-auto max-w-5xl px-6 py-8">
|
||||||
<div className="rounded-lg border border-stone-200 bg-white p-6">
|
<div className="rounded-lg border border-stone-200 bg-white p-6">
|
||||||
<h2 className="text-2xl font-semibold text-stone-900">{user.title || "Untitled Memorial"}</h2>
|
<h2 className="text-2xl font-semibold text-stone-900">{user.title || "Неименуван спомен"}</h2>
|
||||||
{(user.bornDate || user.passedDate) && (
|
{(user.bornDate || user.passedDate) && (
|
||||||
<p className="mt-1 text-sm tracking-wider text-stone-400">
|
<p className="mt-1 text-sm tracking-wider text-stone-400">
|
||||||
{user.bornDate}{user.bornDate && user.passedDate ? " — " : ""}{user.passedDate}
|
{user.bornDate}{user.bornDate && user.passedDate ? " — " : ""}{user.passedDate}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<p className="mt-2 text-sm text-stone-500">
|
<p className="mt-2 text-sm text-stone-500">
|
||||||
{user.published ? "Published" : "Draft"} · Subdomain: <span className="font-mono">{user.subdomain}</span>
|
{user.published ? "Објавено" : "Нацрт"} · Поддомен: <span className="font-mono">{user.subdomain}</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="mt-6 flex flex-wrap gap-3">
|
<div className="mt-6 flex flex-wrap gap-3">
|
||||||
@ -49,20 +49,20 @@ export default async function DashboardPage() {
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
className="rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-primary-light"
|
className="rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-primary-light"
|
||||||
>
|
>
|
||||||
View Monument
|
Погледај спомен
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href="/dashboard/edit"
|
href="/dashboard/edit"
|
||||||
className="rounded-lg border border-stone-200 px-4 py-2 text-sm font-medium text-stone-700 transition-colors hover:bg-stone-50"
|
className="rounded-lg border border-stone-200 px-4 py-2 text-sm font-medium text-stone-700 transition-colors hover:bg-stone-50"
|
||||||
>
|
>
|
||||||
Edit
|
Уреди
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{user.images.length > 0 && (
|
{user.images.length > 0 && (
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<h3 className="text-lg font-medium text-stone-900">Images</h3>
|
<h3 className="text-lg font-medium text-stone-900">Фотографии</h3>
|
||||||
<div className="mt-3 grid grid-cols-3 gap-4">
|
<div className="mt-3 grid grid-cols-3 gap-4">
|
||||||
{user.images.map((img) => (
|
{user.images.map((img) => (
|
||||||
<div key={img.id} className="group relative overflow-hidden rounded-lg border border-stone-200">
|
<div key={img.id} className="group relative overflow-hidden rounded-lg border border-stone-200">
|
||||||
@ -75,12 +75,12 @@ export default async function DashboardPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<h3 className="text-lg font-medium text-stone-900">Monument QR Code</h3>
|
<h3 className="text-lg font-medium text-stone-900">QR код</h3>
|
||||||
<p className="mt-1 text-sm text-stone-500">Download and display this QR code at your monument location.</p>
|
<p className="mt-1 text-sm text-stone-500">Прикажете го овој QR код на споменикот за да можат посетителите да ја прочитаат приказната.</p>
|
||||||
<div className="mt-4 rounded-lg border border-stone-200 bg-white p-4">
|
<div className="mt-4 rounded-lg border border-stone-200 bg-white p-4">
|
||||||
<img
|
<img
|
||||||
src={`https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(monumentUrl)}`}
|
src={`https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(monumentUrl)}`}
|
||||||
alt="QR Code"
|
alt="QR код"
|
||||||
className="h-48 w-48"
|
className="h-48 w-48"
|
||||||
/>
|
/>
|
||||||
<a
|
<a
|
||||||
@ -88,13 +88,13 @@ export default async function DashboardPage() {
|
|||||||
download
|
download
|
||||||
className="mt-4 inline-block rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-primary-light"
|
className="mt-4 inline-block rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-primary-light"
|
||||||
>
|
>
|
||||||
Download QR
|
Превземи QR код
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<h3 className="text-lg font-medium text-stone-900">Share Link</h3>
|
<h3 className="text-lg font-medium text-stone-900">Сподели линк</h3>
|
||||||
<div className="mt-2 flex items-center gap-2">
|
<div className="mt-2 flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@ -107,8 +107,8 @@ export default async function DashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-10 border-t border-stone-200 pt-6">
|
<div className="mt-10 border-t border-stone-200 pt-6">
|
||||||
<h3 className="text-sm font-medium text-red-600">Danger Zone</h3>
|
<h3 className="text-sm font-medium text-red-600">Опасна зона</h3>
|
||||||
<p className="mt-1 text-xs text-stone-500">Permanently delete your monument and all associated images.</p>
|
<p className="mt-1 text-xs text-stone-500">Трачно избришете го вашиот спомен и сите поврзани фотографии.</p>
|
||||||
<div className="mt-3">
|
<div className="mt-3">
|
||||||
<DeleteMonumentButton />
|
<DeleteMonumentButton />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -14,8 +14,8 @@ const geistMono = Geist_Mono({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "SpomeniQR — In Loving Memory",
|
title: "СпоменQR — Во спомен на",
|
||||||
description: "Create beautiful memorial pages with QR codes. Honor and remember those who meant the world to you.",
|
description: "Креирајте убави спомен страници со QR кодови. Оддадете почит и зачувајте ги спомените на најблиските.",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
@ -25,7 +25,7 @@ export default function RootLayout({
|
|||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<ClerkProvider>
|
<ClerkProvider>
|
||||||
<html lang="en" className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}>
|
<html lang="mk" className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}>
|
||||||
<body className="min-h-full flex flex-col">{children}</body>
|
<body className="min-h-full flex flex-col">{children}</body>
|
||||||
</html>
|
</html>
|
||||||
</ClerkProvider>
|
</ClerkProvider>
|
||||||
|
|||||||
@ -1,16 +1,21 @@
|
|||||||
import Link from "next/link";
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "СпоменQR — Во спомен на",
|
||||||
|
description: "Креирајте убави спомен страници со QR кодови. Оддадете почит и зачувајте ги спомените на најблиските.",
|
||||||
|
};
|
||||||
|
|
||||||
export default function NotFound() {
|
export default function NotFound() {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen flex-col items-center justify-center px-6">
|
<div className="flex min-h-screen flex-col items-center justify-center px-6">
|
||||||
<h1 className="text-4xl font-bold text-stone-900">Memorial Not Found</h1>
|
<h1 className="text-4xl font-bold text-stone-900">Спомен страницата не е пронајдена</h1>
|
||||||
<p className="mt-4 text-stone-600">This memorial page does not exist or has not been published yet.</p>
|
<p className="mt-4 text-stone-600">Оваа спомен страница не постои или сè уште не е објавена.</p>
|
||||||
<Link
|
<a
|
||||||
href="/"
|
href="/"
|
||||||
className="mt-8 rounded-lg bg-primary px-6 py-2 text-sm font-medium text-white transition-colors hover:bg-primary-light"
|
className="mt-8 rounded-lg bg-primary px-6 py-2 text-sm font-medium text-white transition-colors hover:bg-primary-light"
|
||||||
>
|
>
|
||||||
Go Home
|
Почетна
|
||||||
</Link>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -6,7 +6,7 @@ import ImageUploader from "@/components/ImageUploader";
|
|||||||
import SubdomainPicker from "@/components/SubdomainPicker";
|
import SubdomainPicker from "@/components/SubdomainPicker";
|
||||||
import TemplatePicker from "@/components/TemplatePicker";
|
import TemplatePicker from "@/components/TemplatePicker";
|
||||||
|
|
||||||
const STEPS = ["Details", "Dates", "Photos", "Subdomain", "Template"] as const;
|
const STEPS = ["Податоци", "Датуми", "Фотографии", "Поддомен", "Шаблон"] as const;
|
||||||
|
|
||||||
export default function OnboardingWizard() {
|
export default function OnboardingWizard() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -52,11 +52,11 @@ export default function OnboardingWizard() {
|
|||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
throw new Error(data.error || "Failed to publish");
|
throw new Error(data.error || "Не успеа објавувањето");
|
||||||
}
|
}
|
||||||
router.push("/dashboard");
|
router.push("/dashboard");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : "Something went wrong");
|
setError(err instanceof Error ? err.message : "Нешто тргна наопаку");
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@ -70,7 +70,7 @@ export default function OnboardingWizard() {
|
|||||||
<div className="min-h-screen bg-stone-50">
|
<div className="min-h-screen bg-stone-50">
|
||||||
<header className="border-b border-stone-200 bg-white">
|
<header className="border-b border-stone-200 bg-white">
|
||||||
<div className="mx-auto max-w-2xl px-6 py-4">
|
<div className="mx-auto max-w-2xl px-6 py-4">
|
||||||
<h1 className="text-xl font-semibold text-stone-900">Create a Memorial Page</h1>
|
<h1 className="text-xl font-semibold text-stone-900">Креирај спомен страница</h1>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@ -103,27 +103,27 @@ export default function OnboardingWizard() {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="title" className="block text-sm font-medium text-stone-700">
|
<label htmlFor="title" className="block text-sm font-medium text-stone-700">
|
||||||
Name
|
Име
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="title"
|
id="title"
|
||||||
type="text"
|
type="text"
|
||||||
value={title}
|
value={title}
|
||||||
onChange={(e) => setTitle(e.target.value)}
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
placeholder="e.g. Maria Novakova"
|
placeholder="нпр. Марија Новаковска"
|
||||||
className="mt-1 block w-full rounded-lg border border-stone-200 px-3 py-2.5 text-stone-900 placeholder:text-stone-400 focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"
|
className="mt-1 block w-full rounded-lg border border-stone-200 px-3 py-2.5 text-stone-900 placeholder:text-stone-400 focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"
|
||||||
maxLength={100}
|
maxLength={100}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="description" className="block text-sm font-medium text-stone-700">
|
<label htmlFor="description" className="block text-sm font-medium text-stone-700">
|
||||||
Epitaph / Life Story
|
Епитафија / Животна приказна
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
id="description"
|
id="description"
|
||||||
value={description}
|
value={description}
|
||||||
onChange={(e) => setDescription(e.target.value)}
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
placeholder="A few words, a poem, or a biography to honor their memory..."
|
placeholder="Неколку зборови, песма или биографија во нивна чест..."
|
||||||
rows={6}
|
rows={6}
|
||||||
className="mt-1 block w-full rounded-lg border border-stone-200 px-3 py-2.5 text-stone-900 placeholder:text-stone-400 focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"
|
className="mt-1 block w-full rounded-lg border border-stone-200 px-3 py-2.5 text-stone-900 placeholder:text-stone-400 focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"
|
||||||
maxLength={2000}
|
maxLength={2000}
|
||||||
@ -135,32 +135,32 @@ export default function OnboardingWizard() {
|
|||||||
{step === 1 && (
|
{step === 1 && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<p className="text-sm text-stone-500">
|
<p className="text-sm text-stone-500">
|
||||||
These are optional. You can enter exact dates, approximate years, or leave them blank.
|
Овие се опционални. Можете да внесете точни датуми, приближни години, или да ги оставите празни.
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="bornDate" className="block text-sm font-medium text-stone-700">
|
<label htmlFor="bornDate" className="block text-sm font-medium text-stone-700">
|
||||||
Born
|
Роден/а
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="bornDate"
|
id="bornDate"
|
||||||
type="text"
|
type="text"
|
||||||
value={bornDate}
|
value={bornDate}
|
||||||
onChange={(e) => setBornDate(e.target.value)}
|
onChange={(e) => setBornDate(e.target.value)}
|
||||||
placeholder="e.g. 1960 or March 15, 1960"
|
placeholder="нпр. 1960 или 15 март 1960"
|
||||||
className="mt-1 block w-full rounded-lg border border-stone-200 px-3 py-2.5 text-stone-900 placeholder:text-stone-400 focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"
|
className="mt-1 block w-full rounded-lg border border-stone-200 px-3 py-2.5 text-stone-900 placeholder:text-stone-400 focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"
|
||||||
maxLength={50}
|
maxLength={50}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="passedDate" className="block text-sm font-medium text-stone-700">
|
<label htmlFor="passedDate" className="block text-sm font-medium text-stone-700">
|
||||||
Passed Away
|
Починат/а
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="passedDate"
|
id="passedDate"
|
||||||
type="text"
|
type="text"
|
||||||
value={passedDate}
|
value={passedDate}
|
||||||
onChange={(e) => setPassedDate(e.target.value)}
|
onChange={(e) => setPassedDate(e.target.value)}
|
||||||
placeholder="e.g. 2024 or November 20, 2024"
|
placeholder="нпр. 2024 или 20 ноември 2024"
|
||||||
className="mt-1 block w-full rounded-lg border border-stone-200 px-3 py-2.5 text-stone-900 placeholder:text-stone-400 focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"
|
className="mt-1 block w-full rounded-lg border border-stone-200 px-3 py-2.5 text-stone-900 placeholder:text-stone-400 focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"
|
||||||
maxLength={50}
|
maxLength={50}
|
||||||
/>
|
/>
|
||||||
@ -197,7 +197,7 @@ export default function OnboardingWizard() {
|
|||||||
onClick={() => setStep(step - 1)}
|
onClick={() => setStep(step - 1)}
|
||||||
className="rounded-lg border border-stone-200 px-6 py-2 text-sm font-medium text-stone-700 transition-colors hover:bg-stone-50"
|
className="rounded-lg border border-stone-200 px-6 py-2 text-sm font-medium text-stone-700 transition-colors hover:bg-stone-50"
|
||||||
>
|
>
|
||||||
Back
|
Назад
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<div />
|
<div />
|
||||||
@ -208,7 +208,7 @@ export default function OnboardingWizard() {
|
|||||||
disabled={!canProceed()}
|
disabled={!canProceed()}
|
||||||
className="rounded-lg bg-primary px-6 py-2 text-sm font-medium text-white transition-colors hover:bg-primary-light disabled:cursor-not-allowed disabled:opacity-50"
|
className="rounded-lg bg-primary px-6 py-2 text-sm font-medium text-white transition-colors hover:bg-primary-light disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
>
|
>
|
||||||
Continue
|
Продолжи
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
@ -216,7 +216,7 @@ export default function OnboardingWizard() {
|
|||||||
disabled={!canProceed() || loading}
|
disabled={!canProceed() || loading}
|
||||||
className="rounded-lg bg-primary px-6 py-2 text-sm font-medium text-white transition-colors hover:bg-primary-light disabled:cursor-not-allowed disabled:opacity-50"
|
className="rounded-lg bg-primary px-6 py-2 text-sm font-medium text-white transition-colors hover:bg-primary-light disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{loading ? "Publishing..." : "Publish Memorial"}
|
{loading ? "Објавување..." : "Објави спомен"}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -7,7 +7,7 @@ export default async function HomePage() {
|
|||||||
<header className="border-b border-stone-200 bg-white">
|
<header className="border-b border-stone-200 bg-white">
|
||||||
<div className="mx-auto flex max-w-5xl items-center justify-between px-6 py-4">
|
<div className="mx-auto flex max-w-5xl items-center justify-between px-6 py-4">
|
||||||
<Link href="/" className="text-xl font-bold text-primary">
|
<Link href="/" className="text-xl font-bold text-primary">
|
||||||
SpomeniQR
|
СпоменQR
|
||||||
</Link>
|
</Link>
|
||||||
<NavAuth />
|
<NavAuth />
|
||||||
</div>
|
</div>
|
||||||
@ -17,10 +17,10 @@ export default async function HomePage() {
|
|||||||
<div className="mx-auto max-w-3xl text-center">
|
<div className="mx-auto max-w-3xl text-center">
|
||||||
<div className="mb-6 text-5xl">💎</div>
|
<div className="mb-6 text-5xl">💎</div>
|
||||||
<h2 className="text-4xl font-bold tracking-tight text-stone-900 sm:text-5xl">
|
<h2 className="text-4xl font-bold tracking-tight text-stone-900 sm:text-5xl">
|
||||||
In Loving Memory
|
Во спомен на
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-4 text-lg text-stone-600">
|
<p className="mt-4 text-lg text-stone-600">
|
||||||
Create beautiful memorial pages with QR codes. Honor and remember those who meant the world to you.
|
Креирајте убави спомен страници со QR кодови. Оддадете почит и зачувајте ги спомените на најблиските.
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-8">
|
<div className="mt-8">
|
||||||
<LandingAuth />
|
<LandingAuth />
|
||||||
@ -29,25 +29,25 @@ export default async function HomePage() {
|
|||||||
<div className="mt-16 grid grid-cols-1 gap-8 text-left sm:grid-cols-3">
|
<div className="mt-16 grid grid-cols-1 gap-8 text-left sm:grid-cols-3">
|
||||||
<div className="rounded-lg border border-stone-200 bg-white p-6">
|
<div className="rounded-lg border border-stone-200 bg-white p-6">
|
||||||
<div className="mb-3 text-2xl">✍</div>
|
<div className="mb-3 text-2xl">✍</div>
|
||||||
<h3 className="font-semibold text-stone-900">Tell Their Story</h3>
|
<h3 className="font-semibold text-stone-900">Раскажете нивната приказна</h3>
|
||||||
<p className="mt-1 text-sm text-stone-500">Write an epitaph, biography, or poem to honor their memory.</p>
|
<p className="mt-1 text-sm text-stone-500">Напишете епитафија, биографија или песма во нивна чест.</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-lg border border-stone-200 bg-white p-6">
|
<div className="rounded-lg border border-stone-200 bg-white p-6">
|
||||||
<div className="mb-3 text-2xl">📷</div>
|
<div className="mb-3 text-2xl">📷</div>
|
||||||
<h3 className="font-semibold text-stone-900">Share Photos</h3>
|
<h3 className="font-semibold text-stone-900">Споделете фотографии</h3>
|
||||||
<p className="mt-1 text-sm text-stone-500">Add up to 3 photos that capture their spirit and legacy.</p>
|
<p className="mt-1 text-sm text-stone-500">Додадете до 3 фотографии што го прикажуваат нивниот дух и наслед.</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-lg border border-stone-200 bg-white p-6">
|
<div className="rounded-lg border border-stone-200 bg-white p-6">
|
||||||
<div className="mb-3 text-2xl">📱</div>
|
<div className="mb-3 text-2xl">📱</div>
|
||||||
<h3 className="font-semibold text-stone-900">QR Code at the Grave</h3>
|
<h3 className="font-semibold text-stone-900">QR код на споменикот</h3>
|
||||||
<p className="mt-1 text-sm text-stone-500">Visitors scan the QR code to read their story, right at the memorial site.</p>
|
<p className="mt-1 text-sm text-stone-500">Посетителите скенираат QR код за да ја прочитаат нивната приказна, директно на споменикот.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer className="border-t border-stone-200 py-6 text-center text-sm text-stone-400">
|
<footer className="border-t border-stone-200 py-6 text-center text-sm text-stone-400">
|
||||||
SpomeniQR — In Loving Memory
|
СпоменQR — Во спомен на
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,12 +1,20 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
export default function CopyButton({ text }: { text: string }) {
|
export default function CopyButton({ text }: { text: string }) {
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={() => navigator.clipboard.writeText(text)}
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(text);
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
}}
|
||||||
className="rounded-lg border border-stone-200 px-4 py-2 text-sm font-medium text-stone-700 transition-colors hover:bg-stone-50"
|
className="rounded-lg border border-stone-200 px-4 py-2 text-sm font-medium text-stone-700 transition-colors hover:bg-stone-50"
|
||||||
>
|
>
|
||||||
Copy
|
{copied ? "Копирано!" : "Копирај"}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -19,10 +19,10 @@ export default function DeleteImageButton({ imageId }: DeleteImageButtonProps) {
|
|||||||
router.refresh();
|
router.refresh();
|
||||||
} else {
|
} else {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
alert(data.error || "Failed to delete image");
|
alert(data.error || "Не успеа бришењето");
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
alert("Something went wrong");
|
alert("Нешто тргна наопаку");
|
||||||
} finally {
|
} finally {
|
||||||
setDeleting(false);
|
setDeleting(false);
|
||||||
}
|
}
|
||||||
@ -33,7 +33,7 @@ export default function DeleteImageButton({ imageId }: DeleteImageButtonProps) {
|
|||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
disabled={deleting}
|
disabled={deleting}
|
||||||
className="absolute right-1 top-1 flex h-6 w-6 items-center justify-center rounded-full bg-red-500 text-xs text-white opacity-0 transition-opacity group-hover:opacity-100 disabled:opacity-50"
|
className="absolute right-1 top-1 flex h-6 w-6 items-center justify-center rounded-full bg-red-500 text-xs text-white opacity-0 transition-opacity group-hover:opacity-100 disabled:opacity-50"
|
||||||
title="Delete image"
|
title="Избриши фотографија"
|
||||||
>
|
>
|
||||||
×
|
×
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -17,10 +17,10 @@ export default function DeleteMonumentButton() {
|
|||||||
router.refresh();
|
router.refresh();
|
||||||
} else {
|
} else {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
alert(data.error || "Failed to delete monument");
|
alert(data.error || "Не успеа бришењето на споменот");
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
alert("Something went wrong");
|
alert("Нешто тргна наопаку");
|
||||||
} finally {
|
} finally {
|
||||||
setDeleting(false);
|
setDeleting(false);
|
||||||
setShowConfirm(false);
|
setShowConfirm(false);
|
||||||
@ -30,20 +30,20 @@ export default function DeleteMonumentButton() {
|
|||||||
if (showConfirm) {
|
if (showConfirm) {
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg border border-red-200 bg-red-50 p-4">
|
<div className="rounded-lg border border-red-200 bg-red-50 p-4">
|
||||||
<p className="text-sm font-medium text-red-800">Are you sure? This will permanently delete your monument and all its images.</p>
|
<p className="text-sm font-medium text-red-800">Дали сте сигурни? Ова ќе го избрише вашиот спомен и сите фотографии трајно.</p>
|
||||||
<div className="mt-3 flex gap-2">
|
<div className="mt-3 flex gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
disabled={deleting}
|
disabled={deleting}
|
||||||
className="rounded-lg bg-red-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-red-700 disabled:opacity-50"
|
className="rounded-lg bg-red-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-red-700 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{deleting ? "Deleting..." : "Yes, delete forever"}
|
{deleting ? "Бришење..." : "Да, избриши трајно"}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowConfirm(false)}
|
onClick={() => setShowConfirm(false)}
|
||||||
className="rounded-lg border border-stone-200 bg-white px-4 py-2 text-sm font-medium text-stone-700 transition-colors hover:bg-stone-50"
|
className="rounded-lg border border-stone-200 bg-white px-4 py-2 text-sm font-medium text-stone-700 transition-colors hover:bg-stone-50"
|
||||||
>
|
>
|
||||||
Cancel
|
Откажи
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -55,7 +55,7 @@ export default function DeleteMonumentButton() {
|
|||||||
onClick={() => setShowConfirm(true)}
|
onClick={() => setShowConfirm(true)}
|
||||||
className="rounded-lg border border-red-200 px-4 py-2 text-sm font-medium text-red-600 transition-colors hover:bg-red-50"
|
className="rounded-lg border border-red-200 px-4 py-2 text-sm font-medium text-red-600 transition-colors hover:bg-red-50"
|
||||||
>
|
>
|
||||||
Delete Monument
|
Избриши спомен
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -14,8 +14,8 @@ interface ImageData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ImageUploaderProps {
|
interface ImageUploaderProps {
|
||||||
images: { key: string; order: number }[];
|
images: { key: string; order: number; url?: string }[];
|
||||||
onImagesChange: (images: { key: string; order: number }[]) => void;
|
onImagesChange: (images: { key: string; order: number; url?: string }[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ImageUploader({ images, onImagesChange }: ImageUploaderProps) {
|
export default function ImageUploader({ images, onImagesChange }: ImageUploaderProps) {
|
||||||
@ -24,8 +24,10 @@ export default function ImageUploader({ images, onImagesChange }: ImageUploaderP
|
|||||||
const [previews, setPreviews] = useState<ImageData[]>([]);
|
const [previews, setPreviews] = useState<ImageData[]>([]);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
|
const currentCount = images.length;
|
||||||
|
|
||||||
const handleFiles = async (files: FileList) => {
|
const handleFiles = async (files: FileList) => {
|
||||||
const remaining = MAX_FILES - images.length;
|
const remaining = MAX_FILES - currentCount;
|
||||||
if (remaining <= 0) return;
|
if (remaining <= 0) return;
|
||||||
|
|
||||||
const validFiles = Array.from(files)
|
const validFiles = Array.from(files)
|
||||||
@ -34,7 +36,7 @@ export default function ImageUploader({ images, onImagesChange }: ImageUploaderP
|
|||||||
.slice(0, remaining);
|
.slice(0, remaining);
|
||||||
|
|
||||||
if (validFiles.length === 0) {
|
if (validFiles.length === 0) {
|
||||||
setError("Invalid file type or size. Accepted: JPEG, PNG, WebP, GIF up to 5MB.");
|
setError("Невалиден тип на датотека или големина. Дозволени: JPEG, PNG, WebP, GIF до 5MB.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,53 +58,48 @@ export default function ImageUploader({ images, onImagesChange }: ImageUploaderP
|
|||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
throw new Error(data.error || "Upload failed");
|
throw new Error(data.error || "Не успеа качувањето");
|
||||||
}
|
}
|
||||||
|
|
||||||
const { key, url } = await res.json();
|
const { key, url } = await res.json();
|
||||||
const order = newImages.length + 1;
|
const order = newImages.length + 1;
|
||||||
newImages.push({ key, order });
|
newImages.push({ key, order, url });
|
||||||
newPreviews.push({ key, url, order });
|
newPreviews.push({ key, url, order });
|
||||||
}
|
}
|
||||||
|
|
||||||
onImagesChange(newImages);
|
onImagesChange(newImages);
|
||||||
setPreviews(newPreviews);
|
setPreviews(newPreviews);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : "Upload failed");
|
setError(err instanceof Error ? err.message : "Не успеа качувањето");
|
||||||
} finally {
|
} finally {
|
||||||
setUploading(false);
|
setUploading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeImage = (key: string) => {
|
const remaining = MAX_FILES - currentCount;
|
||||||
const idx = images.findIndex((img) => img.key === key);
|
|
||||||
if (idx === -1) return;
|
|
||||||
const updated = images
|
|
||||||
.filter((img) => img.key !== key)
|
|
||||||
.map((img, i) => ({ ...img, order: i + 1 }));
|
|
||||||
onImagesChange(updated);
|
|
||||||
setPreviews(previews.filter((p) => p.key !== key));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-stone-600">
|
<p className="text-sm text-stone-600">
|
||||||
Upload up to {MAX_FILES} photos (max {MAX_FILE_SIZE / 1024 / 1024}MB each, JPEG/PNG/WebP/GIF)
|
Качете до {MAX_FILES} фотографии (макс. {MAX_FILE_SIZE / 1024 / 1024}MB секоја, JPEG/PNG/WebP/GIF)
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<label className="mt-3 flex cursor-pointer items-center justify-center rounded-lg border-2 border-dashed border-stone-300 bg-stone-50 px-6 py-8 transition-colors hover:border-primary hover:bg-stone-100">
|
<label className="mt-3 flex cursor-pointer items-center justify-center rounded-lg border-2 border-dashed border-stone-300 bg-stone-50 px-6 py-8 transition-colors hover:border-primary hover:bg-stone-100">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-sm font-medium text-stone-700">
|
<p className="text-sm font-medium text-stone-700">
|
||||||
{uploading ? "Uploading..." : "Click to upload or drag and drop"}
|
{uploading ? "Качување..." : "Кликнете за качување или влечете и пуштете"}
|
||||||
</p>
|
</p>
|
||||||
|
{remaining > 0 && !uploading && (
|
||||||
|
<p className="mt-1 text-xs text-stone-400">Преостануваат {remaining} места</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept={ALLOWED_TYPES.join(",")}
|
accept={ALLOWED_TYPES.join(",")}
|
||||||
multiple
|
multiple
|
||||||
className="hidden"
|
className="hidden"
|
||||||
disabled={uploading || images.length >= MAX_FILES}
|
disabled={uploading || currentCount >= MAX_FILES}
|
||||||
onChange={(e) => e.target.files && handleFiles(e.target.files)}
|
onChange={(e) => e.target.files && handleFiles(e.target.files)}
|
||||||
onDragOver={(e) => e.preventDefault()}
|
onDragOver={(e) => e.preventDefault()}
|
||||||
onDrop={(e) => {
|
onDrop={(e) => {
|
||||||
@ -123,7 +120,13 @@ export default function ImageUploader({ images, onImagesChange }: ImageUploaderP
|
|||||||
<div key={preview.key} className="group relative overflow-hidden rounded-lg border border-stone-200">
|
<div key={preview.key} className="group relative overflow-hidden rounded-lg border border-stone-200">
|
||||||
<img src={preview.url} alt="" className="h-32 w-full object-cover" />
|
<img src={preview.url} alt="" className="h-32 w-full object-cover" />
|
||||||
<button
|
<button
|
||||||
onClick={() => removeImage(preview.key)}
|
onClick={() => {
|
||||||
|
const idx = images.findIndex((img) => img.key === preview.key);
|
||||||
|
if (idx === -1) return;
|
||||||
|
const updated = images.filter((img) => img.key !== preview.key).map((img, i) => ({ ...img, order: i + 1 }));
|
||||||
|
onImagesChange(updated);
|
||||||
|
setPreviews(previews.filter((p) => p.key !== preview.key));
|
||||||
|
}}
|
||||||
className="absolute right-1 top-1 flex h-6 w-6 items-center justify-center rounded-full bg-red-500 text-xs text-white opacity-0 transition-opacity group-hover:opacity-100"
|
className="absolute right-1 top-1 flex h-6 w-6 items-center justify-center rounded-full bg-red-500 text-xs text-white opacity-0 transition-opacity group-hover:opacity-100"
|
||||||
>
|
>
|
||||||
×
|
×
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
import { SignInButton, SignUpButton, UserButton } from "@clerk/nextjs";
|
import { SignInButton, SignUpButton, UserButton } from "@clerk/nextjs";
|
||||||
import { useAuth } from "@clerk/nextjs";
|
import { useAuth } from "@clerk/nextjs";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
@ -15,12 +16,12 @@ export default function NavAuth() {
|
|||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<SignInButton mode="redirect" fallbackRedirectUrl="/dashboard">
|
<SignInButton mode="redirect" fallbackRedirectUrl="/dashboard">
|
||||||
<button className="rounded-lg border border-stone-200 px-4 py-2 text-sm font-medium text-stone-700 transition-colors hover:bg-stone-50">
|
<button className="rounded-lg border border-stone-200 px-4 py-2 text-sm font-medium text-stone-700 transition-colors hover:bg-stone-50">
|
||||||
Sign In
|
Најави се
|
||||||
</button>
|
</button>
|
||||||
</SignInButton>
|
</SignInButton>
|
||||||
<SignUpButton mode="redirect" fallbackRedirectUrl="/onboarding">
|
<SignUpButton mode="redirect" fallbackRedirectUrl="/onboarding">
|
||||||
<button className="rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-primary-light">
|
<button className="rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-primary-light">
|
||||||
Sign Up
|
Регистрирај се
|
||||||
</button>
|
</button>
|
||||||
</SignUpButton>
|
</SignUpButton>
|
||||||
</div>
|
</div>
|
||||||
@ -36,7 +37,7 @@ export function LandingAuth() {
|
|||||||
href="/dashboard"
|
href="/dashboard"
|
||||||
className="rounded-lg bg-primary px-8 py-3 text-base font-medium text-white transition-colors hover:bg-primary-light"
|
className="rounded-lg bg-primary px-8 py-3 text-base font-medium text-white transition-colors hover:bg-primary-light"
|
||||||
>
|
>
|
||||||
Go to Dashboard
|
Оди на контролна табла
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -45,12 +46,12 @@ export function LandingAuth() {
|
|||||||
<div className="flex flex-col gap-3 sm:flex-row sm:justify-center">
|
<div className="flex flex-col gap-3 sm:flex-row sm:justify-center">
|
||||||
<SignInButton mode="redirect" fallbackRedirectUrl="/dashboard">
|
<SignInButton mode="redirect" fallbackRedirectUrl="/dashboard">
|
||||||
<button className="rounded-lg border border-stone-200 px-8 py-3 text-base font-medium text-stone-700 transition-colors hover:bg-stone-50">
|
<button className="rounded-lg border border-stone-200 px-8 py-3 text-base font-medium text-stone-700 transition-colors hover:bg-stone-50">
|
||||||
Sign In
|
Најави се
|
||||||
</button>
|
</button>
|
||||||
</SignInButton>
|
</SignInButton>
|
||||||
<SignUpButton mode="redirect" fallbackRedirectUrl="/onboarding">
|
<SignUpButton mode="redirect" fallbackRedirectUrl="/onboarding">
|
||||||
<button className="rounded-lg bg-primary px-8 py-3 text-base font-medium text-white transition-colors hover:bg-primary-light">
|
<button className="rounded-lg bg-primary px-8 py-3 text-base font-medium text-white transition-colors hover:bg-primary-light">
|
||||||
Get Started
|
Започни
|
||||||
</button>
|
</button>
|
||||||
</SignUpButton>
|
</SignUpButton>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -41,7 +41,7 @@ export default function SubdomainPicker({ value, onChange }: SubdomainPickerProp
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="subdomain" className="block text-sm font-medium text-stone-700">
|
<label htmlFor="subdomain" className="block text-sm font-medium text-stone-700">
|
||||||
Choose your subdomain
|
Изберете поддомен
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-1 flex items-center rounded-lg border border-stone-200 bg-white">
|
<div className="mt-1 flex items-center rounded-lg border border-stone-200 bg-white">
|
||||||
<input
|
<input
|
||||||
@ -49,7 +49,7 @@ export default function SubdomainPicker({ value, onChange }: SubdomainPickerProp
|
|||||||
type="text"
|
type="text"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => onChange(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-"))}
|
onChange={(e) => onChange(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-"))}
|
||||||
placeholder="e.g. eiffel-tower"
|
placeholder="нпр. maria-novakovska"
|
||||||
className="flex-1 rounded-l-lg border-0 px-3 py-2 text-stone-900 placeholder:text-stone-400 focus:outline-none focus:ring-1 focus:ring-primary"
|
className="flex-1 rounded-l-lg border-0 px-3 py-2 text-stone-900 placeholder:text-stone-400 focus:outline-none focus:ring-1 focus:ring-primary"
|
||||||
maxLength={63}
|
maxLength={63}
|
||||||
/>
|
/>
|
||||||
@ -60,15 +60,15 @@ export default function SubdomainPicker({ value, onChange }: SubdomainPickerProp
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-sm">
|
<div className="text-sm">
|
||||||
{checking && <p className="text-stone-500">Checking availability...</p>}
|
{checking && <p className="text-stone-500">Проверка на достапност...</p>}
|
||||||
{!checking && available === true && (
|
{!checking && available === true && (
|
||||||
<p className="text-green-600">✓ {slug}.testbed.mk is available!</p>
|
<p className="text-green-600">✓ {slug}.testbed.mk е достапен!</p>
|
||||||
)}
|
)}
|
||||||
{!checking && available === false && (
|
{!checking && available === false && (
|
||||||
<p className="text-red-600">✗ This subdomain is already taken.</p>
|
<p className="text-red-600">✗ Овој поддомен е веќе зафатен.</p>
|
||||||
)}
|
)}
|
||||||
{!checking && available === null && slug.length > 0 && slug.length < 3 && (
|
{!checking && available === null && slug.length > 0 && slug.length < 3 && (
|
||||||
<p className="text-stone-400">At least 3 characters required.</p>
|
<p className="text-stone-400">Потребни се најмалку 3 карактери.</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -13,15 +13,15 @@ interface TemplatePickerProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const TEMPLATES = [
|
const TEMPLATES = [
|
||||||
{ id: 1, name: "Elegance", tagline: "Serif warmth, dignified tribute" },
|
{ id: 1, name: "Елеганција", tagline: "Серифна топлина, достоенственEMA" },
|
||||||
{ id: 2, name: "Cinematic", tagline: "Bold, immersive, full-screen" },
|
{ id: 2, name: "Кинематски", tagline: "Смело, потопно, целосно екранско" },
|
||||||
{ id: 3, name: "Serene", tagline: "Peaceful, centered, spiritual" },
|
{ id: 3, name: "Спокој", tagline: "Мирно, центрирано, духовно" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function TemplatePicker({ value, onChange, title, description, bornDate, passedDate, images }: TemplatePickerProps) {
|
export default function TemplatePicker({ value, onChange, title, description, bornDate, passedDate, images }: TemplatePickerProps) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<p className="text-sm text-stone-600">Choose how the memorial page will look. Scroll the preview to see the full page.</p>
|
<p className="text-sm text-stone-600">Изберете како ќе изгледа спомен страницата. Скролајте за цел преглед.</p>
|
||||||
|
|
||||||
<div className="grid grid-cols-3 gap-3">
|
<div className="grid grid-cols-3 gap-3">
|
||||||
{TEMPLATES.map((t) => (
|
{TEMPLATES.map((t) => (
|
||||||
@ -42,7 +42,7 @@ export default function TemplatePicker({ value, onChange, title, description, bo
|
|||||||
|
|
||||||
{value && (
|
{value && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="text-xs font-medium text-stone-500 uppercase tracking-wider">Live Preview</p>
|
<p className="text-xs font-medium text-stone-500 uppercase tracking-wider">Преглед во живо</p>
|
||||||
<div className="overflow-hidden rounded-lg border border-stone-300 shadow-md">
|
<div className="overflow-hidden rounded-lg border border-stone-300 shadow-md">
|
||||||
<div className="h-[500px] overflow-y-auto">
|
<div className="h-[500px] overflow-y-auto">
|
||||||
<MemorialPreview
|
<MemorialPreview
|
||||||
|
|||||||
@ -15,7 +15,7 @@ function MemorialFooter({ name }: { name: string | null }) {
|
|||||||
<path d="M12 3C7 8 4 11 4 14.5C4 18 7 21 12 21C17 21 20 18 20 14.5C20 11 17 8 12 3Z" strokeLinecap="round" strokeLinejoin="round" />
|
<path d="M12 3C7 8 4 11 4 14.5C4 18 7 21 12 21C17 21 20 18 20 14.5C20 11 17 8 12 3Z" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm italic text-stone-400">In Loving Memory</p>
|
<p className="text-sm italic text-stone-400">Во спомен на</p>
|
||||||
{name && <p className="mt-1 text-xs text-stone-400">{name}</p>}
|
{name && <p className="mt-1 text-xs text-stone-400">{name}</p>}
|
||||||
</footer>
|
</footer>
|
||||||
);
|
);
|
||||||
@ -44,11 +44,11 @@ export function TemplateElegance({ data }: { data: MemorialData }) {
|
|||||||
<div className="min-h-screen bg-stone-50 font-serif">
|
<div className="min-h-screen bg-stone-50 font-serif">
|
||||||
{heroImage && (
|
{heroImage && (
|
||||||
<div className="relative h-[55vh] w-full overflow-hidden">
|
<div className="relative h-[55vh] w-full overflow-hidden">
|
||||||
<img src={heroImage.url} alt={data.title || "Memorial"} className="h-full w-full object-cover" />
|
<img src={heroImage.url} alt={data.title || "Спомен"} className="h-full w-full object-cover" />
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-stone-900/70 via-stone-900/20 to-transparent" />
|
<div className="absolute inset-0 bg-gradient-to-t from-stone-900/70 via-stone-900/20 to-transparent" />
|
||||||
<div className="absolute bottom-10 left-0 right-0 text-center">
|
<div className="absolute bottom-10 left-0 right-0 text-center">
|
||||||
<h1 className="text-4xl font-bold tracking-wide text-white md:text-6xl drop-shadow-lg">
|
<h1 className="text-4xl font-bold tracking-wide text-white md:text-6xl drop-shadow-lg">
|
||||||
{data.title || "In Loving Memory"}
|
{data.title || "Во спомен на"}
|
||||||
</h1>
|
</h1>
|
||||||
{dates && (
|
{dates && (
|
||||||
<p className="mt-3 text-lg tracking-widest text-stone-200 uppercase drop-shadow">
|
<p className="mt-3 text-lg tracking-widest text-stone-200 uppercase drop-shadow">
|
||||||
@ -62,7 +62,7 @@ export function TemplateElegance({ data }: { data: MemorialData }) {
|
|||||||
{!heroImage && (
|
{!heroImage && (
|
||||||
<div className="mx-auto max-w-2xl px-6 pt-24 text-center">
|
<div className="mx-auto max-w-2xl px-6 pt-24 text-center">
|
||||||
<h1 className="text-4xl font-bold tracking-wide text-stone-900 md:text-6xl">
|
<h1 className="text-4xl font-bold tracking-wide text-stone-900 md:text-6xl">
|
||||||
{data.title || "In Loving Memory"}
|
{data.title || "Во спомен на"}
|
||||||
</h1>
|
</h1>
|
||||||
{dates && (
|
{dates && (
|
||||||
<p className="mt-4 text-lg tracking-widest text-stone-400 uppercase">{dates}</p>
|
<p className="mt-4 text-lg tracking-widest text-stone-400 uppercase">{dates}</p>
|
||||||
@ -105,11 +105,11 @@ export function TemplateCinematic({ data }: { data: MemorialData }) {
|
|||||||
{sortedImages.length > 0 && (
|
{sortedImages.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<div className="relative h-screen w-full">
|
<div className="relative h-screen w-full">
|
||||||
<img src={sortedImages[0].url} alt={data.title || "Memorial"} className="h-full w-full object-cover" />
|
<img src={sortedImages[0].url} alt={data.title || "Спомен"} className="h-full w-full object-cover" />
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-black/90 via-black/40 to-transparent" />
|
<div className="absolute inset-0 bg-gradient-to-t from-black/90 via-black/40 to-transparent" />
|
||||||
<div className="absolute bottom-16 left-8 right-8 md:left-16">
|
<div className="absolute bottom-16 left-8 right-8 md:left-16">
|
||||||
<h1 className="text-5xl font-black tracking-tight md:text-8xl drop-shadow-2xl">
|
<h1 className="text-5xl font-black tracking-tight md:text-8xl drop-shadow-2xl">
|
||||||
{data.title || "In Loving Memory"}
|
{data.title || "Во спомен на"}
|
||||||
</h1>
|
</h1>
|
||||||
{dates && (
|
{dates && (
|
||||||
<p className="mt-3 text-lg font-light tracking-widest text-zinc-300 uppercase md:text-xl">
|
<p className="mt-3 text-lg font-light tracking-widest text-zinc-300 uppercase md:text-xl">
|
||||||
@ -140,7 +140,7 @@ export function TemplateCinematic({ data }: { data: MemorialData }) {
|
|||||||
<div className="flex min-h-screen items-center justify-center px-8">
|
<div className="flex min-h-screen items-center justify-center px-8">
|
||||||
<div className="max-w-2xl text-center">
|
<div className="max-w-2xl text-center">
|
||||||
<h1 className="text-5xl font-black tracking-tight md:text-8xl">
|
<h1 className="text-5xl font-black tracking-tight md:text-8xl">
|
||||||
{data.title || "In Loving Memory"}
|
{data.title || "Во спомен на"}
|
||||||
</h1>
|
</h1>
|
||||||
{dates && (
|
{dates && (
|
||||||
<p className="mt-4 text-lg font-light tracking-widest text-zinc-400 uppercase">{dates}</p>
|
<p className="mt-4 text-lg font-light tracking-widest text-zinc-400 uppercase">{dates}</p>
|
||||||
@ -160,7 +160,7 @@ export function TemplateCinematic({ data }: { data: MemorialData }) {
|
|||||||
<path d="M12 3C7 8 4 11 4 14.5C4 18 7 21 12 21C17 21 20 18 20 14.5C20 11 17 8 12 3Z" strokeLinecap="round" strokeLinejoin="round" />
|
<path d="M12 3C7 8 4 11 4 14.5C4 18 7 21 12 21C17 21 20 18 20 14.5C20 11 17 8 12 3Z" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm italic text-zinc-500">In Loving Memory</p>
|
<p className="text-sm italic text-zinc-500">Во спомен на</p>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -176,12 +176,12 @@ export function TemplateSerene({ data }: { data: MemorialData }) {
|
|||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
{sortedImages.length > 0 && (
|
{sortedImages.length > 0 && (
|
||||||
<div className="mx-auto mb-8 h-40 w-40 overflow-hidden rounded-full shadow-lg">
|
<div className="mx-auto mb-8 h-40 w-40 overflow-hidden rounded-full shadow-lg">
|
||||||
<img src={sortedImages[0].url} alt={data.title || "Memorial"} className="h-full w-full object-cover" />
|
<img src={sortedImages[0].url} alt={data.title || "Спомен"} className="h-full w-full object-cover" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<h1 className="text-3xl font-light tracking-tight text-zinc-900 md:text-5xl">
|
<h1 className="text-3xl font-light tracking-tight text-zinc-900 md:text-5xl">
|
||||||
{data.title || "In Loving Memory"}
|
{data.title || "Во спомен на"}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{dates && (
|
{dates && (
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user