diff --git a/src/app/(frontend)/layout.tsx b/src/app/(frontend)/layout.tsx
index e7681f7d..084d9d1f 100644
--- a/src/app/(frontend)/layout.tsx
+++ b/src/app/(frontend)/layout.tsx
@@ -1,18 +1,22 @@
+import { Footer } from '@/components/Footer'
+import { Header } from '@/components/Header'
import React from 'react'
import './styles.css'
export const metadata = {
- description: 'A blank template using Payload in a Next.js app.',
- title: 'Payload Blank Template',
+ description: 'Um template base usando Payload em uma aplicação Next.js.',
+ title: 'Template Base do Payload',
}
export default async function RootLayout(props: { children: React.ReactNode }) {
const { children } = props
return (
-
+
+
{children}
+
)
diff --git a/src/app/(frontend)/page.tsx b/src/app/(frontend)/page.tsx
index fb253e70..dc517bea 100644
--- a/src/app/(frontend)/page.tsx
+++ b/src/app/(frontend)/page.tsx
@@ -26,8 +26,8 @@ export default async function HomePage() {
width={65}
/>
- {!user && Welcome to your new project.
}
- {user && Welcome back, {user.email}
}
+ {!user && Boas-vindas ao seu novo projeto.
}
+ {user && Que bom ver você de volta, {user.email}.
}
- Tailwind is active: utility classes are being applied.
+ Tailwind está ativo: as classes utilitárias estão sendo aplicadas.
)
diff --git a/src/app/my-route/route.ts b/src/app/my-route/route.ts
index 07558861..5e7d2e0c 100644
--- a/src/app/my-route/route.ts
+++ b/src/app/my-route/route.ts
@@ -7,6 +7,6 @@ export const GET = async (request: Request) => {
})
return Response.json({
- message: 'This is an example of a custom route.',
+ message: 'Este é um exemplo de rota personalizada.',
})
}
diff --git a/src/collections/Media.ts b/src/collections/Media.ts
index 568cf426..c02edab0 100644
--- a/src/collections/Media.ts
+++ b/src/collections/Media.ts
@@ -2,6 +2,13 @@ import type { CollectionConfig } from 'payload'
export const Media: CollectionConfig = {
slug: 'media',
+ labels: {
+ singular: 'Mídia',
+ plural: 'Mídias',
+ },
+ admin: {
+ group: 'Coleções',
+ },
access: {
read: () => true,
},
@@ -9,6 +16,10 @@ export const Media: CollectionConfig = {
{
name: 'alt',
type: 'text',
+ label: 'Texto alternativo',
+ admin: {
+ description: 'Descrição da imagem para acessibilidade.',
+ },
required: true,
},
],
diff --git a/src/collections/Users.ts b/src/collections/Users.ts
index c683d0e2..eb4f2b7a 100644
--- a/src/collections/Users.ts
+++ b/src/collections/Users.ts
@@ -2,7 +2,12 @@ import type { CollectionConfig } from 'payload'
export const Users: CollectionConfig = {
slug: 'users',
+ labels: {
+ singular: 'Usuário',
+ plural: 'Usuários',
+ },
admin: {
+ group: 'Coleções',
useAsTitle: 'email',
},
auth: true,
diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx
new file mode 100644
index 00000000..71e38bcd
--- /dev/null
+++ b/src/components/Footer.tsx
@@ -0,0 +1,7 @@
+import { SocialLinks } from './socialLinks'
+
+export const Footer = () => (
+
+)
diff --git a/src/components/Header.tsx b/src/components/Header.tsx
new file mode 100644
index 00000000..700b0f85
--- /dev/null
+++ b/src/components/Header.tsx
@@ -0,0 +1,15 @@
+import { getCachedGlobal } from '@/utilities/getGlobals'
+import { SocialLinks } from './socialLinks'
+
+export const Header = async () => {
+ const settings = await getCachedGlobal('site-settings', 2)()
+
+ console.log(settings)
+
+ return (
+
+ {settings.headerTitle ? {settings.headerTitle}
: null}
+
+
+ )
+}
diff --git a/src/components/socialLinks.tsx b/src/components/socialLinks.tsx
new file mode 100644
index 00000000..ac9b5882
--- /dev/null
+++ b/src/components/socialLinks.tsx
@@ -0,0 +1,114 @@
+import { getCachedGlobal } from '@/utilities/getGlobals'
+import Link from 'next/link'
+
+type IconProps = {
+ className?: string
+}
+
+const baseIconClass = 'h-5 w-5'
+
+const InstagramIcon = ({ className }: IconProps) => (
+
+)
+
+const FacebookIcon = ({ className }: IconProps) => (
+
+)
+
+const YoutubeIcon = ({ className }: IconProps) => (
+
+)
+
+const GlobeIcon = ({ className }: IconProps) => (
+
+)
+
+const icons = {
+ instagram: InstagramIcon,
+ facebook: FacebookIcon,
+ youtube: YoutubeIcon,
+ custom: GlobeIcon,
+} as const
+
+const defaultA11yLabels: Record = {
+ instagram: 'Instagram',
+ facebook: 'Facebook',
+ youtube: 'YouTube',
+ custom: 'Link externo',
+}
+
+type SocialLinksProps = {
+ ariaLabel: string
+}
+
+export const SocialLinks = async ({ ariaLabel }: SocialLinksProps) => {
+ const { socialLinks: links } = await getCachedGlobal('site-settings', 2)()
+
+ if (!links?.length) return null
+
+ return (
+
+ )
+}
diff --git a/src/globals/SiteSettings.ts b/src/globals/SiteSettings.ts
new file mode 100644
index 00000000..bc982cee
--- /dev/null
+++ b/src/globals/SiteSettings.ts
@@ -0,0 +1,80 @@
+import { revalidateTag } from 'next/cache'
+import type { GlobalConfig } from 'payload'
+
+const revalidate = async () => revalidateTag('global_site-settings')
+
+export const SiteSettings: GlobalConfig = {
+ slug: 'site-settings',
+ label: 'Configurações do site',
+ admin: {
+ group: 'Configurações',
+ },
+ access: {
+ read: () => true,
+ update: ({ req: { user } }) => Boolean(user),
+ },
+ fields: [
+ {
+ name: 'headerTitle',
+ type: 'text',
+ label: 'Título do cabeçalho',
+ },
+ {
+ name: 'socialLinks',
+ type: 'array',
+ label: 'Redes sociais',
+ labels: {
+ singular: 'Rede social',
+ plural: 'Redes sociais',
+ },
+ fields: [
+ {
+ name: 'platform',
+ type: 'select',
+ label: 'Plataforma',
+ required: true,
+ options: [
+ { label: 'Instagram', value: 'instagram' },
+ { label: 'Facebook', value: 'facebook' },
+ { label: 'YouTube', value: 'youtube' },
+ ],
+ },
+ {
+ name: 'url',
+ type: 'text',
+ label: 'URL',
+ required: true,
+ validate: (value: string | null | undefined) => {
+ if (!value) return 'A URL é obrigatória'
+ try {
+ const parsed = new URL(value)
+ if (!['http:', 'https:'].includes(parsed.protocol)) {
+ return 'A URL deve começar com http:// ou https://'
+ }
+ return true
+ } catch {
+ return 'Informe uma URL válida'
+ }
+ },
+ },
+ {
+ name: 'label',
+ type: 'text',
+ label: 'Rótulo de acessibilidade',
+ admin: {
+ description:
+ 'Rótulo opcional para acessibilidade. Se estiver vazio, usamos um rótulo padrão da plataforma.',
+ },
+ },
+ {
+ name: 'enabled',
+ type: 'checkbox',
+ label: 'Ativo',
+ },
+ ],
+ },
+ ],
+ hooks: {
+ afterChange: [revalidate],
+ },
+}
diff --git a/src/payload-types.ts b/src/payload-types.ts
index cee4a2a1..40158787 100644
--- a/src/payload-types.ts
+++ b/src/payload-types.ts
@@ -87,8 +87,12 @@ export interface Config {
defaultIDType: number;
};
fallbackLocale: null;
- globals: {};
- globalsSelect: {};
+ globals: {
+ 'site-settings': SiteSetting;
+ };
+ globalsSelect: {
+ 'site-settings': SiteSettingsSelect | SiteSettingsSelect;
+ };
locale: null;
user: User;
jobs: {
@@ -145,6 +149,9 @@ export interface User {
*/
export interface Media {
id: number;
+ /**
+ * Descrição da imagem para acessibilidade.
+ */
alt: string;
updatedAt: string;
createdAt: string;
@@ -312,6 +319,47 @@ export interface PayloadMigrationsSelect {
updatedAt?: T;
createdAt?: T;
}
+/**
+ * This interface was referenced by `Config`'s JSON-Schema
+ * via the `definition` "site-settings".
+ */
+export interface SiteSetting {
+ id: number;
+ headerTitle?: string | null;
+ socialLinks?:
+ | {
+ platform: 'instagram' | 'facebook' | 'youtube';
+ url: string;
+ /**
+ * Rótulo opcional para acessibilidade. Se estiver vazio, usamos um rótulo padrão da plataforma.
+ */
+ label?: string | null;
+ enabled?: boolean | null;
+ id?: string | null;
+ }[]
+ | null;
+ updatedAt?: string | null;
+ createdAt?: string | null;
+}
+/**
+ * This interface was referenced by `Config`'s JSON-Schema
+ * via the `definition` "site-settings_select".
+ */
+export interface SiteSettingsSelect {
+ headerTitle?: T;
+ socialLinks?:
+ | T
+ | {
+ platform?: T;
+ url?: T;
+ label?: T;
+ enabled?: T;
+ id?: T;
+ };
+ updatedAt?: T;
+ createdAt?: T;
+ globalType?: T;
+}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "auth".
diff --git a/src/payload.config.ts b/src/payload.config.ts
index debb2fd9..28eb83c9 100644
--- a/src/payload.config.ts
+++ b/src/payload.config.ts
@@ -7,6 +7,8 @@ import sharp from 'sharp'
import { Users } from './collections/Users'
import { Media } from './collections/Media'
+import { SiteSettings } from './globals/SiteSettings'
+import { pt } from 'payload/i18n/pt'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
@@ -19,6 +21,7 @@ export default buildConfig({
},
},
collections: [Users, Media],
+ globals: [SiteSettings],
editor: lexicalEditor(),
secret: process.env.PAYLOAD_SECRET || '',
typescript: {
@@ -31,4 +34,8 @@ export default buildConfig({
}),
sharp,
plugins: [],
+ i18n: {
+ fallbackLanguage: 'pt', // use 'pt' (not 'pt-BR') for Payload admin language code
+ supportedLanguages: { pt },
+ },
})
diff --git a/src/utilities/getGlobals.ts b/src/utilities/getGlobals.ts
new file mode 100644
index 00000000..a6783f95
--- /dev/null
+++ b/src/utilities/getGlobals.ts
@@ -0,0 +1,26 @@
+import type { Config } from 'src/payload-types'
+
+import configPromise from '@payload-config'
+import { unstable_cache } from 'next/cache'
+import { getPayload } from 'payload'
+
+type Global = keyof Config['globals']
+
+export async function getGlobal(slug: Global, depth = 0) {
+ const payload = await getPayload({ config: configPromise })
+
+ const global = await payload.findGlobal({
+ slug,
+ depth,
+ })
+
+ return global
+}
+
+/**
+ * Returns a unstable_cache function mapped with the cache tag for the slug
+ */
+export const getCachedGlobal = (slug: Global, depth = 0) =>
+ unstable_cache(async () => getGlobal(slug, depth), [slug], {
+ tags: [`global_${slug}`],
+ })