Przeglądaj źródła

Use organization logo for all public pages (#103)

* workorder dates

* service details

* added set paid button in sidebar

* cleanup

* video upload

* lint

* public status report

* drawer

* translation

* translations

* translations

* cleanup

* translations

* report page

* hydration

* use customer logo on public pages
Bernt Christian Egeland 6 miesięcy temu
rodzic
commit
049714b230

+ 2 - 9
src/app/(public)/auth/forgot-password/page.tsx

@@ -2,13 +2,13 @@
 
 import { useState } from 'react'
 import Link from 'next/link'
-import Image from 'next/image'
 import { useTranslations } from 'next-intl'
 import { authClient } from '@/lib/auth-client'
 import { Button } from '@/components/ui/button'
 import { Input } from '@/components/ui/input'
 import { Label } from '@/components/ui/label'
 import { Loader2, ArrowLeft } from 'lucide-react'
+import { AuthLogo } from '@/components/auth-logo'
 
 export default function ForgotPasswordPage() {
   const t = useTranslations('auth.forgotPassword')
@@ -44,14 +44,7 @@ export default function ForgotPasswordPage() {
       <div className="glass relative z-10 w-full max-w-md rounded-2xl p-8 shadow-2xl">
         <div className="mb-8 text-center">
           <div className="mb-4 inline-flex items-center gap-2">
-            <Image
-              src="/torqvoice_app_logo.png"
-              alt={tc('brandName')}
-              width={48}
-              height={44}
-              className="h-11 w-auto"
-              priority
-            />
+            <AuthLogo alt={tc('brandName')} />
           </div>
           <h1 className="text-2xl font-bold tracking-tight">{t('title')}</h1>
           <p className="mt-1 text-sm text-muted-foreground">

+ 14 - 2
src/app/(public)/auth/layout.tsx

@@ -1,7 +1,19 @@
-export default function AuthLayout({
+import { db } from "@/lib/db";
+import { AuthLogoProvider } from "@/components/auth-logo-provider";
+
+export default async function AuthLayout({
   children,
 }: {
   children: React.ReactNode;
 }) {
-  return <>{children}</>;
+  const logoSetting = await db.appSetting.findFirst({
+    where: { key: "workshop.logo" },
+    select: { id: true },
+  });
+
+  return (
+    <AuthLogoProvider hasCustomLogo={!!logoSetting}>
+      {children}
+    </AuthLogoProvider>
+  );
 }

+ 2 - 9
src/app/(public)/auth/reset-password/page.tsx

@@ -3,13 +3,13 @@
 import { Suspense, useState } from 'react'
 import { useSearchParams } from 'next/navigation'
 import Link from 'next/link'
-import Image from 'next/image'
 import { useTranslations } from 'next-intl'
 import { authClient } from '@/lib/auth-client'
 import { Button } from '@/components/ui/button'
 import { Input } from '@/components/ui/input'
 import { Label } from '@/components/ui/label'
 import { Loader2, XCircle } from 'lucide-react'
+import { AuthLogo } from '@/components/auth-logo'
 
 function ResetPasswordInner() {
   const t = useTranslations('auth.resetPassword')
@@ -145,14 +145,7 @@ export default function ResetPasswordPage() {
       <div className="glass relative z-10 w-full max-w-md rounded-2xl p-8 shadow-2xl">
         <div className="mb-8 text-center">
           <div className="mb-4 inline-flex items-center gap-2">
-            <Image
-              src="/torqvoice_app_logo.png"
-              alt={tc('brandName')}
-              width={48}
-              height={44}
-              className="h-11 w-auto"
-              priority
-            />
+            <AuthLogo alt={tc('brandName')} />
           </div>
           <h1 className="text-2xl font-bold tracking-tight">{t('title')}</h1>
           <p className="mt-1 text-sm text-muted-foreground">{t('description')}</p>

+ 2 - 9
src/app/(public)/auth/sign-in/sign-in-form.tsx

@@ -3,7 +3,6 @@
 import { Suspense, useState } from 'react'
 import { useRouter, useSearchParams } from 'next/navigation'
 import Link from 'next/link'
-import Image from 'next/image'
 import { useTranslations } from 'next-intl'
 import { signIn, authClient } from '@/lib/auth-client'
 import { Button } from '@/components/ui/button'
@@ -11,6 +10,7 @@ import { Input } from '@/components/ui/input'
 import { Label } from '@/components/ui/label'
 import { Separator } from '@/components/ui/separator'
 import { Fingerprint, Loader2, XCircle } from 'lucide-react'
+import { AuthLogo } from '@/components/auth-logo'
 
 function SignInFormInner({ registrationDisabled }: { registrationDisabled: boolean }) {
   const t = useTranslations('auth.signIn')
@@ -71,14 +71,7 @@ function SignInFormInner({ registrationDisabled }: { registrationDisabled: boole
     <div className="glass relative z-10 w-full max-w-md rounded-2xl p-8 shadow-2xl">
       <div className="mb-8 text-center">
         <div className="mb-4 inline-flex items-center gap-2">
-          <Image
-            src="/torqvoice_app_logo.png"
-            alt={tc('brandName')}
-            width={48}
-            height={44}
-            className="h-11 w-auto"
-            priority
-          />
+          <AuthLogo alt={tc('brandName')} />
         </div>
         <h1 className="text-2xl font-bold tracking-tight">{t('title')}</h1>
         <p className="mt-1 text-sm text-muted-foreground">{t('description')}</p>

+ 2 - 9
src/app/(public)/auth/sign-up/sign-up-form.tsx

@@ -3,13 +3,13 @@
 import { useState } from 'react'
 import { useRouter } from 'next/navigation'
 import Link from 'next/link'
-import Image from 'next/image'
 import { useTranslations } from 'next-intl'
 import { signUp } from '@/lib/auth-client'
 import { Button } from '@/components/ui/button'
 import { Input } from '@/components/ui/input'
 import { Label } from '@/components/ui/label'
 import { Loader2, XCircle } from 'lucide-react'
+import { AuthLogo } from '@/components/auth-logo'
 import { acceptInvitation } from '@/features/team/Actions/acceptInvitation'
 
 export function SignUpForm({ inviteToken, emailVerificationRequired, redirectTo }: { inviteToken?: string; emailVerificationRequired?: boolean; redirectTo?: string }) {
@@ -75,14 +75,7 @@ export function SignUpForm({ inviteToken, emailVerificationRequired, redirectTo
       <div className="glass relative z-10 w-full max-w-md rounded-2xl p-8 shadow-2xl">
         <div className="mb-8 text-center">
           <div className="mb-4 inline-flex items-center gap-2">
-            <Image
-              src="/torqvoice_app_logo.png"
-              alt={tc('brandName')}
-              width={48}
-              height={44}
-              className="h-11 w-auto"
-              priority
-            />
+            <AuthLogo alt={tc('brandName')} />
           </div>
           <h1 className="text-2xl font-bold tracking-tight">{t('title')}</h1>
           <p className="mt-1 text-sm text-muted-foreground">

+ 2 - 9
src/app/(public)/auth/verify-2fa/page.tsx

@@ -2,7 +2,6 @@
 
 import { useState } from 'react'
 import { useRouter } from 'next/navigation'
-import Image from 'next/image'
 import { useTranslations } from 'next-intl'
 import { authClient } from '@/lib/auth-client'
 import { Button } from '@/components/ui/button'
@@ -10,6 +9,7 @@ import { Input } from '@/components/ui/input'
 import { Label } from '@/components/ui/label'
 import { Checkbox } from '@/components/ui/checkbox'
 import { Loader2, Shield, XCircle } from 'lucide-react'
+import { AuthLogo } from '@/components/auth-logo'
 
 export default function VerifyTwoFactorPage() {
   const t = useTranslations('auth.verify2fa')
@@ -55,14 +55,7 @@ export default function VerifyTwoFactorPage() {
       <div className="glass relative z-10 w-full max-w-md rounded-2xl p-8 shadow-2xl">
         <div className="mb-8 text-center">
           <div className="mb-4 inline-flex items-center gap-2">
-            <Image
-              src="/torqvoice_app_logo.png"
-              alt={tc('brandName')}
-              width={48}
-              height={44}
-              className="h-11 w-auto"
-              priority
-            />
+            <AuthLogo alt={tc('brandName')} />
           </div>
           <div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-primary/10">
             <Shield className="h-6 w-6 text-primary" />

+ 2 - 9
src/app/(public)/auth/verify-email/page.tsx

@@ -1,13 +1,13 @@
 'use client'
 
 import { useState, useCallback, useEffect, useRef } from 'react'
-import Image from 'next/image'
 import { useTranslations } from 'next-intl'
 import { authClient, signOut, useSession } from '@/lib/auth-client'
 import { Button } from '@/components/ui/button'
 import { Loader2, Mail, LogOut } from 'lucide-react'
 import { useRouter } from 'next/navigation'
 import { useCooldown } from '@/hooks/use-cooldown'
+import { AuthLogo } from '@/components/auth-logo'
 
 const POLL_INTERVAL = 5000
 
@@ -90,14 +90,7 @@ export default function VerifyEmailPage() {
       <div className="glass relative z-10 w-full max-w-md rounded-2xl p-8 shadow-2xl">
         <div className="mb-8 text-center">
           <div className="mb-4 inline-flex items-center gap-2">
-            <Image
-              src="/torqvoice_app_logo.png"
-              alt={tc('brandName')}
-              width={48}
-              height={44}
-              className="h-11 w-auto"
-              priority
-            />
+            <AuthLogo alt={tc('brandName')} />
           </div>
           <div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-primary/10">
             <Mail className="h-8 w-8 text-primary" />

+ 58 - 0
src/app/api/public/logo/route.ts

@@ -0,0 +1,58 @@
+import { NextResponse } from "next/server";
+import { db } from "@/lib/db";
+import { readFile, stat } from "fs/promises";
+import path from "path";
+
+const MIME_TYPES: Record<string, string> = {
+  jpg: "image/jpeg",
+  jpeg: "image/jpeg",
+  png: "image/png",
+  webp: "image/webp",
+  avif: "image/avif",
+  svg: "image/svg+xml",
+};
+
+export async function GET() {
+  const logoSetting = await db.appSetting.findFirst({
+    where: { key: "workshop.logo" },
+    select: { value: true, organizationId: true },
+  });
+
+  if (!logoSetting?.value || !logoSetting.organizationId) {
+    return NextResponse.json({ error: "No logo" }, { status: 404 });
+  }
+
+  // Extract filename from stored URL like /api/protected/files/orgId/logos/filename
+  const urlParts = logoSetting.value.split("/");
+  const filename = urlParts[urlParts.length - 1];
+
+  if (!filename || filename.includes("..") || filename.includes("/") || filename.includes("\\")) {
+    return NextResponse.json({ error: "Invalid" }, { status: 400 });
+  }
+
+  const filePath = path.join(
+    process.cwd(),
+    "data",
+    "uploads",
+    logoSetting.organizationId,
+    "logos",
+    filename,
+  );
+
+  try {
+    await stat(filePath);
+  } catch {
+    return NextResponse.json({ error: "Not found" }, { status: 404 });
+  }
+
+  const buffer = await readFile(filePath);
+  const ext = filename.split(".").pop()?.toLowerCase() || "";
+  const contentType = MIME_TYPES[ext] || "image/png";
+
+  return new NextResponse(buffer, {
+    headers: {
+      "Content-Type": contentType,
+      "Cache-Control": "public, max-age=3600",
+    },
+  });
+}

+ 23 - 0
src/components/auth-logo-provider.tsx

@@ -0,0 +1,23 @@
+"use client";
+
+import { createContext, useContext } from "react";
+
+const AuthLogoContext = createContext(false);
+
+export function AuthLogoProvider({
+  hasCustomLogo,
+  children,
+}: {
+  hasCustomLogo: boolean;
+  children: React.ReactNode;
+}) {
+  return (
+    <AuthLogoContext value={hasCustomLogo}>
+      {children}
+    </AuthLogoContext>
+  );
+}
+
+export function useHasCustomLogo() {
+  return useContext(AuthLogoContext);
+}

+ 30 - 0
src/components/auth-logo.tsx

@@ -0,0 +1,30 @@
+"use client";
+
+import Image from "next/image";
+import { useHasCustomLogo } from "./auth-logo-provider";
+
+export function AuthLogo({ alt }: { alt: string }) {
+  const hasCustomLogo = useHasCustomLogo();
+
+  if (hasCustomLogo) {
+    return (
+      // eslint-disable-next-line @next/next/no-img-element
+      <img
+        src="/api/public/logo"
+        alt={alt}
+        className="h-11 w-auto"
+      />
+    );
+  }
+
+  return (
+    <Image
+      src="/torqvoice_app_logo.png"
+      alt={alt}
+      width={48}
+      height={44}
+      className="h-11 w-auto"
+      priority
+    />
+  );
+}