Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions app/admin/_components/admin-nav.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<nav className="flex items-center gap-1 overflow-x-auto py-2 scrollbar-hide">
{navItems.map((item) => {
const isActive = checkActive(item.href);
return (
<Link
key={item.href}
href={item.href}
className={`flex items-center gap-2 whitespace-nowrap px-4 py-2 text-sm transition-all duration-200 ${
isActive
? "bg-primary text-primary-foreground shadow-md ring-1 ring-primary/20 font-bold"
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
}`}
>
<svg
className={`h-4 w-4 shrink-0 transition-colors ${isActive ? "text-primary-foreground" : ""}`}
fill="none"
viewBox="0 0 24 24"
strokeWidth={isActive ? 2.5 : 1.5}
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d={item.icon}
/>
</svg>
{item.label}
</Link>
);
})}
</nav>
);
}
2 changes: 1 addition & 1 deletion app/admin/categories/categories-admin-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion app/admin/content/[id]/edit/page.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -58,7 +59,7 @@ export default async function AdminContentEditPage({ params }: PageProps) {
<div className="flex items-center gap-2 rounded-lg border border-border bg-secondary/30 px-3 py-1.5 text-xs">
<div className="h-5 w-5 overflow-hidden rounded-full bg-muted">
{session.image ? (
<img src={session.image} alt={session.name} className="h-full w-full object-cover" />
<Image src={session.image} alt={session.name} width={20} height={20} className="h-full w-full object-cover" />
) : (
<div className="flex h-full w-full items-center justify-center text-[10px] font-bold text-muted-foreground">
{session.name.charAt(0).toUpperCase()}
Expand Down
7 changes: 5 additions & 2 deletions app/admin/content/[id]/review/page.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -170,9 +171,11 @@ export default async function ContentReviewPage({ params }: PageProps) {
>
<div className="flex items-center gap-2 mb-2">
{c.authorImage ? (
<img
<Image
src={c.authorImage}
alt={c.authorName}
width={20}
height={20}
className="h-5 w-5 rounded-full"
/>
) : (
Expand Down Expand Up @@ -228,7 +231,7 @@ export default async function ContentReviewPage({ params }: PageProps) {
<dd className="flex items-center gap-2">
<div className="h-8 w-8 overflow-hidden rounded-full bg-muted">
{content.authorImage ? (
<img src={content.authorImage} alt={content.authorName || ''} className="h-full w-full object-cover" />
<Image src={content.authorImage} alt={content.authorName || ''} width={32} height={32} className="h-full w-full object-cover" />
) : (
<div className="flex h-full w-full items-center justify-center text-xs font-bold text-muted-foreground">
{content.authorName?.[0] || '?'}
Expand Down
5 changes: 3 additions & 2 deletions app/admin/content/_components/dashboard/dashboard-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 className="text-2xl font-bold tracking-tight text-foreground">
Gestão de Conteúdos
{title || 'Gestão de Conteúdos'}
</h1>
<p className="mt-1 text-sm text-muted-foreground">
{total} conteúdo{total !== 1 ? 's' : ''} encontrado{total !== 1 ? 's' : ''}
Expand Down
134 changes: 102 additions & 32 deletions app/admin/content/_components/dashboard/dashboard-table.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,61 @@
'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";

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 (
<div className="overflow-hidden rounded-xl border border-border">
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-border bg-secondary/50">
<th className="px-4 py-3 text-left font-medium text-muted-foreground">Título</th>
<th className="px-4 py-3 text-left font-medium text-muted-foreground">Tipo</th>
<th className="px-4 py-3 text-left font-medium text-muted-foreground">Autor</th>
<th className="px-4 py-3 text-left font-medium text-muted-foreground">Status</th>
<th className="px-4 py-3 text-left font-medium text-muted-foreground">Versão</th>
<th className="px-4 py-3 text-left font-medium text-muted-foreground">Atualizado</th>
<th className="px-4 py-3 text-left font-medium text-muted-foreground">Ações</th>
<th className="px-4 py-3 text-left font-medium text-muted-foreground">
Título
</th>
<th className="px-4 py-3 text-left font-medium text-muted-foreground">
Tipo
</th>
<th className="px-4 py-3 text-left font-medium text-muted-foreground">
Autor
</th>
<th className="px-4 py-3 text-left font-medium text-muted-foreground">
Status
</th>
<th className="px-4 py-3 text-left font-medium text-muted-foreground">
Versão
</th>
<th className="px-4 py-3 text-left font-medium text-muted-foreground">
Atualizado
</th>
<th className="px-4 py-3 text-left font-medium text-muted-foreground">
Ações
</th>
</tr>
</thead>
<tbody className="divide-y divide-border">
{isPending && rows.length === 0 ? (
<tr>
<td colSpan={7} className="px-4 py-12 text-center text-muted-foreground">
<td
colSpan={7}
className="px-4 py-12 text-center text-muted-foreground"
>
<div className="flex items-center justify-center gap-2">
<div className="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent" />
Carregando...
Expand All @@ -44,14 +67,28 @@ export function DashboardTable({ rows, isPending, onStatusUpdate, isAdmin }: Das
<td colSpan={7} className="px-4 py-16 text-center">
<div className="flex flex-col items-center gap-3">
<div className="flex h-14 w-14 items-center justify-center rounded-2xl bg-muted">
<svg className="h-7 w-7 text-muted-foreground" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m6.75 12H9.75m3 0h.008v.008h-.008v-.008zm0 3h.008v.008h-.008v-.008zm-3-3h.008v.008H9.75v-.008zm0 3h.008v.008H9.75v-.008z" />
<svg
className="h-7 w-7 text-muted-foreground"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m6.75 12H9.75m3 0h.008v.008h-.008v-.008zm0 3h.008v.008h-.008v-.008zm-3-3h.008v.008H9.75v-.008zm0 3h.008v.008H9.75v-.008z"
/>
</svg>
</div>
<div>
<p className="font-medium text-foreground">Nenhum conteúdo encontrado</p>
<p className="font-medium text-foreground">
Nenhum conteúdo encontrado
</p>
<p className="mt-1 text-sm text-muted-foreground">
{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."}
</p>
</div>
</div>
Expand All @@ -61,11 +98,18 @@ export function DashboardTable({ rows, isPending, onStatusUpdate, isAdmin }: Das
rows.map((row) => {
const badge = STATUS_BADGES[row.status];
return (
<tr key={row.id} className="transition-colors hover:bg-accent/50">
<tr
key={row.id}
className="transition-colors hover:bg-accent/50"
>
<td className="px-4 py-3">
<div>
<p className="font-medium text-foreground">{row.title}</p>
<p className="text-xs text-muted-foreground font-mono">{row.slug}</p>
<p className="font-medium text-foreground">
{row.title}
</p>
<p className="text-xs text-muted-foreground font-mono">
{row.slug}
</p>
</div>
</td>
<td className="px-4 py-3">
Expand All @@ -77,33 +121,41 @@ export function DashboardTable({ rows, isPending, onStatusUpdate, isAdmin }: Das
<div className="flex items-center gap-2">
<div className="h-6 w-6 overflow-hidden rounded-full bg-muted">
{row.authorImage ? (
<Image
src={row.authorImage}
alt={row.authorName || ''}
<Image
src={row.authorImage}
alt={row.authorName || ""}
width={24}
height={24}
className="h-full w-full object-cover"
className="h-full w-full object-cover"
/>
) : (
<div className="flex h-full w-full items-center justify-center text-[10px] font-bold text-muted-foreground">
{row.authorName?.[0] || '?'}
{row.authorName?.[0] || "?"}
</div>
)}
</div>
<div>
<p className="text-xs font-medium text-foreground">{row.authorName || 'Sistema'}</p>
<p className="text-[10px] text-muted-foreground font-mono">{row.authorId?.slice(0, 8)}</p>
<p className="text-xs font-medium text-foreground">
{row.authorName || "Sistema"}
</p>
<p className="text-[10px] text-muted-foreground font-mono">
{row.authorId?.slice(0, 8)}
</p>
</div>
</div>
</td>
<td className="px-4 py-3">
<span className={`inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium ${badge.className}`}>
<span
className={`inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium ${badge.className}`}
>
{badge.label}
</span>
</td>
<td className="px-4 py-3 tabular-nums text-muted-foreground">v{row.version}</td>
<td className="px-4 py-3 tabular-nums text-muted-foreground">
{new Date(row.updatedAt).toLocaleDateString('pt-BR')}
v{row.version}
</td>
<td className="px-4 py-3 tabular-nums text-muted-foreground">
{new Date(row.updatedAt).toLocaleDateString("pt-BR")}
</td>
<td className="px-4 py-3">
<div className="flex items-center gap-2">
Expand All @@ -121,15 +173,33 @@ export function DashboardTable({ rows, isPending, onStatusUpdate, isAdmin }: Das
Revisar
</Link>
)}
{isAdmin && row.status === 'PENDING_REVIEW' && (
{isAdmin && row.status === "PENDING_REVIEW" && (
<button
onClick={() => onStatusUpdate(row.id, 'PUBLISHED')}
onClick={() => onStatusUpdate(row.id, "PUBLISHED")}
disabled={isPending}
className="inline-flex h-7 items-center rounded-md bg-emerald-500/10 px-2.5 text-xs font-medium text-emerald-600 dark:text-emerald-400 transition-colors hover:bg-emerald-500/20 disabled:opacity-50"
>
Publicar
</button>
)}
{isAdmin && row.access === "pro" && (
<button
onClick={() => onAccessUpdate(row.id, "free")}
disabled={isPending}
className="inline-flex h-7 items-center rounded-md bg-blue-500/10 px-2.5 text-xs font-bold tracking-tight text-blue-600 dark:text-blue-400 transition-colors hover:bg-blue-500/20 disabled:opacity-50 shadow-sm"
>
Tornar Gratuito
</button>
)}
{isAdmin && row.access === "free" && (
<button
onClick={() => onAccessUpdate(row.id, "pro")}
disabled={isPending}
className="inline-flex h-7 items-center rounded-md bg-amber-500/10 px-2.5 text-xs font-bold tracking-tight text-amber-600 dark:text-amber-400 transition-colors hover:bg-amber-500/20 disabled:opacity-50 shadow-sm"
>
Tornar Pago
</button>
)}
</div>
</td>
</tr>
Expand Down
Loading
Loading