mirror of
https://github.com/Since-3/since3-portal.git
synced 2026-10-12 20:51:17 +00:00
mobile responsive
This commit is contained in:
parent
496ad28723
commit
b6f741be8f
30 changed files with 245 additions and 159 deletions
|
|
@ -1,7 +1,8 @@
|
||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
|
import { SidebarProvider } from "@/components/ui/sidebar";
|
||||||
import { verifySession } from "@/lib/supabase/dal";
|
import { verifySession } from "@/lib/supabase/dal";
|
||||||
import { AppSidebar } from "@/modules/shared/ui/components/app-sidebar";
|
import { AppSidebar } from "@/modules/shared/ui/components/app-sidebar";
|
||||||
|
import { MobileHeader } from "@/modules/shared/ui/components/mobile-header";
|
||||||
import { TermsDialog } from "@/modules/shared/ui/components/terms-dialog";
|
import { TermsDialog } from "@/modules/shared/ui/components/terms-dialog";
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -14,11 +15,9 @@ export default async function AdminLayout({ children }: { children: React.ReactN
|
||||||
<SidebarProvider defaultOpen={defaultOpen}>
|
<SidebarProvider defaultOpen={defaultOpen}>
|
||||||
{!session.isAdmin && !session.hasAcceptedTerms && <TermsDialog/>}
|
{!session.isAdmin && !session.hasAcceptedTerms && <TermsDialog/>}
|
||||||
<AppSidebar session={session}/>
|
<AppSidebar session={session}/>
|
||||||
<div className="flex w-full flex-col">
|
<div className="flex min-w-0 flex-1 flex-col">
|
||||||
<div className="flex items-center border-b border-border p-3 md:hidden">
|
<MobileHeader />
|
||||||
<SidebarTrigger />
|
<main className="flex-1 px-4 py-6 sm:px-6 md:p-8">{children}</main>
|
||||||
</div>
|
|
||||||
<main className="flex-1 p-4 md:p-8">{children}</main>
|
|
||||||
</div>
|
</div>
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
|
import { SidebarProvider } from "@/components/ui/sidebar";
|
||||||
import { verifySession } from "@/lib/supabase/dal";
|
import { verifySession } from "@/lib/supabase/dal";
|
||||||
import { AppSidebar } from "@/modules/shared/ui/components/app-sidebar";
|
import { AppSidebar } from "@/modules/shared/ui/components/app-sidebar";
|
||||||
|
import { MobileHeader } from "@/modules/shared/ui/components/mobile-header";
|
||||||
import { TermsDialog } from "@/modules/shared/ui/components/terms-dialog";
|
import { TermsDialog } from "@/modules/shared/ui/components/terms-dialog";
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -17,11 +18,9 @@ export default async function ClientLayout({ children }: { children: React.React
|
||||||
return (
|
return (
|
||||||
<SidebarProvider defaultOpen={defaultOpen}>
|
<SidebarProvider defaultOpen={defaultOpen}>
|
||||||
<AppSidebar session={session}/>
|
<AppSidebar session={session}/>
|
||||||
<div className="flex w-full flex-col">
|
<div className="flex min-w-0 flex-1 flex-col">
|
||||||
<div className="flex items-center border-b border-border p-3 md:hidden">
|
<MobileHeader />
|
||||||
<SidebarTrigger />
|
<main className="flex-1 px-4 py-6 sm:px-6 md:p-8">{children}</main>
|
||||||
</div>
|
|
||||||
<main className="flex-1 p-4 md:p-8">{children}</main>
|
|
||||||
</div>
|
</div>
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -213,3 +213,9 @@
|
||||||
transition-duration: 0.01ms !important;
|
transition-duration: 0.01ms !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* iOS zoomt beim Fokussieren in Felder mit < 16px Schrift. Auf Touch-Geräten daher mindestens 16px. */
|
||||||
|
@media (pointer: coarse) {
|
||||||
|
input:not([type="checkbox"], [type="radio"], [type="file"]), select, textarea {
|
||||||
|
font-size: max(16px, 1em);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Metadata } from "next";
|
import type { Metadata, Viewport } from "next";
|
||||||
import { Inter, JetBrains_Mono } from "next/font/google";
|
import { Inter, JetBrains_Mono } from "next/font/google";
|
||||||
import { ThemeProvider } from "@/components/theme-provider";
|
import { ThemeProvider } from "@/components/theme-provider";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
|
@ -15,13 +15,29 @@ const jetbrainsMono = JetBrains_Mono({
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Since3 Portal",
|
title: "Since3 Portal",
|
||||||
description: "Generated by create next app",
|
description: "Kundenportal von Since3: Status, Rechnungen, Dokumente und Zugangsdaten.",
|
||||||
|
applicationName: "Since3 Portal",
|
||||||
|
appleWebApp: {
|
||||||
|
capable: true,
|
||||||
|
title: "Since3",
|
||||||
|
statusBarStyle: "default",
|
||||||
|
},
|
||||||
|
formatDetection: {
|
||||||
|
telephone: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const viewport: Viewport = {
|
||||||
|
themeColor: [
|
||||||
|
{ media: "(prefers-color-scheme: light)", color: "#fcfcfc" },
|
||||||
|
{ media: "(prefers-color-scheme: dark)", color: "#020202" },
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({ children }: LayoutProps<"/">) {
|
export default function RootLayout({ children }: LayoutProps<"/">) {
|
||||||
return (
|
return (
|
||||||
<html
|
<html
|
||||||
lang="en"
|
lang="de"
|
||||||
className={`${inter.variable} ${jetbrainsMono.variable} h-full antialiased`}
|
className={`${inter.variable} ${jetbrainsMono.variable} h-full antialiased`}
|
||||||
suppressHydrationWarning
|
suppressHydrationWarning
|
||||||
>
|
>
|
||||||
|
|
|
||||||
36
app/manifest.ts
Normal file
36
app/manifest.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import type { MetadataRoute } from "next";
|
||||||
|
|
||||||
|
export default function manifest(): MetadataRoute.Manifest {
|
||||||
|
return {
|
||||||
|
id: "/",
|
||||||
|
name: "Since3 Portal",
|
||||||
|
short_name: "Since3",
|
||||||
|
description: "Kundenportal von Since3: Status, Rechnungen, Dokumente und Zugangsdaten.",
|
||||||
|
lang: "de",
|
||||||
|
start_url: "/",
|
||||||
|
scope: "/",
|
||||||
|
display: "standalone",
|
||||||
|
background_color: "#020202",
|
||||||
|
theme_color: "#020202",
|
||||||
|
icons: [
|
||||||
|
{
|
||||||
|
src: "/icons/icon-192.png",
|
||||||
|
sizes: "192x192",
|
||||||
|
type: "image/png",
|
||||||
|
purpose: "any",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "/icons/icon-512.png",
|
||||||
|
sizes: "512x512",
|
||||||
|
type: "image/png",
|
||||||
|
purpose: "any",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "/icons/icon-maskable-512.png",
|
||||||
|
sizes: "512x512",
|
||||||
|
type: "image/png",
|
||||||
|
purpose: "maskable",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
import { getProfile } from "@/lib/supabase/dal";
|
||||||
import LoginPageView from "@/modules/loginPage/ui/views/login-view";
|
import LoginPageView from "@/modules/loginPage/ui/views/login-view";
|
||||||
|
|
||||||
export default function Home() {
|
export default async function Home() {
|
||||||
|
// Installierte App startet auf "/": mit gültiger Session direkt ins Dashboard.
|
||||||
|
const profile = await getProfile();
|
||||||
|
if (profile) redirect(profile.role === "admin" ? "/admin/dashboard" : "/client/dashboard");
|
||||||
|
|
||||||
return <LoginPageView/>;
|
return <LoginPageView/>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
const TermsPage = () => {
|
const TermsPage = () => {
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-2xl p-8">
|
<div className="mx-auto max-w-2xl px-4 py-8 sm:p-8">
|
||||||
<h1 className="mb-4 text-2xl font-semibold">Nutzungsbedingungen</h1>
|
<h1 className="mb-4 text-2xl font-semibold">Nutzungsbedingungen</h1>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
TODO: Nutzungsbedingungen einfügen.
|
TODO: Nutzungsbedingungen einfügen.
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ function DialogContent({
|
||||||
<DialogPrimitive.Popup
|
<DialogPrimitive.Popup
|
||||||
data-slot="dialog-content"
|
data-slot="dialog-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
"fixed top-1/2 left-1/2 z-50 grid max-h-[calc(100dvh-2rem)] w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 overflow-y-auto overscroll-contain rounded-xl bg-popover p-4 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ function ResetPasswordContent({ client, onDone }: { client: Client; onDone: () =
|
||||||
<input type="hidden" name="profileId" value={client.id} />
|
<input type="hidden" name="profileId" value={client.id} />
|
||||||
<input type="hidden" name="mode" value={mode} />
|
<input type="hidden" name="mode" value={mode} />
|
||||||
<FieldGroup>
|
<FieldGroup>
|
||||||
<div role="radiogroup" aria-label="Art des neuen Passworts" className="grid grid-cols-2 gap-2">
|
<div role="radiogroup" aria-label="Art des neuen Passworts" className="grid gap-2 sm:grid-cols-2">
|
||||||
{(["generate", "manual"] as const).map((value) => (
|
{(["generate", "manual"] as const).map((value) => (
|
||||||
<label
|
<label
|
||||||
key={value}
|
key={value}
|
||||||
|
|
|
||||||
|
|
@ -51,9 +51,9 @@ export function ClientsTable({ clients }: { clients: Client[] }) {
|
||||||
}, [clients, query]);
|
}, [clients, query]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-8 w-full">
|
<div className="flex w-full flex-col gap-6 md:gap-8">
|
||||||
<div className="flex flex-row items-center justify-between gap-2 w-full p-2">
|
<div className="flex w-full flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<InputGroup className="max-w-lg rounded-md focus-visible:ring-0">
|
<InputGroup className="w-full rounded-md focus-visible:ring-0 sm:max-w-lg">
|
||||||
<InputGroupInput
|
<InputGroupInput
|
||||||
placeholder="Search..."
|
placeholder="Search..."
|
||||||
value={query}
|
value={query}
|
||||||
|
|
@ -75,8 +75,8 @@ export function ClientsTable({ clients }: { clients: Client[] }) {
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>Client</TableHead>
|
<TableHead>Client</TableHead>
|
||||||
<TableHead>Status</TableHead>
|
<TableHead className="hidden sm:table-cell">Status</TableHead>
|
||||||
<TableHead className="w-24" />
|
<TableHead className="w-20 sm:w-24" />
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
|
|
@ -86,7 +86,7 @@ export function ClientsTable({ clients }: { clients: Client[] }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow key={client.id}>
|
<TableRow key={client.id}>
|
||||||
<TableCell>
|
<TableCell className="w-full max-w-0 sm:w-auto sm:max-w-none">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Avatar size="lg" className="shrink-0">
|
<Avatar size="lg" className="shrink-0">
|
||||||
<AvatarFallback>
|
<AvatarFallback>
|
||||||
|
|
@ -99,7 +99,7 @@ export function ClientsTable({ clients }: { clients: Client[] }) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell className="hidden sm:table-cell">
|
||||||
<Badge variant={status.variant}>{status.label}</Badge>
|
<Badge variant={status.variant}>{status.label}</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
|
|
|
||||||
|
|
@ -256,7 +256,7 @@ function ActiveToggle({ template }: { template: AdminProductTemplate }) {
|
||||||
export function ProductTemplatesSection({ templates }: { templates: AdminProductTemplate[] }) {
|
export function ProductTemplatesSection({ templates }: { templates: AdminProductTemplate[] }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="flex justify-end">
|
<div className="flex sm:justify-end">
|
||||||
<TemplateDialog />
|
<TemplateDialog />
|
||||||
</div>
|
</div>
|
||||||
{templates.length === 0 ? (
|
{templates.length === 0 ? (
|
||||||
|
|
@ -267,8 +267,8 @@ export function ProductTemplatesSection({ templates }: { templates: AdminProduct
|
||||||
<ul className="flex flex-col divide-y divide-border rounded-lg border border-border">
|
<ul className="flex flex-col divide-y divide-border rounded-lg border border-border">
|
||||||
{templates.map((template) => (
|
{templates.map((template) => (
|
||||||
<li key={template.id} className={cn("flex flex-wrap items-center gap-3 px-4 py-3", !template.isActive && "opacity-60")}>
|
<li key={template.id} className={cn("flex flex-wrap items-center gap-3 px-4 py-3", !template.isActive && "opacity-60")}>
|
||||||
<div className="flex min-w-0 flex-1 flex-col">
|
<div className="flex min-w-0 flex-1 basis-full flex-col sm:basis-auto">
|
||||||
<span className="flex items-center gap-2 font-medium">
|
<span className="flex flex-wrap items-center gap-2 font-medium break-words">
|
||||||
{template.name}
|
{template.name}
|
||||||
{!template.isActive && <Badge>Inaktiv</Badge>}
|
{!template.isActive && <Badge>Inaktiv</Badge>}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -278,7 +278,7 @@ export function ProductTemplatesSection({ templates }: { templates: AdminProduct
|
||||||
<span className="w-20 text-right text-small text-muted-foreground tabular-nums">
|
<span className="w-20 text-right text-small text-muted-foreground tabular-nums">
|
||||||
{template.taxRate.toLocaleString("de-DE")} % MwSt
|
{template.taxRate.toLocaleString("de-DE")} % MwSt
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center gap-1">
|
<div className="ml-auto flex items-center gap-1">
|
||||||
<ActiveToggle template={template} />
|
<ActiveToggle template={template} />
|
||||||
<TemplateDialog template={template} />
|
<TemplateDialog template={template} />
|
||||||
<DeleteTemplateDialog template={template} />
|
<DeleteTemplateDialog template={template} />
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@ const ClientListViewPage = async () => {
|
||||||
const clients = await getClients()
|
const clients = await getClients()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-8 w-full">
|
<div className="flex w-full flex-col gap-6 md:gap-8">
|
||||||
<h1 className="text-3xl font-semibold">Clients</h1>
|
<h1 className="text-2xl font-semibold md:text-3xl">Clients</h1>
|
||||||
<ClientsTable clients={clients} />
|
<ClientsTable clients={clients} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ function StatCard({
|
||||||
<span className="text-small text-muted-foreground">{label}</span>
|
<span className="text-small text-muted-foreground">{label}</span>
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-2xl font-semibold tabular-nums",
|
"text-xl font-semibold tabular-nums break-words sm:text-2xl",
|
||||||
tone === "danger" && "text-signal-error",
|
tone === "danger" && "text-signal-error",
|
||||||
tone === "success" && "text-signal-success"
|
tone === "success" && "text-signal-success"
|
||||||
)}
|
)}
|
||||||
|
|
@ -57,7 +57,7 @@ function StatCard({
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
||||||
const className = "flex flex-col gap-1 rounded-lg border border-border p-4"
|
const className = "flex min-w-0 flex-col gap-1 rounded-lg border border-border p-4"
|
||||||
return href ? (
|
return href ? (
|
||||||
<Link href={href} className={cn(className, "transition-colors hover:bg-surface")}>{content}</Link>
|
<Link href={href} className={cn(className, "transition-colors hover:bg-surface")}>{content}</Link>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -68,7 +68,7 @@ function StatCard({
|
||||||
function Section({ title, count, children }: { title: string; count?: number; children: React.ReactNode }) {
|
function Section({ title, count, children }: { title: string; count?: number; children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<section className="flex flex-col gap-3">
|
<section className="flex flex-col gap-3">
|
||||||
<h2 className="flex items-center gap-2 text-xl font-semibold">
|
<h2 className="flex items-center gap-2 text-lg font-semibold md:text-xl">
|
||||||
{title}
|
{title}
|
||||||
{count !== undefined && count > 0 && <Badge>{count}</Badge>}
|
{count !== undefined && count > 0 && <Badge>{count}</Badge>}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
@ -101,13 +101,13 @@ const DashboardView = async ({ name }: { name: string }) => {
|
||||||
const today = berlinToday()
|
const today = berlinToday()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-10 w-full">
|
<div className="flex w-full flex-col gap-8 md:gap-10">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<h1 className="text-3xl font-semibold">Hallo {name.split(" ")[0]}</h1>
|
<h1 className="text-2xl font-semibold md:text-3xl">Hallo {name.split(" ")[0]}</h1>
|
||||||
<p className="text-accent-400">Alles Wichtige über alle Clients auf einen Blick.</p>
|
<p className="text-accent-400">Alles Wichtige über alle Clients auf einen Blick.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
|
<div className="grid gap-3 min-[480px]:grid-cols-2 xl:grid-cols-4">
|
||||||
<StatCard label="Offene Rechnungen" value={formatCurrency(data.openTotal)} detail={`${data.openCount} ${data.openCount === 1 ? "Rechnung" : "Rechnungen"}`} />
|
<StatCard label="Offene Rechnungen" value={formatCurrency(data.openTotal)} detail={`${data.openCount} ${data.openCount === 1 ? "Rechnung" : "Rechnungen"}`} />
|
||||||
<StatCard
|
<StatCard
|
||||||
label="Davon überfällig"
|
label="Davon überfällig"
|
||||||
|
|
@ -148,7 +148,7 @@ const DashboardView = async ({ name }: { name: string }) => {
|
||||||
)}
|
)}
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<div className="grid gap-10 lg:grid-cols-2">
|
<div className="grid gap-8 md:gap-10 xl:grid-cols-2">
|
||||||
<Section title="Neueste Uploads von Clients">
|
<Section title="Neueste Uploads von Clients">
|
||||||
{data.uploads.length === 0 ? (
|
{data.uploads.length === 0 ? (
|
||||||
<EmptyHint icon={UploadIcon}>Noch keine Uploads von Clients.</EmptyHint>
|
<EmptyHint icon={UploadIcon}>Noch keine Uploads von Clients.</EmptyHint>
|
||||||
|
|
@ -179,15 +179,17 @@ const DashboardView = async ({ name }: { name: string }) => {
|
||||||
<ul className="flex flex-col divide-y divide-border overflow-hidden rounded-lg border border-border">
|
<ul className="flex flex-col divide-y divide-border overflow-hidden rounded-lg border border-border">
|
||||||
{data.tasks.map((task) => (
|
{data.tasks.map((task) => (
|
||||||
<LinkRow key={task.id} href={task.href}>
|
<LinkRow key={task.id} href={task.href}>
|
||||||
<Badge variant={task.status === "in_progress" ? "warning" : "neutral"} className="w-20">
|
<Badge variant={task.status === "in_progress" ? "warning" : "neutral"} className="w-20 shrink-0">
|
||||||
{TASK_STATUS_LABELS[task.status]}
|
{TASK_STATUS_LABELS[task.status]}
|
||||||
</Badge>
|
</Badge>
|
||||||
<span className="flex min-w-0 flex-1 flex-col">
|
<span className="flex min-w-0 flex-1 flex-col sm:flex-row sm:items-center sm:gap-3">
|
||||||
<span className="truncate font-medium">{task.name}</span>
|
<span className="flex min-w-0 flex-1 flex-col">
|
||||||
<span className="truncate text-small text-muted-foreground">{task.clientName}</span>
|
<span className="truncate font-medium">{task.name}</span>
|
||||||
</span>
|
<span className="truncate text-small text-muted-foreground">{task.clientName}</span>
|
||||||
<span className="shrink-0 text-small text-muted-foreground">
|
</span>
|
||||||
{formatTaskTiming(task.status, task.since, today)}
|
<span className="text-small text-muted-foreground sm:shrink-0">
|
||||||
|
{formatTaskTiming(task.status, task.since, today)}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</LinkRow>
|
</LinkRow>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,8 @@ const SettingsView = async ({ name, email }: { name: string; email: string }) =>
|
||||||
const templates = await getAllProductTemplates()
|
const templates = await getAllProductTemplates()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-8 w-full max-w-4xl">
|
<div className="flex w-full max-w-4xl flex-col gap-6 md:gap-8">
|
||||||
<h1 className="text-3xl font-semibold">Einstellungen</h1>
|
<h1 className="text-2xl font-semibold md:text-3xl">Einstellungen</h1>
|
||||||
|
|
||||||
<Section title="Profil" description="So wirst du im Portal angezeigt, z. B. bei „zuletzt bearbeitet von“.">
|
<Section title="Profil" description="So wirst du im Portal angezeigt, z. B. bei „zuletzt bearbeitet von“.">
|
||||||
<ProfileForm name={name} email={email} />
|
<ProfileForm name={name} email={email} />
|
||||||
|
|
|
||||||
|
|
@ -56,8 +56,8 @@ function FormSection({
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg border border-border bg-surface/50 p-5">
|
<div className="rounded-lg border border-border bg-surface/50 p-4 sm:p-5">
|
||||||
<div className="mb-5 flex items-center gap-2">
|
<div className="mb-4 flex items-center gap-2 sm:mb-5">
|
||||||
<Icon className="size-4.5 text-muted-foreground" />
|
<Icon className="size-4.5 text-muted-foreground" />
|
||||||
<h2 className="text-base font-semibold">{title}</h2>
|
<h2 className="text-base font-semibold">{title}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -217,8 +217,8 @@ const StammdatenForm = ({
|
||||||
/>
|
/>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
|
|
||||||
<div className="rounded-lg border border-border bg-surface/50 p-5">
|
<div className="rounded-lg border border-border bg-surface/50 p-4 sm:p-5">
|
||||||
<label className="mb-5 flex items-center gap-2 has-disabled:opacity-50">
|
<label className="mb-4 flex items-center gap-2 has-disabled:opacity-50 sm:mb-5">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={hasDifferentBillingAddress}
|
checked={hasDifferentBillingAddress}
|
||||||
|
|
@ -294,7 +294,7 @@ const StammdatenForm = ({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isEditing && (
|
{isEditing && (
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
|
||||||
<Button type="button" variant="outline" onClick={onDone} disabled={pending}>
|
<Button type="button" variant="outline" onClick={onDone} disabled={pending}>
|
||||||
Abbrechen
|
Abbrechen
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -13,21 +13,21 @@ const LoginPageView = () => {
|
||||||
const [state, formAction, pending] = useActionState(login, undefined);
|
const [state, formAction, pending] = useActionState(login, undefined);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-background h-screen p-6">
|
<div className="flex min-h-svh flex-col bg-background p-4 sm:p-6">
|
||||||
<Image
|
<Image
|
||||||
src="/pictures/since3-logo.svg" alt=""
|
src="/pictures/since3-logo.svg" alt=""
|
||||||
width={1617}
|
width={1617}
|
||||||
height={332}
|
height={332}
|
||||||
loading="eager"
|
loading="eager"
|
||||||
className="h-5 w-auto md:h-6 dark:hidden"/>
|
className="h-5 w-auto self-start md:h-6 dark:hidden"/>
|
||||||
<Image
|
<Image
|
||||||
src="/pictures/since3-logo-weiss.svg" alt=""
|
src="/pictures/since3-logo-weiss.svg" alt=""
|
||||||
width={1617}
|
width={1617}
|
||||||
height={332}
|
height={332}
|
||||||
loading="eager"
|
loading="eager"
|
||||||
className="hidden h-5 w-auto md:h-6 dark:block"/>
|
className="hidden h-5 w-auto self-start md:h-6 dark:block"/>
|
||||||
<div className="flex flex-col h-full items-center justify-center">
|
<div className="flex flex-1 flex-col items-center justify-center py-10">
|
||||||
<h1 className="mb-6 text-3xl font-semibold">Login</h1>
|
<h1 className="mb-6 text-2xl font-semibold sm:text-3xl">Login</h1>
|
||||||
<form action={formAction} className="w-full max-w-sm ">
|
<form action={formAction} className="w-full max-w-sm ">
|
||||||
<FieldGroup>
|
<FieldGroup>
|
||||||
<Field>
|
<Field>
|
||||||
|
|
@ -72,8 +72,8 @@ const LoginPageView = () => {
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute bottom-2">
|
<div>
|
||||||
<p className="text-muted-foreground text-sm">Bei Fragen: <a href="mailto:info@since3.de" className="underline">info@since3.de</a></p>
|
<p className="text-center text-muted-foreground text-sm sm:text-left">Bei Fragen: <a href="mailto:info@since3.de" className="underline">info@since3.de</a></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -183,7 +183,7 @@ function BillingDialogContent({
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Vorschau</DialogTitle>
|
<DialogTitle>Vorschau</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<iframe src={previewUrl} title="Rechnungsvorschau" className="h-[70vh] w-full rounded-md border border-border" />
|
<iframe src={previewUrl} title="Rechnungsvorschau" className="h-[60dvh] w-full rounded-md border border-border sm:h-[70dvh]" />
|
||||||
{error && <p className="text-sm text-destructive">{error}</p>}
|
{error && <p className="text-sm text-destructive">{error}</p>}
|
||||||
<DialogFooter className="gap-2">
|
<DialogFooter className="gap-2">
|
||||||
<Button type="button" variant="outline" onClick={() => setPreviewUrl(null)} disabled={createPending}>
|
<Button type="button" variant="outline" onClick={() => setPreviewUrl(null)} disabled={createPending}>
|
||||||
|
|
@ -215,7 +215,7 @@ function BillingDialogContent({
|
||||||
<FieldLabel htmlFor="billingStreet">Straße <span className="text-destructive">*</span></FieldLabel>
|
<FieldLabel htmlFor="billingStreet">Straße <span className="text-destructive">*</span></FieldLabel>
|
||||||
<Input id="billingStreet" value={header.billingStreet} onChange={(e) => updateHeader("billingStreet", e.target.value)} />
|
<Input id="billingStreet" value={header.billingStreet} onChange={(e) => updateHeader("billingStreet", e.target.value)} />
|
||||||
</Field>
|
</Field>
|
||||||
<div className="grid grid-cols-[1fr_2fr_4rem] gap-2">
|
<div className="grid grid-cols-[minmax(0,1fr)_minmax(0,2fr)_4rem] gap-2">
|
||||||
<Field>
|
<Field>
|
||||||
<FieldLabel htmlFor="billingPostalCode">PLZ <span className="text-destructive">*</span></FieldLabel>
|
<FieldLabel htmlFor="billingPostalCode">PLZ <span className="text-destructive">*</span></FieldLabel>
|
||||||
<Input id="billingPostalCode" value={header.billingPostalCode} onChange={(e) => updateHeader("billingPostalCode", e.target.value)} />
|
<Input id="billingPostalCode" value={header.billingPostalCode} onChange={(e) => updateHeader("billingPostalCode", e.target.value)} />
|
||||||
|
|
@ -256,8 +256,8 @@ function BillingDialogContent({
|
||||||
const net = Number.isFinite(item.quantity) && Number.isFinite(item.unitPrice) ? lineNet(item) : null;
|
const net = Number.isFinite(item.quantity) && Number.isFinite(item.unitPrice) ? lineNet(item) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={row.key} className="grid gap-2 rounded-lg border border-border p-3 md:grid-cols-[10rem_1fr_5rem_7rem_5rem_auto] md:items-end">
|
<div key={row.key} className="grid grid-cols-3 items-end gap-2 rounded-lg border border-border p-3 md:grid-cols-[10rem_1fr_5rem_7rem_5rem_auto]">
|
||||||
<Field>
|
<Field className="col-span-3 md:col-span-1">
|
||||||
<FieldLabel htmlFor={`template-${row.key}`}>Vorlage</FieldLabel>
|
<FieldLabel htmlFor={`template-${row.key}`}>Vorlage</FieldLabel>
|
||||||
<select
|
<select
|
||||||
id={`template-${row.key}`}
|
id={`template-${row.key}`}
|
||||||
|
|
@ -274,7 +274,7 @@ function BillingDialogContent({
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</Field>
|
</Field>
|
||||||
<Field>
|
<Field className="col-span-3 md:col-span-1">
|
||||||
<FieldLabel htmlFor={`description-${row.key}`}>Bezeichnung</FieldLabel>
|
<FieldLabel htmlFor={`description-${row.key}`}>Bezeichnung</FieldLabel>
|
||||||
<Input id={`description-${row.key}`} value={row.description} onChange={(e) => updateRow(row.key, { description: e.target.value })} />
|
<Input id={`description-${row.key}`} value={row.description} onChange={(e) => updateRow(row.key, { description: e.target.value })} />
|
||||||
</Field>
|
</Field>
|
||||||
|
|
@ -290,7 +290,7 @@ function BillingDialogContent({
|
||||||
<FieldLabel htmlFor={`tax-${row.key}`}>MwSt %</FieldLabel>
|
<FieldLabel htmlFor={`tax-${row.key}`}>MwSt %</FieldLabel>
|
||||||
<Input id={`tax-${row.key}`} inputMode="decimal" value={row.taxRate} onChange={(e) => updateRow(row.key, { taxRate: e.target.value })} />
|
<Input id={`tax-${row.key}`} inputMode="decimal" value={row.taxRate} onChange={(e) => updateRow(row.key, { taxRate: e.target.value })} />
|
||||||
</Field>
|
</Field>
|
||||||
<div className="flex items-center justify-between gap-2 md:justify-end md:h-10">
|
<div className="col-span-3 flex items-center justify-between gap-2 md:col-span-1 md:h-10 md:justify-end">
|
||||||
<span className="min-w-20 text-right tabular-nums text-muted-foreground">
|
<span className="min-w-20 text-right tabular-nums text-muted-foreground">
|
||||||
{net === null ? "–" : formatCurrency(net)}
|
{net === null ? "–" : formatCurrency(net)}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -363,7 +363,7 @@ export function BillingDialog({
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-5xl">
|
<DialogContent className="sm:max-w-5xl">
|
||||||
{prefill ? (
|
{prefill ? (
|
||||||
<BillingDialogContent
|
<BillingDialogContent
|
||||||
key={instanceKey}
|
key={instanceKey}
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ function RevealButton({ credentialId }: { credentialId: string }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="min-w-32 font-mono text-small">
|
<span className="font-mono text-small break-all sm:min-w-32">
|
||||||
{password ?? "••••••••"}
|
{password ?? "••••••••"}
|
||||||
</span>
|
</span>
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -86,9 +86,9 @@ export function CredentialsTable({
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow className="hover:bg-transparent">
|
<TableRow className="hover:bg-transparent">
|
||||||
<TableHead>Dienst</TableHead>
|
<TableHead>Dienst</TableHead>
|
||||||
<TableHead>Benutzername</TableHead>
|
<TableHead className="hidden md:table-cell">Benutzername</TableHead>
|
||||||
<TableHead>Passwort</TableHead>
|
<TableHead>Passwort</TableHead>
|
||||||
<TableHead className="w-24" />
|
<TableHead className="w-20 sm:w-24" />
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
|
|
@ -102,10 +102,13 @@ export function CredentialsTable({
|
||||||
<div className="flex size-9 shrink-0 items-center justify-center rounded-md border border-border bg-background">
|
<div className="flex size-9 shrink-0 items-center justify-center rounded-md border border-border bg-background">
|
||||||
<ServiceLogo serviceId={credential.service} size={18} className="size-4.5 object-contain" />
|
<ServiceLogo serviceId={credential.service} size={18} className="size-4.5 object-contain" />
|
||||||
</div>
|
</div>
|
||||||
<span className="font-medium">{service.label}</span>
|
<div className="flex min-w-0 flex-col">
|
||||||
|
<span className="font-medium">{service.label}</span>
|
||||||
|
<span className="text-small text-muted-foreground break-all md:hidden">{credential.username}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-muted-foreground">{credential.username}</TableCell>
|
<TableCell className="hidden text-muted-foreground break-all md:table-cell">{credential.username}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<RevealButton credentialId={credential.id} />
|
<RevealButton credentialId={credential.id} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
|
||||||
|
|
@ -108,16 +108,16 @@ function PreviewContent({ doc, url }: { doc: DocumentItem; url: string }) {
|
||||||
switch (kind) {
|
switch (kind) {
|
||||||
case "image":
|
case "image":
|
||||||
// eslint-disable-next-line @next/next/no-img-element -- signierte, kurzlebige Storage-URL
|
// eslint-disable-next-line @next/next/no-img-element -- signierte, kurzlebige Storage-URL
|
||||||
return <img src={url} alt={doc.name} className="mx-auto max-h-[70vh] w-auto rounded-md object-contain" />;
|
return <img src={url} alt={doc.name} className="mx-auto max-h-[70dvh] w-auto max-w-full rounded-md object-contain" />;
|
||||||
case "pdf":
|
case "pdf":
|
||||||
return <iframe src={url} title={doc.name} className="h-[70vh] w-full rounded-md border border-border" />;
|
return <iframe src={url} title={doc.name} className="h-[60dvh] w-full rounded-md border border-border sm:h-[70dvh]" />;
|
||||||
case "video":
|
case "video":
|
||||||
return <video src={url} controls className="max-h-[70vh] w-full rounded-md" />;
|
return <video src={url} controls className="max-h-[70dvh] w-full rounded-md" />;
|
||||||
case "audio":
|
case "audio":
|
||||||
return <audio src={url} controls className="w-full" />;
|
return <audio src={url} controls className="w-full" />;
|
||||||
case "text":
|
case "text":
|
||||||
return (
|
return (
|
||||||
<pre className="max-h-[70vh] overflow-auto rounded-md border border-border bg-surface p-4 text-small whitespace-pre-wrap break-words">
|
<pre className="max-h-[70dvh] overflow-auto rounded-md border border-border bg-surface p-4 text-small whitespace-pre-wrap break-words">
|
||||||
{text ?? "Wird geladen..."}
|
{text ?? "Wird geladen..."}
|
||||||
</pre>
|
</pre>
|
||||||
);
|
);
|
||||||
|
|
@ -185,7 +185,7 @@ function PreviewDialog({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={doc !== null} onOpenChange={handleOpenChange}>
|
<Dialog open={doc !== null} onOpenChange={handleOpenChange}>
|
||||||
<DialogContent className="max-h-[95vh] overflow-y-auto sm:max-w-4xl">
|
<DialogContent className="max-h-[95dvh] overflow-y-auto sm:max-w-4xl">
|
||||||
{doc && (
|
{doc && (
|
||||||
<>
|
<>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
|
|
@ -398,7 +398,7 @@ export function DocumentsBrowser({
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<Button variant="secondary" onClick={() => handleDownload(visibleSelection)} disabled={visibleSelection.length === 0 || busy}>
|
<Button variant="secondary" onClick={() => handleDownload(visibleSelection)} disabled={visibleSelection.length === 0 || busy}>
|
||||||
{downloading ? <LoaderCircleIcon className="size-4 animate-spin" /> : <DownloadIcon className="size-4" />}
|
{downloading ? <LoaderCircleIcon className="size-4 animate-spin" /> : <DownloadIcon className="size-4" />}
|
||||||
{downloading ? "Wird vorbereitet..." : visibleSelection.length > 1 ? `Download (${visibleSelection.length})` : "Download"}
|
{downloading ? "Wird vorbereitet..." : visibleSelection.length > 1 ? `Download (${visibleSelection.length})` : "Download"}
|
||||||
|
|
@ -427,7 +427,7 @@ export function DocumentsBrowser({
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => inputRef.current?.click()}
|
onClick={() => inputRef.current?.click()}
|
||||||
className="flex flex-col items-center gap-2 rounded-lg border border-dashed border-border bg-surface/50 p-12 text-center text-muted-foreground transition-colors hover:bg-surface"
|
className="flex flex-col items-center gap-2 rounded-lg border border-dashed border-border bg-surface/50 p-8 text-center text-muted-foreground transition-colors hover:bg-surface sm:p-12"
|
||||||
>
|
>
|
||||||
<UploadIcon className="size-8" />
|
<UploadIcon className="size-8" />
|
||||||
<span>Noch keine Dateien. Hierher ziehen oder klicken zum Hochladen.</span>
|
<span>Noch keine Dateien. Hierher ziehen oder klicken zum Hochladen.</span>
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ function NoteItem({ profileId, note }: { profileId: string; note: InternalNote }
|
||||||
<NoteForm profileId={profileId} note={note} onDone={() => setEditing(false)} onCancel={() => setEditing(false)} />
|
<NoteForm profileId={profileId} note={note} onDone={() => setEditing(false)} onCancel={() => setEditing(false)} />
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-start justify-between gap-4">
|
<div className="flex items-start justify-between gap-4">
|
||||||
<p className="whitespace-pre-wrap break-words">{note.body}</p>
|
<p className="min-w-0 whitespace-pre-wrap break-words">{note.body}</p>
|
||||||
<div className="flex shrink-0 gap-1">
|
<div className="flex shrink-0 gap-1">
|
||||||
<Button variant="ghost" size="icon-sm" aria-label="Notiz bearbeiten" onClick={() => setEditing(true)}>
|
<Button variant="ghost" size="icon-sm" aria-label="Notiz bearbeiten" onClick={() => setEditing(true)}>
|
||||||
<PencilIcon className="size-4" />
|
<PencilIcon className="size-4" />
|
||||||
|
|
@ -87,7 +87,7 @@ export function InternalNotes({ profileId, notes }: { profileId: string; notes:
|
||||||
return (
|
return (
|
||||||
<section className="flex flex-col gap-4 rounded-lg border border-dashed border-border bg-surface/50 p-4 md:p-6">
|
<section className="flex flex-col gap-4 rounded-lg border border-dashed border-border bg-surface/50 p-4 md:p-6">
|
||||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||||
<h2 className="text-xl font-semibold">Interne Notiz</h2>
|
<h2 className="text-lg font-semibold md:text-xl">Interne Notiz</h2>
|
||||||
<span className="inline-flex items-center gap-1.5 rounded-full bg-ink-100 px-2.5 py-0.5 text-label text-ink-700 dark:bg-ink-800 dark:text-ink-300">
|
<span className="inline-flex items-center gap-1.5 rounded-full bg-ink-100 px-2.5 py-0.5 text-label text-ink-700 dark:bg-ink-800 dark:text-ink-300">
|
||||||
<EyeOffIcon className="size-3.5" /> Kunde sieht das nicht
|
<EyeOffIcon className="size-3.5" /> Kunde sieht das nicht
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
|
|
@ -204,7 +204,7 @@ export function InvoicesTable({
|
||||||
</InputGroupAddon>
|
</InputGroupAddon>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<Button variant="secondary" onClick={handleDownload} disabled={selectedVisible.length === 0 || downloading}>
|
<Button variant="secondary" onClick={handleDownload} disabled={selectedVisible.length === 0 || downloading}>
|
||||||
<ArrowDownToLineIcon />
|
<ArrowDownToLineIcon />
|
||||||
{downloading
|
{downloading
|
||||||
|
|
@ -242,14 +242,14 @@ export function InvoicesTable({
|
||||||
<TableHead>Rechnung</TableHead>
|
<TableHead>Rechnung</TableHead>
|
||||||
<TableHead>Status</TableHead>
|
<TableHead>Status</TableHead>
|
||||||
<TableHead className="text-right">Betrag</TableHead>
|
<TableHead className="text-right">Betrag</TableHead>
|
||||||
<TableHead>Fällig am</TableHead>
|
<TableHead className="hidden sm:table-cell">Fällig am</TableHead>
|
||||||
{isAdmin && <TableHead className="w-12" />}
|
{isAdmin && <TableHead className="w-12" />}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{visible.length === 0 ? (
|
{visible.length === 0 ? (
|
||||||
<TableRow className="hover:bg-transparent">
|
<TableRow className="hover:bg-transparent">
|
||||||
<TableCell colSpan={isAdmin ? 6 : 5} className="py-8 text-center text-muted-foreground">
|
<TableCell colSpan={isAdmin ? 6 : 5} className="py-8 text-center text-muted-foreground wrap-anywhere">
|
||||||
Keine Treffer für „{query}“.
|
Keine Treffer für „{query}“.
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|
@ -275,18 +275,21 @@ export function InvoicesTable({
|
||||||
href={invoiceUrl(invoice.id)}
|
href={invoiceUrl(invoice.id)}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="hover:underline underline-offset-4"
|
className="whitespace-nowrap hover:underline underline-offset-4"
|
||||||
>
|
>
|
||||||
{invoice.invoiceNumber}
|
{invoice.invoiceNumber}
|
||||||
</a>
|
</a>
|
||||||
|
<span className="block text-small font-normal whitespace-nowrap text-muted-foreground tabular-nums sm:hidden">
|
||||||
|
fällig {formatDate(invoice.dueDate)}
|
||||||
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge variant={STATUS_VARIANTS[invoice.displayStatus]}>
|
<Badge variant={STATUS_VARIANTS[invoice.displayStatus]}>
|
||||||
{STATUS_LABELS[invoice.displayStatus]}
|
{STATUS_LABELS[invoice.displayStatus]}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right tabular-nums">{formatCurrency(invoice.grossTotal)}</TableCell>
|
<TableCell className="text-right tabular-nums whitespace-nowrap">{formatCurrency(invoice.grossTotal)}</TableCell>
|
||||||
<TableCell className="tabular-nums text-muted-foreground">{formatDate(invoice.dueDate)}</TableCell>
|
<TableCell className="hidden tabular-nums text-muted-foreground sm:table-cell">{formatDate(invoice.dueDate)}</TableCell>
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
|
|
|
||||||
24
modules/shared/ui/components/mobile-header.tsx
Normal file
24
modules/shared/ui/components/mobile-header.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import Image from "next/image";
|
||||||
|
import { SidebarTrigger } from "@/components/ui/sidebar";
|
||||||
|
|
||||||
|
export function MobileHeader() {
|
||||||
|
return (
|
||||||
|
<header className="sticky top-0 z-30 flex h-14 items-center gap-3 border-b border-border bg-background/95 px-3 backdrop-blur supports-backdrop-filter:bg-background/80 md:hidden">
|
||||||
|
<SidebarTrigger className="size-9" />
|
||||||
|
<Image
|
||||||
|
src="/pictures/since3-logo.svg"
|
||||||
|
alt="Since3"
|
||||||
|
width={1617}
|
||||||
|
height={332}
|
||||||
|
className="h-5 w-auto dark:hidden"
|
||||||
|
/>
|
||||||
|
<Image
|
||||||
|
src="/pictures/since3-logo-weiss.svg"
|
||||||
|
alt="Since3"
|
||||||
|
width={1617}
|
||||||
|
height={332}
|
||||||
|
className="hidden h-5 w-auto dark:block"
|
||||||
|
/>
|
||||||
|
</header>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -29,8 +29,8 @@ export function ProductCheckLine({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-1 text-small">
|
<div className="flex items-start gap-1 text-small sm:items-center">
|
||||||
<span aria-live="polite" className={cn(lastCheck && !lastCheck.ok ? "text-signal-error" : "text-muted-foreground")}>
|
<span aria-live="polite" className={cn("min-w-0 wrap-anywhere", lastCheck && !lastCheck.ok ? "text-signal-error" : "text-muted-foreground")}>
|
||||||
{pending
|
{pending
|
||||||
? "Wird geprüft..."
|
? "Wird geprüft..."
|
||||||
: error ??
|
: error ??
|
||||||
|
|
@ -44,7 +44,7 @@ export function ProductCheckLine({
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
className="size-6"
|
className="size-6 shrink-0"
|
||||||
onClick={handleCheck}
|
onClick={handleCheck}
|
||||||
disabled={pending}
|
disabled={pending}
|
||||||
aria-label="Erreichbarkeit jetzt prüfen"
|
aria-label="Erreichbarkeit jetzt prüfen"
|
||||||
|
|
|
||||||
|
|
@ -43,9 +43,9 @@ export async function BillingPanel({
|
||||||
const invoices = clientId ? await getInvoices(clientId) : []
|
const invoices = clientId ? await getInvoices(clientId) : []
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-8 w-full">
|
<div className="flex w-full flex-col gap-6 md:gap-8">
|
||||||
<div className="w-full flex flex-row justify-between items-center">
|
<div className="flex w-full flex-wrap items-center justify-between gap-3">
|
||||||
<h1 className="text-3xl font-semibold">Rechnungen</h1>
|
<h1 className="text-2xl font-semibold md:text-3xl">Rechnungen</h1>
|
||||||
</div>
|
</div>
|
||||||
<InvoicesTable
|
<InvoicesTable
|
||||||
profileId={profileId}
|
profileId={profileId}
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,9 @@ export async function CredentialsPanel({
|
||||||
const credentials = clientId ? await getCredentials(clientId) : []
|
const credentials = clientId ? await getCredentials(clientId) : []
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-8 w-full">
|
<div className="flex w-full flex-col gap-6 md:gap-8">
|
||||||
<div className="w-full flex flex-row justify-between items-center">
|
<div className="flex w-full flex-wrap items-center justify-between gap-3">
|
||||||
<h1 className="text-3xl font-semibold">Passwörter</h1>
|
<h1 className="text-2xl font-semibold md:text-3xl">Passwörter</h1>
|
||||||
<CredentialDialog profileId={profileId} />
|
<CredentialDialog profileId={profileId} />
|
||||||
</div>
|
</div>
|
||||||
<CredentialsTable profileId={profileId} credentials={credentials} isAdmin={isAdmin} />
|
<CredentialsTable profileId={profileId} credentials={credentials} isAdmin={isAdmin} />
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,8 @@ export async function DocumentsPanel({
|
||||||
|
|
||||||
if (!clientId) {
|
if (!clientId) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-8 w-full">
|
<div className="flex w-full flex-col gap-6 md:gap-8">
|
||||||
<h1 className="text-3xl font-semibold">Dokumente</h1>
|
<h1 className="text-2xl font-semibold md:text-3xl">Dokumente</h1>
|
||||||
<EmptyState>
|
<EmptyState>
|
||||||
{isAdmin
|
{isAdmin
|
||||||
? "Für diesen Client sind noch keine Stammdaten hinterlegt. Bitte zuerst die Stammdaten ausfüllen."
|
? "Für diesen Client sind noch keine Stammdaten hinterlegt. Bitte zuerst die Stammdaten ausfüllen."
|
||||||
|
|
@ -46,9 +46,9 @@ export async function DocumentsPanel({
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6 w-full">
|
<div className="flex flex-col gap-6 w-full">
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||||
<nav aria-label="Pfad" className="flex min-w-0 items-center gap-1.5 text-3xl font-semibold">
|
<nav aria-label="Pfad" className="flex min-w-0 items-center gap-1.5 text-2xl font-semibold md:text-3xl">
|
||||||
<Link href={basePath} className="text-muted-foreground hover:text-foreground">Dokumente</Link>
|
<Link href={basePath} className="text-muted-foreground hover:text-foreground">Dokumente</Link>
|
||||||
<ChevronRightIcon className="size-6 shrink-0 text-muted-foreground" aria-hidden />
|
<ChevronRightIcon className="size-5 shrink-0 text-muted-foreground md:size-6" aria-hidden />
|
||||||
<span className="truncate">{folder.name}</span>
|
<span className="truncate">{folder.name}</span>
|
||||||
</nav>
|
</nav>
|
||||||
{isAdmin && documents.length === 0 && (
|
{isAdmin && documents.length === 0 && (
|
||||||
|
|
@ -65,7 +65,7 @@ export async function DocumentsPanel({
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6 w-full">
|
<div className="flex flex-col gap-6 w-full">
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||||
<h1 className="text-3xl font-semibold">Dokumente</h1>
|
<h1 className="text-2xl font-semibold md:text-3xl">Dokumente</h1>
|
||||||
{isAdmin && <CreateFolderDialog profileId={profileId} />}
|
{isAdmin && <CreateFolderDialog profileId={profileId} />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -79,9 +79,9 @@ export async function DocumentsPanel({
|
||||||
<li key={folder.id}>
|
<li key={folder.id}>
|
||||||
<Link
|
<Link
|
||||||
href={`${basePath}?folder=${folder.id}`}
|
href={`${basePath}?folder=${folder.id}`}
|
||||||
className="flex flex-col items-center gap-2 rounded-lg border border-border bg-background p-5 text-center transition-colors hover:border-ink-300 hover:bg-surface focus-visible:ring-2 focus-visible:ring-ring outline-none"
|
className="flex flex-col items-center gap-2 rounded-lg border border-border bg-background p-4 text-center sm:p-5 transition-colors hover:border-ink-300 hover:bg-surface focus-visible:ring-2 focus-visible:ring-ring outline-none"
|
||||||
>
|
>
|
||||||
<FolderIcon className="size-12 fill-accent-400/20 stroke-[1.25] text-accent-500" aria-hidden />
|
<FolderIcon className="size-10 sm:size-12 fill-accent-400/20 stroke-[1.25] text-accent-500" aria-hidden />
|
||||||
<span className="w-full truncate font-medium">{folder.name}</span>
|
<span className="w-full truncate font-medium">{folder.name}</span>
|
||||||
<span className="text-label text-muted-foreground">
|
<span className="text-label text-muted-foreground">
|
||||||
{folder.documentCount} {folder.documentCount === 1 ? "Datei" : "Dateien"}
|
{folder.documentCount} {folder.documentCount === 1 ? "Datei" : "Dateien"}
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ function SectionHeader({ title, meta, action }: { title: React.ReactNode; meta?:
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap items-end justify-between gap-2">
|
<div className="flex flex-wrap items-end justify-between gap-2">
|
||||||
<div className="flex flex-col gap-0.5">
|
<div className="flex flex-col gap-0.5">
|
||||||
<h2 className="text-xl font-semibold">{title}</h2>
|
<h2 className="text-lg font-semibold md:text-xl">{title}</h2>
|
||||||
{meta && <p className="text-small text-muted-foreground">{meta}</p>}
|
{meta && <p className="text-small text-muted-foreground">{meta}</p>}
|
||||||
</div>
|
</div>
|
||||||
{action}
|
{action}
|
||||||
|
|
@ -115,9 +115,9 @@ const InfoPanel = async ({ profileId, isAdmin }: { profileId: string; isAdmin: b
|
||||||
const today = berlinToday()
|
const today = berlinToday()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-10 w-full">
|
<div className="flex w-full flex-col gap-8 md:gap-10">
|
||||||
<div className="w-full flex flex-col justify-between gap-2">
|
<div className="w-full flex flex-col justify-between gap-2">
|
||||||
<h1 className="text-3xl font-semibold">Info</h1>
|
<h1 className="text-2xl font-semibold md:text-3xl">Info</h1>
|
||||||
<p className="text-accent-400">Status eurer Systeme und laufende Arbeiten auf einen Blick.</p>
|
<p className="text-accent-400">Status eurer Systeme und laufende Arbeiten auf einen Blick.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -147,7 +147,7 @@ const InfoPanel = async ({ profileId, isAdmin }: { profileId: string; isAdmin: b
|
||||||
<EmptyState>Noch keine Systeme hinterlegt.</EmptyState>
|
<EmptyState>Noch keine Systeme hinterlegt.</EmptyState>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div role="status" className={cn("flex items-center gap-3 rounded-lg border px-4 py-3 font-medium", banner.className)}>
|
<div role="status" className={cn("flex items-start gap-3 rounded-lg border px-4 py-3 font-medium sm:items-center", banner.className)}>
|
||||||
<banner.icon className="size-5 shrink-0" />
|
<banner.icon className="size-5 shrink-0" />
|
||||||
{banner.text}
|
{banner.text}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -158,8 +158,8 @@ const InfoPanel = async ({ profileId, isAdmin }: { profileId: string; isAdmin: b
|
||||||
<TableRow className="hover:bg-transparent">
|
<TableRow className="hover:bg-transparent">
|
||||||
<TableHead>Produkt</TableHead>
|
<TableHead>Produkt</TableHead>
|
||||||
<TableHead>Status</TableHead>
|
<TableHead>Status</TableHead>
|
||||||
<TableHead className="text-right">Uptime</TableHead>
|
<TableHead className="hidden text-right sm:table-cell">Uptime</TableHead>
|
||||||
{isAdmin && <TableHead className="w-24" />}
|
{isAdmin && <TableHead className="w-20 sm:w-24" />}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
|
|
@ -173,11 +173,14 @@ const InfoPanel = async ({ profileId, isAdmin }: { profileId: string; isAdmin: b
|
||||||
aria-label={PRODUCT_STATUS_LABELS[product.status]}
|
aria-label={PRODUCT_STATUS_LABELS[product.status]}
|
||||||
title={PRODUCT_STATUS_LABELS[product.status]}
|
title={PRODUCT_STATUS_LABELS[product.status]}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col">
|
<div className="flex min-w-0 flex-col">
|
||||||
<span className="font-medium">{product.name}</span>
|
<span className="font-medium">{product.name}</span>
|
||||||
<span className="text-small text-muted-foreground">
|
<span className="text-small text-muted-foreground wrap-anywhere">
|
||||||
{product.url ? <ProductLink value={product.url} /> : CATEGORY_LABELS[product.category]}
|
{product.url ? <ProductLink value={product.url} /> : CATEGORY_LABELS[product.category]}
|
||||||
</span>
|
</span>
|
||||||
|
<span className="text-small tabular-nums sm:hidden">
|
||||||
|
<UptimeValue value={displayedUptime(product)} />
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
@ -196,7 +199,7 @@ const InfoPanel = async ({ profileId, isAdmin }: { profileId: string; isAdmin: b
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell
|
<TableCell
|
||||||
className="text-right tabular-nums whitespace-nowrap"
|
className="hidden text-right tabular-nums whitespace-nowrap sm:table-cell"
|
||||||
title={product.checks30d > 0 ? `Gemessen aus ${product.checks30d} Prüfungen` : undefined}
|
title={product.checks30d > 0 ? `Gemessen aus ${product.checks30d} Prüfungen` : undefined}
|
||||||
>
|
>
|
||||||
<UptimeValue value={displayedUptime(product)} />
|
<UptimeValue value={displayedUptime(product)} />
|
||||||
|
|
@ -234,19 +237,21 @@ const InfoPanel = async ({ profileId, isAdmin }: { profileId: string; isAdmin: b
|
||||||
) : (
|
) : (
|
||||||
<ul className="flex flex-col divide-y divide-border rounded-lg border border-border">
|
<ul className="flex flex-col divide-y divide-border rounded-lg border border-border">
|
||||||
{tasks.map((task) => (
|
{tasks.map((task) => (
|
||||||
<li key={task.id} className="flex items-center gap-4 px-4 py-3">
|
<li key={task.id} className="flex items-start gap-3 px-4 py-3 sm:items-center sm:gap-4">
|
||||||
<Badge variant={task.status === "in_progress" ? "warning" : "neutral"} className="w-20">
|
<Badge variant={task.status === "in_progress" ? "warning" : "neutral"} className="mt-0.5 w-20 shrink-0 sm:mt-0">
|
||||||
{TASK_STATUS_LABELS[task.status]}
|
{TASK_STATUS_LABELS[task.status]}
|
||||||
</Badge>
|
</Badge>
|
||||||
<div className="flex min-w-0 flex-1 flex-col">
|
<div className="flex min-w-0 flex-1 flex-col gap-0.5 sm:flex-row sm:items-center sm:gap-4">
|
||||||
<span className="font-medium">{task.name}</span>
|
<div className="flex min-w-0 flex-1 flex-col">
|
||||||
{task.description && (
|
<span className="font-medium break-words">{task.name}</span>
|
||||||
<span className="text-small text-muted-foreground">{task.description}</span>
|
{task.description && (
|
||||||
)}
|
<span className="text-small text-muted-foreground break-words">{task.description}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span className="text-small text-muted-foreground tabular-nums sm:shrink-0">
|
||||||
|
{formatTaskTiming(task.status, task.since, today)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="shrink-0 text-small text-muted-foreground tabular-nums">
|
|
||||||
{formatTaskTiming(task.status, task.since, today)}
|
|
||||||
</span>
|
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<div className="flex shrink-0 gap-1">
|
<div className="flex shrink-0 gap-1">
|
||||||
<TaskDialog profileId={profileId} task={task} />
|
<TaskDialog profileId={profileId} task={task} />
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,9 @@ export function StammdatenPanel({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-8 w-full">
|
<div className="flex w-full flex-col gap-6 md:gap-8">
|
||||||
<div className="w-full flex flex-row justify-between items-center">
|
<div className="flex w-full flex-wrap items-center justify-between gap-3">
|
||||||
<h1 className="text-3xl font-semibold">Stammdaten</h1>
|
<h1 className="text-2xl font-semibold md:text-3xl">Stammdaten</h1>
|
||||||
{!isEditing && (
|
{!isEditing && (
|
||||||
<Button variant="outline" onClick={() => setIsEditing(true)}>
|
<Button variant="outline" onClick={() => setIsEditing(true)}>
|
||||||
<Pen className="w-4 h-4 mr-2" /> Edit
|
<Pen className="w-4 h-4 mr-2" /> Edit
|
||||||
|
|
|
||||||
51
package-lock.json
generated
51
package-lock.json
generated
|
|
@ -693,18 +693,6 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@dotenvx/dotenvx/node_modules/picomatch": {
|
|
||||||
"version": "4.0.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
|
|
||||||
"integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@dotenvx/dotenvx/node_modules/signal-exit": {
|
"node_modules/@dotenvx/dotenvx/node_modules/signal-exit": {
|
||||||
"version": "3.0.7",
|
"version": "3.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
|
||||||
|
|
@ -2625,7 +2613,7 @@
|
||||||
"version": "19.3.0",
|
"version": "19.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz",
|
||||||
"integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==",
|
"integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
|
|
@ -4254,7 +4242,7 @@
|
||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/damerau-levenshtein": {
|
"node_modules/damerau-levenshtein": {
|
||||||
|
|
@ -7423,6 +7411,18 @@
|
||||||
"node": ">=8.6"
|
"node": ">=8.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/micromatch/node_modules/picomatch": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/mime-db": {
|
"node_modules/mime-db": {
|
||||||
"version": "1.54.0",
|
"version": "1.54.0",
|
||||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
|
||||||
|
|
@ -8184,12 +8184,12 @@
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/picomatch": {
|
"node_modules/picomatch": {
|
||||||
"version": "2.3.2",
|
"version": "4.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
|
||||||
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
"integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8.6"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
|
|
@ -9674,19 +9674,6 @@
|
||||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tinyglobby/node_modules/picomatch": {
|
|
||||||
"version": "4.0.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
|
|
||||||
"integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/to-regex-range": {
|
"node_modules/to-regex-range": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||||
|
|
@ -9897,7 +9884,7 @@
|
||||||
"version": "5.9.3",
|
"version": "5.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
|
|
|
||||||
2
proxy.ts
2
proxy.ts
|
|
@ -22,5 +22,5 @@ export default async function proxy(request: NextRequest) {
|
||||||
|
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
matcher: ["/((?!_next/static|_next/image|favicon.ico|pictures).*)"],
|
matcher: ["/((?!_next/static|_next/image|favicon.ico|pictures|manifest.webmanifest|apple-icon|icons/icon-).*)"],
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue