mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-05-09 02:51:46 +00:00
feat(logo): migrar token Logo.dev para runtime server-side
NEXT_PUBLIC_LOGO_DEV_TOKEN renomeado para LOGO_DEV_TOKEN — lido apenas em runtime no servidor. URL construída nos endpoints /api/logo/mapping e /api/logo/search; cliente nunca recebe o token. Novo server.ts com isLogoDevEnabled() e buildLogoDevUrl(). LogoDevProvider (Context) propaga flag `enabled` para Client Components. Build arg removido do Dockerfile e do workflow docker-publish.yml. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -58,7 +58,9 @@ OPENROUTER_API_KEY=
|
|||||||
|
|
||||||
# === Logo.dev (Opcional) ===
|
# === Logo.dev (Opcional) ===
|
||||||
# Logos automáticos de estabelecimentos. Cadastre em https://www.logo.dev
|
# Logos automáticos de estabelecimentos. Cadastre em https://www.logo.dev
|
||||||
# NEXT_PUBLIC_LOGO_DEV_TOKEN — token público (aparece no frontend, ok por design)
|
# Ambas as variáveis são lidas em runtime pelo servidor — basta configurá-las
|
||||||
# LOGO_DEV_SECRET_KEY — chave secreta (apenas server-side, nunca expor)
|
# no host (Coolify, Railway, Docker Compose etc.), sem mexer no CI.
|
||||||
NEXT_PUBLIC_LOGO_DEV_TOKEN=
|
# LOGO_DEV_TOKEN — token público usado pelo servidor para montar a URL da imagem
|
||||||
|
# LOGO_DEV_SECRET_KEY — chave secreta para a Brand Search API (picker de logo)
|
||||||
|
LOGO_DEV_TOKEN=
|
||||||
LOGO_DEV_SECRET_KEY=
|
LOGO_DEV_SECRET_KEY=
|
||||||
2
.github/workflows/docker-publish.yml
vendored
2
.github/workflows/docker-publish.yml
vendored
@@ -85,8 +85,6 @@ jobs:
|
|||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
build-args: |
|
|
||||||
NEXT_PUBLIC_LOGO_DEV_TOKEN=${{ secrets.NEXT_PUBLIC_LOGO_DEV_TOKEN }}
|
|
||||||
|
|
||||||
- name: Image digest
|
- name: Image digest
|
||||||
run: echo ${{ steps.meta.outputs.digest }}
|
run: echo ${{ steps.meta.outputs.digest }}
|
||||||
|
|||||||
@@ -40,9 +40,9 @@ COPY --from=deps /app/public/pdf.worker.min.mjs ./public/pdf.worker.min.mjs
|
|||||||
ENV NEXT_TELEMETRY_DISABLED=1 \
|
ENV NEXT_TELEMETRY_DISABLED=1 \
|
||||||
NODE_ENV=production
|
NODE_ENV=production
|
||||||
|
|
||||||
# Token público do Logo.dev — injetado em build time (NEXT_PUBLIC_* é inlined pelo Next.js)
|
# Nota: a integração Logo.dev não precisa mais de build args. O token
|
||||||
ARG NEXT_PUBLIC_LOGO_DEV_TOKEN
|
# `LOGO_DEV_TOKEN` é lido em runtime no servidor — basta configurá-lo no
|
||||||
ENV NEXT_PUBLIC_LOGO_DEV_TOKEN=$NEXT_PUBLIC_LOGO_DEV_TOKEN
|
# host (Coolify, Railway, etc.) junto com `LOGO_DEV_SECRET_KEY`.
|
||||||
|
|
||||||
# Build da aplicação Next.js
|
# Build da aplicação Next.js
|
||||||
RUN pnpm build
|
RUN pnpm build
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { connection } from "next/server";
|
import { connection } from "next/server";
|
||||||
import { fetchDashboardNavbarData } from "@/features/dashboard/navbar-queries";
|
import { fetchDashboardNavbarData } from "@/features/dashboard/navbar-queries";
|
||||||
import { AppNavbar } from "@/shared/components/navigation/navbar/app-navbar";
|
import { AppNavbar } from "@/shared/components/navigation/navbar/app-navbar";
|
||||||
|
import { LogoDevProvider } from "@/shared/components/providers/logo-dev-provider";
|
||||||
import { PrivacyProvider } from "@/shared/components/providers/privacy-provider";
|
import { PrivacyProvider } from "@/shared/components/providers/privacy-provider";
|
||||||
import { DotPattern } from "@/shared/components/ui/dot-pattern";
|
|
||||||
import { getUserSession } from "@/shared/lib/auth/server";
|
import { getUserSession } from "@/shared/lib/auth/server";
|
||||||
|
import { isLogoDevEnabled } from "@/shared/lib/logo/server";
|
||||||
|
|
||||||
export default async function DashboardLayout({
|
export default async function DashboardLayout({
|
||||||
children,
|
children,
|
||||||
@@ -13,33 +14,25 @@ export default async function DashboardLayout({
|
|||||||
await connection();
|
await connection();
|
||||||
const session = await getUserSession();
|
const session = await getUserSession();
|
||||||
const navbarData = await fetchDashboardNavbarData(session.user.id);
|
const navbarData = await fetchDashboardNavbarData(session.user.id);
|
||||||
|
const logoDevEnabled = isLogoDevEnabled();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PrivacyProvider>
|
<LogoDevProvider enabled={logoDevEnabled}>
|
||||||
<AppNavbar
|
<PrivacyProvider>
|
||||||
user={{ ...session.user, image: session.user.image ?? null }}
|
<AppNavbar
|
||||||
pagadorAvatarUrl={navbarData.pagadorAvatarUrl}
|
user={{ ...session.user, image: session.user.image ?? null }}
|
||||||
preLancamentosCount={navbarData.preLancamentosCount}
|
pagadorAvatarUrl={navbarData.pagadorAvatarUrl}
|
||||||
notificationsSnapshot={navbarData.notificationsSnapshot}
|
preLancamentosCount={navbarData.preLancamentosCount}
|
||||||
/>
|
notificationsSnapshot={navbarData.notificationsSnapshot}
|
||||||
<div className="relative flex flex-1 flex-col pt-16">
|
/>
|
||||||
<div className="pointer-events-none absolute inset-x-0 top-0 h-32 overflow-hidden md:h-36">
|
<div className="relative flex flex-1 flex-col pt-16">
|
||||||
<DotPattern
|
<div className="@container/main flex flex-1 flex-col gap-2">
|
||||||
width={20}
|
<div className="flex flex-col gap-4 py-5 md:gap-6 w-full max-w-8xl mx-auto px-4 ">
|
||||||
height={20}
|
{children}
|
||||||
cx={1.25}
|
</div>
|
||||||
cy={1.25}
|
|
||||||
cr={1.25}
|
|
||||||
className="text-primary/10 mask-[linear-gradient(to_bottom,black_0%,transparent_100%)]"
|
|
||||||
/>
|
|
||||||
<div className="absolute inset-0 bg-linear-to-b from-primary/6 to-transparent" />
|
|
||||||
</div>
|
|
||||||
<div className="@container/main flex flex-1 flex-col gap-2">
|
|
||||||
<div className="flex flex-col gap-4 py-5 md:gap-6 w-full max-w-8xl mx-auto px-4 ">
|
|
||||||
{children}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</PrivacyProvider>
|
||||||
</PrivacyProvider>
|
</LogoDevProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,29 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { getOptionalUserSession } from "@/shared/lib/auth/server";
|
import { getOptionalUserSession } from "@/shared/lib/auth/server";
|
||||||
import { fetchEstablishmentLogoDomain } from "@/shared/lib/logo/establishment-logo-queries";
|
import { fetchEstablishmentLogoDomain } from "@/shared/lib/logo/establishment-logo-queries";
|
||||||
|
import { buildLogoDevUrl } from "@/shared/lib/logo/server";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/logo/mapping?name={name}
|
* GET /api/logo/mapping?name={name}
|
||||||
*
|
*
|
||||||
* Retorna o domínio Logo.dev salvo pelo usuário para um estabelecimento.
|
* Retorna o domínio Logo.dev salvo pelo usuário para um estabelecimento,
|
||||||
* Usado pelo EstablishmentLogo para hidratar o domain salvo no banco.
|
* junto com a `logoUrl` final (construída server-side com o token). O
|
||||||
|
* cliente usa `logoUrl` diretamente — sem precisar conhecer o token.
|
||||||
*/
|
*/
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
const session = await getOptionalUserSession();
|
const session = await getOptionalUserSession();
|
||||||
if (!session) {
|
if (!session) {
|
||||||
return NextResponse.json({ domain: null }, { status: 200 });
|
return NextResponse.json({ domain: null, logoUrl: null }, { status: 200 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
const name = searchParams.get("name")?.trim();
|
const name = searchParams.get("name")?.trim();
|
||||||
|
|
||||||
if (!name) {
|
if (!name) {
|
||||||
return NextResponse.json({ domain: null }, { status: 200 });
|
return NextResponse.json({ domain: null, logoUrl: null }, { status: 200 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const domain = await fetchEstablishmentLogoDomain(session.user.id, name);
|
const domain = await fetchEstablishmentLogoDomain(session.user.id, name);
|
||||||
return NextResponse.json({ domain });
|
const logoUrl = buildLogoDevUrl(domain);
|
||||||
|
return NextResponse.json({ domain, logoUrl });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { getOptionalUserSession } from "@/shared/lib/auth/server";
|
import { getOptionalUserSession } from "@/shared/lib/auth/server";
|
||||||
|
import { buildLogoDevUrl } from "@/shared/lib/logo/server";
|
||||||
|
|
||||||
const LOGO_DEV_SEARCH_URL = "https://api.logo.dev/search";
|
const LOGO_DEV_SEARCH_URL = "https://api.logo.dev/search";
|
||||||
|
|
||||||
@@ -8,6 +9,10 @@ interface LogoResult {
|
|||||||
domain: string;
|
domain: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface LogoResultWithUrl extends LogoResult {
|
||||||
|
logoUrl: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
async function searchByStrategy(
|
async function searchByStrategy(
|
||||||
q: string,
|
q: string,
|
||||||
strategy: "match" | "typeahead",
|
strategy: "match" | "typeahead",
|
||||||
@@ -66,12 +71,14 @@ export async function GET(request: Request) {
|
|||||||
|
|
||||||
// Mescla e deduplica por domain, mantendo ordem (match tem prioridade)
|
// Mescla e deduplica por domain, mantendo ordem (match tem prioridade)
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
const merged: LogoResult[] = [];
|
const merged: LogoResultWithUrl[] = [];
|
||||||
|
|
||||||
for (const result of [...matchResults, ...typeaheadResults]) {
|
for (const result of [...matchResults, ...typeaheadResults]) {
|
||||||
if (!seen.has(result.domain)) {
|
if (!seen.has(result.domain)) {
|
||||||
seen.add(result.domain);
|
seen.add(result.domain);
|
||||||
merged.push(result);
|
// logoUrl é construída server-side com o token — o cliente nunca
|
||||||
|
// precisa conhecer LOGO_DEV_TOKEN para renderizar a imagem.
|
||||||
|
merged.push({ ...result, logoUrl: buildLogoDevUrl(result.domain) });
|
||||||
if (merged.length >= 20) break;
|
if (merged.length >= 20) break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const sizeVariants = {
|
|||||||
sm: {
|
sm: {
|
||||||
container: "size-8",
|
container: "size-8",
|
||||||
icon: "size-4",
|
icon: "size-4",
|
||||||
text: "text-[10px]",
|
text: "text-xs",
|
||||||
},
|
},
|
||||||
md: {
|
md: {
|
||||||
container: "size-9",
|
container: "size-9",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from "@/shared/components/ui/popover";
|
} from "@/shared/components/ui/popover";
|
||||||
import { Spinner } from "@/shared/components/ui/spinner";
|
import { Spinner } from "@/shared/components/ui/spinner";
|
||||||
import { buildLogoDevUrl, logoQueryKeys, toNameKey } from "@/shared/lib/logo";
|
import { logoQueryKeys, toNameKey } from "@/shared/lib/logo";
|
||||||
import {
|
import {
|
||||||
removeEstablishmentLogoAction,
|
removeEstablishmentLogoAction,
|
||||||
saveEstablishmentLogoAction,
|
saveEstablishmentLogoAction,
|
||||||
@@ -24,6 +24,8 @@ import { cn } from "@/shared/utils/ui";
|
|||||||
interface LogoResult {
|
interface LogoResult {
|
||||||
name: string;
|
name: string;
|
||||||
domain: string;
|
domain: string;
|
||||||
|
/** URL da imagem construída server-side — cliente usa direto sem token. */
|
||||||
|
logoUrl: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchLogoResults(query: string): Promise<LogoResult[]> {
|
async function fetchLogoResults(query: string): Promise<LogoResult[]> {
|
||||||
@@ -77,13 +79,14 @@ export function EstablishmentLogoPicker({
|
|||||||
staleTime: 1000 * 60 * 60,
|
staleTime: 1000 * 60 * 60,
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleSelect(domain: string) {
|
function handleSelect(result: LogoResult) {
|
||||||
startTransition(async () => {
|
startTransition(async () => {
|
||||||
await saveEstablishmentLogoAction(name, domain);
|
await saveEstablishmentLogoAction(name, result.domain);
|
||||||
queryClient.setQueryData(logoQueryKeys.mapping(toNameKey(name)), {
|
queryClient.setQueryData(logoQueryKeys.mapping(toNameKey(name)), {
|
||||||
domain,
|
domain: result.domain,
|
||||||
|
logoUrl: result.logoUrl,
|
||||||
});
|
});
|
||||||
onSelect(domain);
|
onSelect(result.domain);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,6 +95,7 @@ export function EstablishmentLogoPicker({
|
|||||||
await removeEstablishmentLogoAction(name);
|
await removeEstablishmentLogoAction(name);
|
||||||
queryClient.setQueryData(logoQueryKeys.mapping(toNameKey(name)), {
|
queryClient.setQueryData(logoQueryKeys.mapping(toNameKey(name)), {
|
||||||
domain: null,
|
domain: null,
|
||||||
|
logoUrl: null,
|
||||||
});
|
});
|
||||||
onSelect(null);
|
onSelect(null);
|
||||||
});
|
});
|
||||||
@@ -143,7 +147,7 @@ export function EstablishmentLogoPicker({
|
|||||||
>
|
>
|
||||||
{buildInitials(name)}
|
{buildInitials(name)}
|
||||||
</div>
|
</div>
|
||||||
<span className="w-full truncate text-[10px] leading-tight text-muted-foreground">
|
<span className="w-full truncate text-xs leading-tight text-muted-foreground">
|
||||||
Iniciais
|
Iniciais
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -153,7 +157,7 @@ export function EstablishmentLogoPicker({
|
|||||||
key={r.domain}
|
key={r.domain}
|
||||||
type="button"
|
type="button"
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
onClick={() => handleSelect(r.domain)}
|
onClick={() => handleSelect(r)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col items-center gap-1 rounded-md p-1.5 text-center transition-colors hover:bg-accent disabled:opacity-50",
|
"flex flex-col items-center gap-1 rounded-md p-1.5 text-center transition-colors hover:bg-accent disabled:opacity-50",
|
||||||
resolvedDomain === r.domain &&
|
resolvedDomain === r.domain &&
|
||||||
@@ -161,9 +165,8 @@ export function EstablishmentLogoPicker({
|
|||||||
)}
|
)}
|
||||||
title={r.name}
|
title={r.name}
|
||||||
>
|
>
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
||||||
<img
|
<img
|
||||||
src={buildLogoDevUrl(r.domain) ?? ""}
|
src={r.logoUrl ?? ""}
|
||||||
alt={r.name}
|
alt={r.name}
|
||||||
width={36}
|
width={36}
|
||||||
height={36}
|
height={36}
|
||||||
@@ -173,7 +176,7 @@ export function EstablishmentLogoPicker({
|
|||||||
(e.target as HTMLImageElement).style.display = "none";
|
(e.target as HTMLImageElement).style.display = "none";
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span className="w-full truncate text-[10px] leading-tight">
|
<span className="w-full truncate text-xs leading-tight">
|
||||||
{r.name}
|
{r.name}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -2,13 +2,9 @@
|
|||||||
|
|
||||||
import { RiPencilLine } from "@remixicon/react";
|
import { RiPencilLine } from "@remixicon/react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useEffect, useState } from "react";
|
import { useState } from "react";
|
||||||
import {
|
import { useLogoDevEnabled } from "@/shared/components/providers/logo-dev-provider";
|
||||||
buildLogoDevUrl,
|
import { logoQueryKeys, toNameKey } from "@/shared/lib/logo";
|
||||||
LOGO_DEV_TOKEN,
|
|
||||||
logoQueryKeys,
|
|
||||||
toNameKey,
|
|
||||||
} from "@/shared/lib/logo";
|
|
||||||
import {
|
import {
|
||||||
buildInitials,
|
buildInitials,
|
||||||
getCategoryBgColorFromName,
|
getCategoryBgColorFromName,
|
||||||
@@ -17,11 +13,14 @@ import {
|
|||||||
import { cn } from "@/shared/utils/ui";
|
import { cn } from "@/shared/utils/ui";
|
||||||
import { EstablishmentLogoPicker } from "./establishment-logo-picker";
|
import { EstablishmentLogoPicker } from "./establishment-logo-picker";
|
||||||
|
|
||||||
async function fetchLogoMapping(
|
interface LogoMappingResponse {
|
||||||
name: string,
|
domain: string | null;
|
||||||
): Promise<{ domain: string | null }> {
|
logoUrl: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchLogoMapping(name: string): Promise<LogoMappingResponse> {
|
||||||
const res = await fetch(`/api/logo/mapping?name=${encodeURIComponent(name)}`);
|
const res = await fetch(`/api/logo/mapping?name=${encodeURIComponent(name)}`);
|
||||||
if (!res.ok) return { domain: null };
|
if (!res.ok) return { domain: null, logoUrl: null };
|
||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,6 +28,8 @@ interface EstablishmentLogoProps {
|
|||||||
name: string;
|
name: string;
|
||||||
/** Domínio Logo.dev pré-carregado pelo servidor (otimização opcional). */
|
/** Domínio Logo.dev pré-carregado pelo servidor (otimização opcional). */
|
||||||
domain?: string | null;
|
domain?: string | null;
|
||||||
|
/** URL pré-construída no servidor — evita flicker no primeiro render. */
|
||||||
|
logoUrl?: string | null;
|
||||||
size?: number;
|
size?: number;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
@@ -36,31 +37,33 @@ interface EstablishmentLogoProps {
|
|||||||
export function EstablishmentLogo({
|
export function EstablishmentLogo({
|
||||||
name,
|
name,
|
||||||
domain: initialDomain,
|
domain: initialDomain,
|
||||||
|
logoUrl: initialLogoUrl,
|
||||||
size = 32,
|
size = 32,
|
||||||
className,
|
className,
|
||||||
}: EstablishmentLogoProps) {
|
}: EstablishmentLogoProps) {
|
||||||
|
const logoDevEnabled = useLogoDevEnabled();
|
||||||
const [pickerOpen, setPickerOpen] = useState(false);
|
const [pickerOpen, setPickerOpen] = useState(false);
|
||||||
const [imgError, setImgError] = useState(false);
|
const [imgError, setImgError] = useState(false);
|
||||||
|
|
||||||
|
const hasPlaceholder =
|
||||||
|
initialDomain !== undefined || initialLogoUrl !== undefined;
|
||||||
|
|
||||||
const { data: mappingData } = useQuery({
|
const { data: mappingData } = useQuery({
|
||||||
queryKey: logoQueryKeys.mapping(toNameKey(name)),
|
queryKey: logoQueryKeys.mapping(toNameKey(name)),
|
||||||
queryFn: () => fetchLogoMapping(name),
|
queryFn: () => fetchLogoMapping(name),
|
||||||
placeholderData:
|
placeholderData: hasPlaceholder
|
||||||
initialDomain !== undefined
|
? {
|
||||||
? { domain: initialDomain ?? null }
|
domain: initialDomain ?? null,
|
||||||
: undefined,
|
logoUrl: initialLogoUrl ?? null,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
staleTime: 1000 * 60 * 5,
|
staleTime: 1000 * 60 * 5,
|
||||||
enabled: LOGO_DEV_TOKEN !== undefined && LOGO_DEV_TOKEN !== "",
|
enabled: logoDevEnabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
const resolvedDomain = mappingData?.domain ?? null;
|
const resolvedDomain = mappingData?.domain ?? null;
|
||||||
|
const logoUrl = mappingData?.logoUrl ?? null;
|
||||||
|
|
||||||
// biome-ignore lint/correctness/useExhaustiveDependencies: resetar imgError é o efeito de domain mudar
|
|
||||||
useEffect(() => {
|
|
||||||
setImgError(false);
|
|
||||||
}, [resolvedDomain]);
|
|
||||||
|
|
||||||
const logoUrl = buildLogoDevUrl(resolvedDomain);
|
|
||||||
const showLogo = Boolean(logoUrl) && !imgError;
|
const showLogo = Boolean(logoUrl) && !imgError;
|
||||||
|
|
||||||
const initials = buildInitials(name);
|
const initials = buildInitials(name);
|
||||||
@@ -85,7 +88,6 @@ export function EstablishmentLogo({
|
|||||||
|
|
||||||
const logoImage =
|
const logoImage =
|
||||||
showLogo && logoUrl ? (
|
showLogo && logoUrl ? (
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
|
||||||
<img
|
<img
|
||||||
src={logoUrl}
|
src={logoUrl}
|
||||||
alt={name}
|
alt={name}
|
||||||
@@ -99,7 +101,7 @@ export function EstablishmentLogo({
|
|||||||
initialsAvatar
|
initialsAvatar
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!LOGO_DEV_TOKEN) {
|
if (!logoDevEnabled) {
|
||||||
return (
|
return (
|
||||||
<div className={cn("shrink-0", className)} aria-hidden>
|
<div className={cn("shrink-0", className)} aria-hidden>
|
||||||
{initialsAvatar}
|
{initialsAvatar}
|
||||||
|
|||||||
29
src/shared/components/providers/logo-dev-provider.tsx
Normal file
29
src/shared/components/providers/logo-dev-provider.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { createContext, useContext } from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expõe, para Client Components, se a integração Logo.dev está configurada.
|
||||||
|
*
|
||||||
|
* O valor é determinado server-side (lendo `LOGO_DEV_TOKEN` via
|
||||||
|
* `isLogoDevEnabled()` em `@/shared/lib/logo/server`) e passado como prop
|
||||||
|
* a partir do layout do dashboard — evitando que o cliente precise do token.
|
||||||
|
*/
|
||||||
|
const LogoDevContext = createContext<boolean>(false);
|
||||||
|
|
||||||
|
interface LogoDevProviderProps {
|
||||||
|
enabled: boolean;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LogoDevProvider({ enabled, children }: LogoDevProviderProps) {
|
||||||
|
return (
|
||||||
|
<LogoDevContext.Provider value={enabled}>
|
||||||
|
{children}
|
||||||
|
</LogoDevContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useLogoDevEnabled(): boolean {
|
||||||
|
return useContext(LogoDevContext);
|
||||||
|
}
|
||||||
@@ -1,14 +1,5 @@
|
|||||||
/**
|
|
||||||
* Logo utilities
|
|
||||||
*
|
|
||||||
* Consolidated from:
|
|
||||||
* - /lib/logo.ts (utility functions)
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalizes logo path to get just the filename
|
* Normalizes logo path to get just the filename
|
||||||
* @param logo - Logo path or URL
|
|
||||||
* @returns Filename only
|
|
||||||
*/
|
*/
|
||||||
export const normalizeLogo = (logo?: string | null) =>
|
export const normalizeLogo = (logo?: string | null) =>
|
||||||
logo?.split("/").filter(Boolean).pop() ?? "";
|
logo?.split("/").filter(Boolean).pop() ?? "";
|
||||||
@@ -45,13 +36,11 @@ export const deriveNameFromLogo = (logo?: string | null) => {
|
|||||||
export const toNameKey = (name: string): string => name.trim().toLowerCase();
|
export const toNameKey = (name: string): string => name.trim().toLowerCase();
|
||||||
|
|
||||||
// === Logo.dev ===
|
// === Logo.dev ===
|
||||||
|
//
|
||||||
export const LOGO_DEV_TOKEN = process.env.NEXT_PUBLIC_LOGO_DEV_TOKEN;
|
// A construção de URLs e a leitura do token acontecem server-side em
|
||||||
|
// `./server.ts`. O cliente consome `logoUrl` pré-construída a partir das
|
||||||
export function buildLogoDevUrl(domain?: string | null): string | null {
|
// API routes (`/api/logo/mapping` e `/api/logo/search`) e usa o
|
||||||
if (!LOGO_DEV_TOKEN || !domain) return null;
|
// `LogoDevProvider` para saber se a integração está habilitada.
|
||||||
return `https://img.logo.dev/${domain}?token=${LOGO_DEV_TOKEN}&size=64&format=png`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const logoQueryKeys = {
|
export const logoQueryKeys = {
|
||||||
mapping: (nameKey: string) => ["logo-mapping", nameKey] as const,
|
mapping: (nameKey: string) => ["logo-mapping", nameKey] as const,
|
||||||
|
|||||||
30
src/shared/lib/logo/server.ts
Normal file
30
src/shared/lib/logo/server.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* Helpers server-only para Logo.dev.
|
||||||
|
*
|
||||||
|
* IMPORTANTE: este módulo lê `process.env.LOGO_DEV_TOKEN`, que não existe
|
||||||
|
* no bundle do cliente. Nunca importe este arquivo de Client Components
|
||||||
|
* — use o `LogoDevProvider` para propagar o estado `enabled` e consuma
|
||||||
|
* `logoUrl` a partir das respostas das API routes.
|
||||||
|
*/
|
||||||
|
function getLogoDevToken(): string | undefined {
|
||||||
|
const token = process.env.LOGO_DEV_TOKEN;
|
||||||
|
return token && token.length > 0 ? token : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indica se a integração Logo.dev está configurada.
|
||||||
|
* Usado para habilitar o picker e a exibição de logos na UI.
|
||||||
|
*/
|
||||||
|
export function isLogoDevEnabled(): boolean {
|
||||||
|
return getLogoDevToken() !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constrói a URL final da imagem Logo.dev com o token aplicado server-side.
|
||||||
|
* Retorna null se o token não estiver configurado ou se o domínio for vazio.
|
||||||
|
*/
|
||||||
|
export function buildLogoDevUrl(domain?: string | null): string | null {
|
||||||
|
const token = getLogoDevToken();
|
||||||
|
if (!token || !domain) return null;
|
||||||
|
return `https://img.logo.dev/${domain}?token=${token}&size=64&format=png`;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user