From f220ee6c3ead7889881220555b520f71f92bacfc Mon Sep 17 00:00:00 2001 From: Peter Maquiran Date: Sun, 19 Apr 2026 03:39:38 +0100 Subject: [PATCH] change link redirect --- .../components/CategoryTree.tsx | 264 ++++++++++++++++++ app/(page)/admin/panel/page.tsx | 188 +++++++++++++ app/api/auth/callback/route.ts | 2 +- lib/getPermisions.ts | 32 +++ lib/utels.ts | 8 + 5 files changed, 493 insertions(+), 1 deletion(-) create mode 100644 app/(page)/admin/manage-category/components/CategoryTree.tsx create mode 100644 app/(page)/admin/panel/page.tsx create mode 100644 lib/getPermisions.ts create mode 100644 lib/utels.ts diff --git a/app/(page)/admin/manage-category/components/CategoryTree.tsx b/app/(page)/admin/manage-category/components/CategoryTree.tsx new file mode 100644 index 0000000..d91d863 --- /dev/null +++ b/app/(page)/admin/manage-category/components/CategoryTree.tsx @@ -0,0 +1,264 @@ +"use client"; + +import React, { useEffect, useState } from "react"; +import { + FolderTree, + Edit3, + Trash2, + Plus, + ChevronRight, + ChevronDown, +} from "lucide-react"; +import { getTree, getFlat, updateCategory, createCategory, deleteCategory } from "@/lib/categories.api"; + +/* ================= TYPES ================= */ +interface Category { + id: string; + name: string; + slug: string; + parentId?: string | null; + children?: Category[]; +} + +/* ================= API ================= */ + +/* ================= UTIL ================= */ +function slugify(text: string) { + return text + .toLowerCase() + .trim() + .replace(/\s+/g, "-") + .replace(/[^a-z0-9-]/g, ""); +} + +/* ================= PAGE ================= */ +export default function CategoriesPage() { + const [tree, setTree] = useState([]); + const [flat, setFlat] = useState([]); + const [loading, setLoading] = useState(false); + + const [modalOpen, setModalOpen] = useState(false); + + const [form, setForm] = useState({ + id: null as string | null, + name: "", + slug: "", + parentId: null as string | null, + }); + + /* ================= LOAD ================= */ + async function load() { + setLoading(true); + try { + const [t, f] = await Promise.all([getTree(), getFlat()]); + setTree(t); + setFlat(f); + } finally { + setLoading(false); + } + } + + useEffect(() => { + load(); + }, []); + + /* ================= CRUD ================= */ + async function save() { + const payload = { + name: form.name, + slug: form.slug || slugify(form.name), + parentId: form.parentId, + }; + + if (form.id) { + await updateCategory(form.id, payload); + } else { + await createCategory(payload); + } + + closeModal(); + load(); + } + + async function remove(id: string) { + if (!confirm("Delete this category?")) return; + await deleteCategory(id); + load(); + } + + /* ================= MODAL ================= */ + function openCreate(parentId?: string) { + setForm({ + id: null, + name: "", + slug: "", + parentId: parentId || null, + }); + setModalOpen(true); + } + + function openEdit(cat: Category) { + setForm({ + id: cat.id, + name: cat.name, + slug: cat.slug, + parentId: cat.parentId || null, + }); + setModalOpen(true); + } + + function closeModal() { + setModalOpen(false); + setForm({ id: null, name: "", slug: "", parentId: null }); + } + + /* ================= TREE ================= */ + function TreeNode({ + node, + level = 0, + }: { + node: Category; + level?: number; + }) { + const [open, setOpen] = useState(true); + + return ( +
+ + {/* NODE */} +
+ +
+ + + + + + openEdit(node)} + className="text-sm font-medium cursor-pointer hover:text-blue-600" + > + {node.name} + +
+ + {/* ACTIONS */} +
+ + + + + + +
+
+ + {/* CHILDREN */} + {open && + node.children?.map((child) => ( + + ))} +
+ ); + } + + /* ================= UI ================= */ + return ( +
+ + {/* HEADER */} +
+

+ Category Manager +

+ + +
+ + {/* TREE */} +
+ {loading ? ( +

Loading...

+ ) : ( + tree.map((node) => ( + + )) + )} +
+ + {/* MODAL */} + {modalOpen && ( +
+ +
+ +

+ {form.id ? "Edit Category" : "Create Category"} +

+ + + setForm({ + ...form, + name: e.target.value, + slug: slugify(e.target.value), + }) + } + /> + + + setForm({ ...form, slug: e.target.value }) + } + /> + +
+ + + + + +
+
+
+ )} +
+ ); +} \ No newline at end of file diff --git a/app/(page)/admin/panel/page.tsx b/app/(page)/admin/panel/page.tsx new file mode 100644 index 0000000..0c397b4 --- /dev/null +++ b/app/(page)/admin/panel/page.tsx @@ -0,0 +1,188 @@ +"use client"; +import React, { useState, useCallback } from "react"; +import Cropper from "react-easy-crop"; + +const RATIOS = [ + { label: "Hero Banner (Ultra Wide)", value: 21 / 9, text: "21/9" }, + { label: "News Feed (Widescreen)", value: 16 / 9, text: "16/9" }, + { label: "Profile / Post (Square)", value: 1 / 1, text: "1/1" }, +]; + +export default function FullPageEditor() { + const [image, setImage] = useState(null); + const [crops, setCrops] = useState>( + RATIOS.reduce((acc, r) => ({ ...acc, [r.text]: { x: 0, y: 0, zoom: 1 } }), {}) + ); + const [completedCrops, setCompletedCrops] = useState>({}); + const [isExporting, setIsExporting] = useState(false); + const [results, setResults] = useState>({}); + + const onSelectFile = (e: React.ChangeEvent) => { + if (e.target.files && e.target.files.length > 0) { + const reader = new FileReader(); + reader.onload = () => setImage(reader.result as string); + reader.readAsDataURL(e.target.files[0]); + } + }; + + const handleExport = async () => { + setIsExporting(true); + const bundle: Record = {}; + for (const ratio of RATIOS) { + const crop = completedCrops[ratio.text]; + if (crop) bundle[ratio.text] = await getCroppedImg(image!, crop); + } + setResults(bundle); + setIsExporting(false); + }; + + return ( +
+ {/* 1. Global Navigation */} + + + {/* 2. Workspace Area */} +
+ {!image ? ( +
+
📁
+

Editor is Empty

+

Upload a high-resolution image to begin the multi-aspect framing process.

+
+ ) : ( +
+ {RATIOS.map((ratio) => ( +
+ {/* Section Header */} +
+
+ Aspect Ratio +

{ratio.label}

+
+
+

Current Zoom

+

{Math.round((crops[ratio.text]?.zoom || 1) * 100)}%

+
+
+ + {/* BIG Editor Window */} +
+ setCrops(prev => ({ ...prev, [ratio.text]: { ...prev[ratio.text], ...c } }))} + onZoomChange={(z) => setCrops(prev => ({ ...prev, [ratio.text]: { ...prev[ratio.text], zoom: z } }))} + onCropComplete={(_, px) => setCompletedCrops(prev => ({ ...prev, [ratio.text]: px }))} + objectFit="cover" + showGrid={true} + /> +
+ + {/* Floating Zoom Control (Large & Accessible) */} +
+ Adjust Zoom + setCrops(prev => ({ ...prev, [ratio.text]: { ...prev[ratio.text], zoom: Number(e.target.value) } }))} + className="flex-1 accent-indigo-500 h-2 bg-zinc-800 rounded-lg appearance-none cursor-pointer" + /> + +
+
+ ))} +
+ )} +
+ + {/* 3. Global Results Modal/Area */} + {Object.keys(results).length > 0 && ( +
+
+

EXPORT BUNDLE

+ +
+
+ {Object.entries(results).map(([ratio, b64]) => ( +
+
+ {ratio} Result + +
+ Crop preview +