From 233cf86fea58c089a080c47bd62754797d1fda1c Mon Sep 17 00:00:00 2001 From: Peter Maquiran Date: Mon, 20 Apr 2026 23:06:41 +0100 Subject: [PATCH] reuse the same header and footer --- app/(page)/admin/create-news/create-news.tsx | 2 - app/(page)/admin/dashboard/page.tsx | 357 ++++++------------ .../manage-category-client.tsx | 203 ++++++++++ app/(page)/admin/manage-category/page.tsx | 343 +---------------- app/(page)/admin/panel/page.tsx | 194 +--------- app/(page)/admin/panel/panel-editor.tsx | 215 +++++++++++ 6 files changed, 555 insertions(+), 759 deletions(-) create mode 100644 app/(page)/admin/manage-category/manage-category-client.tsx create mode 100644 app/(page)/admin/panel/panel-editor.tsx diff --git a/app/(page)/admin/create-news/create-news.tsx b/app/(page)/admin/create-news/create-news.tsx index f766808..5e10e29 100644 --- a/app/(page)/admin/create-news/create-news.tsx +++ b/app/(page)/admin/create-news/create-news.tsx @@ -10,8 +10,6 @@ import { // Importe o componente que criámos (ajuste o caminho se necessário) import MultiAspectEditor from '../../../components/MultiAspectEditor'; import dynamic from "next/dynamic"; -import { AdminHeaderPage } from '@/app/components/layout/admin/header'; -import { AdminSideBar } from '@/app/components/layout/admin/sidebar'; // import { keycloak } from '@/app/feature/auth/keycloak-config'; const Editor = dynamic( () => import("@tinymce/tinymce-react").then((mod) => mod.Editor), diff --git a/app/(page)/admin/dashboard/page.tsx b/app/(page)/admin/dashboard/page.tsx index ce23dd5..5c11f27 100644 --- a/app/(page)/admin/dashboard/page.tsx +++ b/app/(page)/admin/dashboard/page.tsx @@ -1,184 +1,62 @@ import React from 'react'; -import { - LayoutDashboard, Newspaper, Users, BarChart3, - Settings, HelpCircle, TrendingUp, Eye, Clock, - AlertCircle, ChevronRight, Edit3, Trash2, ExternalLink +import { + Newspaper, + TrendingUp, + Eye, + Clock, + AlertCircle, + ChevronRight, + Edit3, + Trash2, } from 'lucide-react'; -import Image from 'next/image'; +import { AdminHeaderPage } from '@/app/components/layout/admin/header'; +import { AdminSideBar } from '@/app/components/layout/admin/sidebar'; const DashboardMain = () => { return (
- - {/* Sidebar Lateral - Consistente com a página de criação */} - - - {/* Main Content Area */} -
- {/* Header Superior */} - {/* Header Superior - Secção do Utilizador Atualizada */} -
-
- -
- -
- {/* Notificações (Opcional, mas completa o look) */} - - - {/* Menu do Utilizador */} -
-
-

James Wilson

-

Online

-
- -
- {/* Container da Imagem com Efeito de Anel */} -
- Avatar do utilizador -
- - {/* Indicador de Status (Mobile) */} -
-
- - {/* Seta para indicar menu (Chevron) */} - - - -
-
-
- -
- - {/* Métricas Principais (Grid de 4 colunas) */} +
- } + } /> - } + } /> - } + } /> - } + } isAlert />
- {/* Tabela de Artigos Recentes (Coluna Dupla) */}
-

Artigos Recentes

+

+ Artigos Recentes +

@@ -192,38 +70,39 @@ const DashboardMain = () => { - - -
- {/* Performance por Categoria (Coluna Única) */}
-

Performance de Conteúdo

+

+ Performance de Conteúdo +

- +

O tráfego orgânico cresceu 15% nos últimos 7 dias. @@ -237,24 +116,31 @@ const DashboardMain = () => { ); }; -// --- Subcomponentes para Organização --- - -const NavItem = ({ icon, label, active = false }: { icon: any, label: string, active?: boolean }) => ( -

- {icon} - {label} -
-); - -const StatCard = ({ label, value, trend, icon, isAlert = false }: any) => ( -
+const StatCard = ({ + label, + value, + trend, + icon, + isAlert = false, +}: { + label: string; + value: string; + trend: string; + icon: React.ReactNode; + isAlert?: boolean; +}) => ( +
-
- {icon} -
- +
{icon}
+ {trend}
@@ -263,16 +149,31 @@ const StatCard = ({ label, value, trend, icon, isAlert = false }: any) => (
); -const TableRow = ({ title, status, author, views }: any) => ( +const TableRow = ({ + title, + status, + author, + views, +}: { + title: string; + status: string; + author: string; + views: string; +}) => (

{title}

- + {status} @@ -280,14 +181,26 @@ const TableRow = ({ title, status, author, views }: any) => ( {views}
- - + +
); -const CategoryBar = ({ label, percentage, color }: any) => ( +const CategoryBar = ({ + label, + percentage, + color, +}: { + label: string; + percentage: number; + color: string; +}) => (
{label} @@ -300,39 +213,3 @@ const CategoryBar = ({ label, percentage, color }: any) => ( ); export default DashboardMain; - - -{/* Subcomponente ActivityItem (Coloque fora do componente principal) */} -const ActivityItem = ({ name, action, target, time, avatar }: { - name: string, - action: string, - target: string, - time: string, - avatar: string - }) => ( -
- {/* Avatar com Ring */} -
- {name} -
-
- - {/* Texto da Atividade */} -
-

- - {name} - - {" "}{action}{" "} - {target} -

- - {time} - -
-
- ); \ No newline at end of file diff --git a/app/(page)/admin/manage-category/manage-category-client.tsx b/app/(page)/admin/manage-category/manage-category-client.tsx new file mode 100644 index 0000000..6c04d3f --- /dev/null +++ b/app/(page)/admin/manage-category/manage-category-client.tsx @@ -0,0 +1,203 @@ +"use client"; + +import React, { useEffect, useState } from "react"; +import { + FolderTree, + Edit3, + Trash2, + Plus, + ChevronRight, + ChevronDown, +} from "lucide-react"; +import { createCategory, deleteCategory, getTree, updateCategory } from "@/lib/categories.api"; +import { slugify } from "@/lib/slug"; + +interface Category { + id: string; + name: string; + slug: string; + parentId?: string | null; + children?: Category[]; +} + +export default function ManageCategoryClient() { + const [tree, setTree] = 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, + }); + + async function load() { + setLoading(true); + try { + const t = await getTree(); + setTree(t); + } finally { + setLoading(false); + } + } + + useEffect(() => { + load(); + }, []); + + 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(); + } + + 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 }); + } + + function TreeNode({ + node, + level = 0, + }: { + node: Category; + level?: number; + }) { + const [open, setOpen] = useState(true); + + return ( +
+
+
+ + + + + openEdit(node)} + className="text-sm font-medium cursor-pointer hover:text-blue-600" + > + {node.name} + +
+ +
+ + + + + +
+
+ + {open && + node.children?.map((child) => )} +
+ ); + } + + return ( + <> +
+
+

Categories

+ + +
+ +
+ {loading ? ( +

Loading...

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

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

+ + + setForm({ + ...form, + name: e.target.value, + slug: slugify(e.target.value), + }) + } + /> + + setForm({ ...form, slug: e.target.value })} + /> + +
+ + + +
+
+
+ )} + + ); +} diff --git a/app/(page)/admin/manage-category/page.tsx b/app/(page)/admin/manage-category/page.tsx index 6cab277..3f8fe0c 100644 --- a/app/(page)/admin/manage-category/page.tsx +++ b/app/(page)/admin/manage-category/page.tsx @@ -1,343 +1,16 @@ -"use client"; +import { AdminHeaderPage } from '@/app/components/layout/admin/header'; +import { AdminSideBar } from '@/app/components/layout/admin/sidebar'; +import ManageCategoryClient from './manage-category-client'; -import React, { useEffect, useState } from "react"; -import Image from "next/image"; -import { - LayoutDashboard, - Newspaper, - Users, - BarChart3, - Settings, - HelpCircle, - Tag, - FolderTree, - Edit3, - Trash2, - Plus, - ChevronRight, - ChevronDown, -} from "lucide-react"; -import { createCategory, deleteCategory, getFlat, getTree, updateCategory } from "@/lib/categories.api"; -import { slugify } from "@/lib/slug"; - -/* ================= TYPES ================= */ -interface Category { - id: string; - name: string; - slug: string; - parentId?: string | null; - children?: Category[]; -} - -/* ================= 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 ( -
- -
- -
- - - - - - openEdit(node)} - className="text-sm font-medium cursor-pointer hover:text-blue-600" - > - {node.name} - -
- -
- - - - - - -
-
- - {open && - node.children?.map((child) => ( - - ))} -
- ); - } - - /* ================= UI ================= */ +export default function ManageCategoryPage() { return (
+ - {/* ================= SIDEBAR ================= */} - {/* Sidebar Lateral */} - - - {/* ================= MAIN ================= */} -
- - {/* HEADER */} -
- - - -
- - - Admin - - - -
-
- - {/* CONTENT */} -
- - {/* TOP BAR */} -
- -

- Categories -

- - -
- - {/* 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 }) - } - /> - -
- - - - - -
-
-
- )} -
); } - - -// Componentes Auxiliares para Limpeza de Código -const NavItem = ({ icon, label, active = false }: { icon: any, label: string, active?: boolean }) => ( -
- {icon} - {label} -
- ); - - const ActivityItem = ({ user, action }: { user: string, action: string }) => ( -
-
-

- {user} {action} -

-
- ); \ No newline at end of file diff --git a/app/(page)/admin/panel/page.tsx b/app/(page)/admin/panel/page.tsx index 0c397b4..6fe542a 100644 --- a/app/(page)/admin/panel/page.tsx +++ b/app/(page)/admin/panel/page.tsx @@ -1,188 +1,18 @@ -"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); - }; +import { AdminHeaderPage } from '@/app/components/layout/admin/header'; +import { AdminSideBar } from '@/app/components/layout/admin/sidebar'; +import PanelEditor from './panel-editor'; +export default function PanelPage() { 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 -