diff --git a/app/admin/_components/admin-nav.tsx b/app/admin/_components/admin-nav.tsx new file mode 100644 index 0000000..cea0100 --- /dev/null +++ b/app/admin/_components/admin-nav.tsx @@ -0,0 +1,75 @@ +"use client"; + +import Link from "next/link"; +import { usePathname, useSearchParams } from "next/navigation"; + +interface NavItem { + href: string; + label: string; + icon: string; +} + +export function AdminNav({ + navItems, +}: { + navItems: NavItem[]; +}) { + const pathname = usePathname(); + const searchParams = useSearchParams(); + + function checkActive(href: string) { + if (href.includes("?")) { + const [path, query] = href.split("?"); + const params = new URLSearchParams(query); + // Verifica se o path bate E se TODOS os query params do item estão presentes na URL atual + return ( + pathname === path && + Array.from(params.entries()).every( + ([k, v]) => searchParams.get(k) === v, + ) + ); + } + + // Caso especial: se estamos na página de conteúdos mas com filtro de acesso, + // o link geral de "Conteúdos" não deve ficar ativo. + if (href === "/admin/content" && searchParams.get("access")) { + return false; + } + + return pathname === href; + } + + return ( + + ); +} diff --git a/app/admin/categories/categories-admin-client.tsx b/app/admin/categories/categories-admin-client.tsx index 79dca24..870d315 100644 --- a/app/admin/categories/categories-admin-client.tsx +++ b/app/admin/categories/categories-admin-client.tsx @@ -7,7 +7,7 @@ import { updateCategory, deleteCategory } from '@/lib/actions/admin'; -import { Plus, Trash2, Edit2, X, Check } from 'lucide-react'; +import { Plus, Trash2, Edit2, Check } from 'lucide-react'; interface Category { id: string; diff --git a/app/admin/content/[id]/edit/page.tsx b/app/admin/content/[id]/edit/page.tsx index 1972705..d5ff558 100644 --- a/app/admin/content/[id]/edit/page.tsx +++ b/app/admin/content/[id]/edit/page.tsx @@ -1,4 +1,5 @@ import Link from 'next/link'; +import Image from 'next/image'; import { redirect } from 'next/navigation'; import { getContentById } from '@/lib/actions/admin'; @@ -58,7 +59,7 @@ export default async function AdminContentEditPage({ params }: PageProps) {
{session.image ? ( - {session.name} + {session.name} ) : (
{session.name.charAt(0).toUpperCase()} diff --git a/app/admin/content/[id]/review/page.tsx b/app/admin/content/[id]/review/page.tsx index e76a9db..f689809 100644 --- a/app/admin/content/[id]/review/page.tsx +++ b/app/admin/content/[id]/review/page.tsx @@ -1,4 +1,5 @@ import Link from "next/link"; +import Image from "next/image"; import { redirect } from "next/navigation"; import { ReviewActions } from "@/app/admin/content/[id]/review/review-actions"; @@ -170,9 +171,11 @@ export default async function ContentReviewPage({ params }: PageProps) { >
{c.authorImage ? ( - {c.authorName} ) : ( @@ -228,7 +231,7 @@ export default async function ContentReviewPage({ params }: PageProps) {
{content.authorImage ? ( - {content.authorName + {content.authorName ) : (
{content.authorName?.[0] || '?'} diff --git a/app/admin/content/_components/dashboard/dashboard-header.tsx b/app/admin/content/_components/dashboard/dashboard-header.tsx index 7bad394..6f0a3d0 100644 --- a/app/admin/content/_components/dashboard/dashboard-header.tsx +++ b/app/admin/content/_components/dashboard/dashboard-header.tsx @@ -5,14 +5,15 @@ import Link from 'next/link'; interface DashboardHeaderProps { total: number; tab: 'editorial' | 'sistema'; + title?: string; } -export function DashboardHeader({ total, tab }: DashboardHeaderProps) { +export function DashboardHeader({ total, tab, title }: DashboardHeaderProps) { return (

- Gestão de Conteúdos + {title || 'Gestão de Conteúdos'}

{total} conteúdo{total !== 1 ? 's' : ''} encontrado{total !== 1 ? 's' : ''} diff --git a/app/admin/content/_components/dashboard/dashboard-table.tsx b/app/admin/content/_components/dashboard/dashboard-table.tsx index 9e79cee..4ff4d5c 100644 --- a/app/admin/content/_components/dashboard/dashboard-table.tsx +++ b/app/admin/content/_components/dashboard/dashboard-table.tsx @@ -1,8 +1,7 @@ -'use client'; +"use client"; -import Link from 'next/link'; -import Image from 'next/image'; -import { cn } from "@/lib/utils"; +import Image from "next/image"; +import Link from "next/link"; import { ContentRow, ContentStatus } from "../types"; import { STATUS_BADGES } from "./dashboard-types"; @@ -10,29 +9,53 @@ interface DashboardTableProps { rows: ContentRow[]; isPending: boolean; onStatusUpdate: (id: string, status: ContentStatus) => void; + onAccessUpdate: (id: string, access: "free" | "pro") => void; isAdmin: boolean; } -export function DashboardTable({ rows, isPending, onStatusUpdate, isAdmin }: DashboardTableProps) { +export function DashboardTable({ + rows, + isPending, + onStatusUpdate, + onAccessUpdate, + isAdmin, +}: DashboardTableProps) { return (

- - - - - - - + + + + + + + {isPending && rows.length === 0 ? ( - + - + diff --git a/app/admin/content/_components/forms/concept-form.tsx b/app/admin/content/_components/forms/concept-form.tsx index 3f1f8d0..1b13995 100644 --- a/app/admin/content/_components/forms/concept-form.tsx +++ b/app/admin/content/_components/forms/concept-form.tsx @@ -4,6 +4,18 @@ import { FormProps, CATEGORIES, DIFFICULTIES, ACCESS_OPTIONS } from "../types"; import { FormField, TextInput, SelectInput, NumberInput } from "../form-elements"; import { MarkdownEditor } from "../markdown-editor"; import { MetadataPreview } from "../metadata-preview"; +import { useState } from "react"; +import { FileJson, Download } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; export function ConceptForm({ slug, @@ -21,9 +33,110 @@ export function ConceptForm({ { value: "fundamentals", label: "Fundamentos" }, ]; + const [importJson, setImportJson] = useState(""); + const [isImportOpen, setIsImportOpen] = useState(false); + + const handleImportJson = () => { + try { + const parsed = JSON.parse(importJson); + if (typeof parsed !== "object" || parsed === null) throw new Error(); + + if (parsed.title) setTitle(parsed.title); + if (parsed.slug) setSlug(parsed.slug); + if (parsed.body) setBody(parsed.body); + + const importedMeta = parsed.meta || {}; + setMeta({ + ...meta, + category: importedMeta.category || meta.category, + difficulty: importedMeta.difficulty || meta.difficulty, + access: importedMeta.access || meta.access, + estimatedMinutes: importedMeta.estimatedMinutes || meta.estimatedMinutes, + prerequisites: importedMeta.prerequisites || meta.prerequisites, + summary: importedMeta.summary || meta.summary, + }); + + setIsImportOpen(false); + setImportJson(""); + } catch (err) { + alert("Erro ao importar JSON. Verifica se o formato é válido."); + console.error(err); + } + }; + + const handleExportJson = () => { + const data = { + title, + slug, + body, + meta, + }; + const dataStr = JSON.stringify(data, null, 2); + const dataUri = "data:application/json;charset=utf-8," + encodeURIComponent(dataStr); + const exportFileDefaultName = `${slug || "concept"}.json`; + + const linkElement = document.createElement("a"); + linkElement.setAttribute("href", dataUri); + linkElement.setAttribute("download", exportFileDefaultName); + linkElement.click(); + }; + return (
-
+
+
+
+ Modo de Edição de Conceito: Podes importar definições JSON para preenchimento rápido. +
+ + + + + + + + + Importar Conceito (JSON) + + + Cola o JSON completo do conceito abaixo para preencher os campos automaticamente. + + +
+
TítuloTipoAutorStatusVersãoAtualizadoAções + Título + + Tipo + + Autor + + Status + + Versão + + Atualizado + + Ações +
+
Carregando... @@ -44,14 +67,28 @@ export function DashboardTable({ rows, isPending, onStatusUpdate, isAdmin }: Das
- - + +
-

Nenhum conteúdo encontrado

+

+ Nenhum conteúdo encontrado +

- {isAdmin ? 'Os conteúdos aparecerão aqui após serem migrados.' : 'Ainda não criaste nenhum conteúdo.'} + {isAdmin + ? "Os conteúdos aparecerão aqui após serem migrados." + : "Ainda não criaste nenhum conteúdo."}

@@ -61,11 +98,18 @@ export function DashboardTable({ rows, isPending, onStatusUpdate, isAdmin }: Das rows.map((row) => { const badge = STATUS_BADGES[row.status]; return ( -
-

{row.title}

-

{row.slug}

+

+ {row.title} +

+

+ {row.slug} +

@@ -77,33 +121,41 @@ export function DashboardTable({ rows, isPending, onStatusUpdate, isAdmin }: Das
{row.authorImage ? ( - {row.authorName ) : (
- {row.authorName?.[0] || '?'} + {row.authorName?.[0] || "?"}
)}
-

{row.authorName || 'Sistema'}

-

{row.authorId?.slice(0, 8)}

+

+ {row.authorName || "Sistema"} +

+

+ {row.authorId?.slice(0, 8)} +

- + {badge.label} v{row.version} - {new Date(row.updatedAt).toLocaleDateString('pt-BR')} + v{row.version} + + {new Date(row.updatedAt).toLocaleDateString("pt-BR")}
@@ -121,15 +173,33 @@ export function DashboardTable({ rows, isPending, onStatusUpdate, isAdmin }: Das Revisar )} - {isAdmin && row.status === 'PENDING_REVIEW' && ( + {isAdmin && row.status === "PENDING_REVIEW" && ( )} + {isAdmin && row.access === "pro" && ( + + )} + {isAdmin && row.access === "free" && ( + + )}