updated
This commit is contained in:
parent
5e6f7e872d
commit
e2a71e4fa5
@ -143,21 +143,21 @@ function DeviceDetailsGrid({
|
||||
|
||||
<div className="mt-2 space-y-2">
|
||||
{/* Zeile 1: Badge + Buttons nebeneinander */}
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
{/* Badge */}
|
||||
<div className="flex w-auto shrink-0">
|
||||
<div className="min-w-0">
|
||||
<span
|
||||
className={`inline-flex w-fit items-center gap-2 rounded-full px-3 py-1 text-xs font-medium ${statusClasses}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-2.5 w-2.5 rounded-full ${dotClasses}`}
|
||||
/>
|
||||
<span>{statusLabel}</span>
|
||||
<span className="truncate">{statusLabel}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* rechte Seite: Buttons */}
|
||||
<div className="flex flex-row gap-2">
|
||||
<div className="shrink-0 flex flex-row gap-2">
|
||||
<Button
|
||||
size="md"
|
||||
variant="primary"
|
||||
@ -528,7 +528,7 @@ export default function DeviceDetailModal({
|
||||
{ id: 'details', label: 'Details' },
|
||||
{ id: 'history', label: 'Änderungsverlauf' },
|
||||
]}
|
||||
variant='pillsBrand'
|
||||
variant="pillsBrand"
|
||||
value={activeTab}
|
||||
onChange={(id) =>
|
||||
setActiveTab(id as 'details' | 'history')
|
||||
@ -553,11 +553,11 @@ export default function DeviceDetailModal({
|
||||
}
|
||||
sidebar={
|
||||
device ? (
|
||||
<div className="hidden w-full h-full sm:flex sm:flex-col sm:gap-4">
|
||||
<div className="hidden h-full w-full sm:flex sm:flex-col sm:gap-4">
|
||||
{/* QR-Code oben, nicht scrollend */}
|
||||
<div className="rounded-lg border border-gray-800 bg-gray-900/70 px-4 py-3 shadow-sm">
|
||||
<div className="mt-2 flex justify-center">
|
||||
<div className="rounded-md bg-black/80 px-3 py-3 flex flex-col items-center gap-2">
|
||||
<div className="flex flex-col items-center gap-2 rounded-md bg-black/80 px-3 py-3">
|
||||
<DeviceQrCode inventoryNumber={device.inventoryNumber} />
|
||||
<p className="text-[13px] font-mono tracking-wide text-gray-100">
|
||||
{device.inventoryNumber}
|
||||
@ -566,10 +566,10 @@ export default function DeviceDetailModal({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-800 dark:border-white/10 mx-1" />
|
||||
<div className="mx-1 border-t border-gray-800 dark:border-white/10" />
|
||||
|
||||
{/* Änderungsverlauf: nimmt den Rest der Höhe ein und scrollt intern */}
|
||||
<div className="flex-1 min-h-0 overflow-hidden">
|
||||
<div className="min-h-0 flex-1 overflow-hidden">
|
||||
<DeviceHistorySidebar
|
||||
key={device.updatedAt}
|
||||
inventoryNumber={device.inventoryNumber}
|
||||
@ -581,6 +581,8 @@ export default function DeviceDetailModal({
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
{/* 👇 Nur der Body ist scrollbar, Header & Footer bleiben fix */}
|
||||
<div className="max-h-[70vh] sm:max-h-[75vh] overflow-y-auto pr-2">
|
||||
{loading && (
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Gerätedaten werden geladen …
|
||||
@ -602,7 +604,11 @@ export default function DeviceDetailModal({
|
||||
device={device}
|
||||
onStartLoan={handleStartLoan}
|
||||
canEdit={canEdit}
|
||||
onEdit={onEdit ? () => onEdit(device.inventoryNumber) : undefined}
|
||||
onEdit={
|
||||
onEdit
|
||||
? () => onEdit(device.inventoryNumber)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<DeviceHistorySidebar
|
||||
@ -619,13 +625,17 @@ export default function DeviceDetailModal({
|
||||
device={device}
|
||||
onStartLoan={handleStartLoan}
|
||||
canEdit={canEdit}
|
||||
onEdit={onEdit ? () => onEdit(device.inventoryNumber) : undefined}
|
||||
onEdit={
|
||||
onEdit ? () => onEdit(device.inventoryNumber) : undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
|
||||
{device && (
|
||||
<LoanDeviceModal
|
||||
open={loanModalOpen}
|
||||
|
||||
@ -577,18 +577,31 @@ export default function UsersTablesClient({
|
||||
header: 'Arbeitsname',
|
||||
sortable: true,
|
||||
render: (row) => {
|
||||
const isCurrent =
|
||||
!!currentUserId && row.nwkennung === currentUserId;
|
||||
const isCurrent = !!currentUserId && row.nwkennung === currentUserId;
|
||||
|
||||
const fullName = [row.firstName, row.lastName].filter(Boolean).join(' ');
|
||||
const group = allGroups.find((g) => g.id === row.groupId);
|
||||
const canEdit = !!group?.canEditDevices;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{row.arbeitsname}</span>
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<span className="truncate">{row.arbeitsname}</span>
|
||||
{isCurrent && (
|
||||
<Badge size="sm" tone="green" variant="flat">
|
||||
Du
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Mobile-Detailszeile: Vor-/Nachname + Rechte kompakt */}
|
||||
<div className="sm:hidden mt-0.5 flex flex-wrap gap-x-2 gap-y-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{fullName && <span className="truncate">{fullName}</span>}
|
||||
<span className="before:content-['·'] before:px-1">
|
||||
Geräte: {canEdit ? 'Ja' : 'Nein'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
@ -596,20 +609,22 @@ export default function UsersTablesClient({
|
||||
key: 'lastName',
|
||||
header: 'Nachname',
|
||||
sortable: true,
|
||||
headerClassName: 'hidden sm:table-cell',
|
||||
cellClassName: 'hidden sm:table-cell',
|
||||
},
|
||||
{
|
||||
key: 'firstName',
|
||||
header: 'Vorname',
|
||||
sortable: true,
|
||||
headerClassName: 'hidden sm:table-cell',
|
||||
cellClassName: 'hidden sm:table-cell',
|
||||
},
|
||||
|
||||
// 🔹 NEUE SPALTE: Darf Geräte bearbeiten
|
||||
{
|
||||
key: 'canEditDevices',
|
||||
header: 'Geräte bearbeiten',
|
||||
sortable: false,
|
||||
headerClassName: 'w-40',
|
||||
cellClassName: 'w-40',
|
||||
headerClassName: 'hidden md:table-cell w-40',
|
||||
cellClassName: 'hidden md:table-cell w-40',
|
||||
render: (row) => {
|
||||
const group = allGroups.find((g) => g.id === row.groupId);
|
||||
const canEdit = !!group?.canEditDevices;
|
||||
@ -641,13 +656,30 @@ export default function UsersTablesClient({
|
||||
key: 'groupId',
|
||||
header: 'Gruppe',
|
||||
sortable: false,
|
||||
render: (row) => (
|
||||
render: (row) => {
|
||||
const groupName =
|
||||
allGroups.find((g) => g.id === row.groupId)?.name ?? 'Ohne Gruppe';
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Mobile: nur Text (kein gequetschtes Select) */}
|
||||
<div className="sm:hidden">
|
||||
<span className="inline-flex max-w-[220px] truncate rounded-md bg-gray-100 px-2 py-1 text-xs text-gray-700 dark:bg-gray-800 dark:text-gray-200">
|
||||
{groupName}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Desktop/Tablet: interaktives AssignGroupForm */}
|
||||
<div className="hidden sm:block">
|
||||
<AssignGroupForm
|
||||
user={row}
|
||||
defaultGroupId={row.groupId ?? null}
|
||||
allGroups={allGroups}
|
||||
/>
|
||||
),
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
[allGroups, currentUserId],
|
||||
@ -681,7 +713,7 @@ export default function UsersTablesClient({
|
||||
<div className="space-y-4">
|
||||
{/* 1. Tab-Reihe: Hauptgruppen (Cluster) + Cluster-Löschen-Button */}
|
||||
<div className="mt-2 space-y-2">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between sm:gap-3">
|
||||
<Tabs
|
||||
tabs={mainTabs}
|
||||
variant='pillsBrand'
|
||||
@ -708,7 +740,7 @@ export default function UsersTablesClient({
|
||||
|
||||
{/* 2. Tab-Reihe: Untergruppen + Untergruppe-löschen-Button */}
|
||||
{safeActiveMainTab !== 'ungrouped' && subTabs.length > 0 && (
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between sm:gap-3">
|
||||
<Tabs
|
||||
tabs={subTabs}
|
||||
variant='pillsBrand'
|
||||
@ -736,6 +768,10 @@ export default function UsersTablesClient({
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
{/* Mobile: nutze volle Breite + horizontales Scrollen statt Quetschen */}
|
||||
<div className="-mx-4 sm:mx-0">
|
||||
<div className="overflow-x-auto px-4 sm:px-0">
|
||||
<div className="min-w-[860px] sm:min-w-0">
|
||||
<Table<UserWithAvatar>
|
||||
data={tableData}
|
||||
columns={userColumns}
|
||||
@ -761,6 +797,9 @@ export default function UsersTablesClient({
|
||||
defaultSortKey="arbeitsname"
|
||||
defaultSortDirection="asc"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Floating Actions in Card, unten mittig über der Tabelle */}
|
||||
{selectedUserIds.length > 0 && (
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// /components/GlobalSearch.tsx
|
||||
'use client';
|
||||
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Combobox } from '@headlessui/react';
|
||||
import { MagnifyingGlassIcon } from '@heroicons/react/20/solid';
|
||||
import clsx from 'clsx';
|
||||
@ -24,6 +24,8 @@ export default function GlobalSearch({ onDeviceSelected }: GlobalSearchProps) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadError, setLoadError] = useState<string | null>(null);
|
||||
const [hasLoaded, setHasLoaded] = useState(false);
|
||||
const anchorRef = useRef<HTMLDivElement>(null);
|
||||
const [menuTop, setMenuTop] = useState<number | null>(null);
|
||||
|
||||
// Geräte nur einmal laden, wenn das erste Mal gesucht wird
|
||||
useEffect(() => {
|
||||
@ -105,6 +107,39 @@ export default function GlobalSearch({ onDeviceSelected }: GlobalSearchProps) {
|
||||
const hasMenu =
|
||||
query.trim().length > 0 && (loading || loadError || filteredDevices.length > 0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasMenu) {
|
||||
setMenuTop(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const update = () => {
|
||||
const el = anchorRef.current;
|
||||
if (!el) return;
|
||||
|
||||
// Nur auf Mobile fix positionieren
|
||||
const isMobile = window.matchMedia('(max-width: 639px)').matches; // < sm
|
||||
if (!isMobile) {
|
||||
setMenuTop(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = el.getBoundingClientRect();
|
||||
// +4px entspricht ungefähr "mt-1"
|
||||
setMenuTop(rect.bottom + 4);
|
||||
};
|
||||
|
||||
update();
|
||||
window.addEventListener('resize', update);
|
||||
// capture=true, damit es auch in scroll-containern zuverlässig feuert
|
||||
window.addEventListener('scroll', update, true);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', update);
|
||||
window.removeEventListener('scroll', update, true);
|
||||
};
|
||||
}, [hasMenu, query, loading, loadError, filteredDevices.length]);
|
||||
|
||||
const handleSelect = (item: DeviceSearchItem | null) => {
|
||||
if (!item) return;
|
||||
onDeviceSelected?.(item.inventoryNumber);
|
||||
@ -114,7 +149,7 @@ export default function GlobalSearch({ onDeviceSelected }: GlobalSearchProps) {
|
||||
|
||||
return (
|
||||
<Combobox value={null} onChange={handleSelect} nullable>
|
||||
<div className="relative w-full">
|
||||
<div ref={anchorRef} className="relative w-full">
|
||||
{/* Suchfeld */}
|
||||
<MagnifyingGlassIcon
|
||||
aria-hidden="true"
|
||||
@ -131,8 +166,14 @@ export default function GlobalSearch({ onDeviceSelected }: GlobalSearchProps) {
|
||||
{/* Dropdown-Menü unterhalb */}
|
||||
{hasMenu && (
|
||||
<Combobox.Options
|
||||
style={menuTop != null ? { top: menuTop } : undefined}
|
||||
className={clsx(
|
||||
'absolute z-50 mt-1 max-h-80 w-full overflow-auto rounded-md bg-white py-1 text-sm shadow-lg ring-1 ring-black/5',
|
||||
// ✅ Mobile: wirklich Viewport-edge-to-edge
|
||||
'fixed z-50 max-h-80 inset-x-0 overflow-auto bg-white py-1 text-sm shadow-lg ring-1 ring-black/5',
|
||||
// optional: edge-to-edge fühlt sich ohne rounding oft besser an
|
||||
'rounded-none sm:rounded-md',
|
||||
// ✅ Ab sm: wieder "normal" unter dem Input, so wie vorher
|
||||
'sm:absolute sm:inset-x-0 sm:top-full sm:mt-1',
|
||||
'dark:bg-gray-800 dark:ring-white/10',
|
||||
)}
|
||||
>
|
||||
@ -162,7 +203,7 @@ export default function GlobalSearch({ onDeviceSelected }: GlobalSearchProps) {
|
||||
value={device}
|
||||
className={({ active }) =>
|
||||
clsx(
|
||||
'cursor-pointer px-3 py-2',
|
||||
'cursor-pointer w-full px-3 py-2', // ⬅️ hier: w-full ergänzt
|
||||
'flex flex-col gap-0.5',
|
||||
active
|
||||
? 'bg-indigo-50 text-indigo-700 dark:bg-indigo-600/25 dark:text-white'
|
||||
@ -184,9 +225,7 @@ export default function GlobalSearch({ onDeviceSelected }: GlobalSearchProps) {
|
||||
{device.name || 'Ohne Bezeichnung'}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
{device.manufacturer && (
|
||||
<span>{device.manufacturer}</span>
|
||||
)}
|
||||
{device.manufacturer && <span>{device.manufacturer}</span>}
|
||||
{device.group && (
|
||||
<span className="before:content-['·'] before:px-1">
|
||||
{device.group}
|
||||
|
||||
@ -126,7 +126,7 @@ export default function ScanModal({ open, onClose, onResult }: ScanModalProps) {
|
||||
|
||||
{!error && (
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Richte deine Kamera auf den QR-Code. Sobald er erkannt wird, öffnet sich das Gerätedetail.
|
||||
Richte deine Kamera auf den QR-Code. Sobald er erkannt wird, öffnen sich die Gerätedetails.
|
||||
</p>
|
||||
)}
|
||||
|
||||
|
||||
@ -137,7 +137,6 @@ function renderActionButton(
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/* ───────── Modal-Komponente ───────── */
|
||||
|
||||
export function Modal({
|
||||
@ -173,14 +172,14 @@ export function Modal({
|
||||
className="fixed inset-0 bg-gray-500/75 transition-opacity data-closed:opacity-0 data-enter:duration-300 data-enter:ease-out data-leave:duration-200 data-leave:ease-in dark:bg-gray-900/50"
|
||||
/>
|
||||
|
||||
<div className="fixed inset-0 z-50 w-screen overflow-y-auto">
|
||||
<div className="flex min-h-full items-start justify-center p-4 text-center sm:p-8">
|
||||
{/* Zentrierter Container – keine eigene Scrollbar mehr, die Höhe begrenzt das Panel */}
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 text-center sm:p-8">
|
||||
<DialogPanel
|
||||
transition
|
||||
className={clsx(
|
||||
'relative flex w-full max-h-[calc(100vh-8rem)] lg:max-h-[950px] flex-col transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all ' +
|
||||
'relative flex w-full max-h-[calc(100vh-3rem)] flex-col transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all ' +
|
||||
'data-closed:translate-y-4 data-closed:opacity-0 data-enter:duration-300 data-enter:ease-out ' +
|
||||
'data-leave:duration-200 data-leave:ease-in sm:my-8 data-closed:sm:translate-y-0 data-closed:sm:scale-95 ' +
|
||||
'data-leave:duration-200 data-leave:ease-in ' +
|
||||
'dark:bg-gray-800 dark:outline dark:-outline-offset-1 dark:outline-white/10',
|
||||
panelSizeClasses,
|
||||
)}
|
||||
@ -199,9 +198,9 @@ export function Modal({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* HEADER + MAIN (Body+Sidebar) */}
|
||||
<div className="flex-1 flex flex-col min-h-0 bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4 dark:bg-gray-800 overflow-hidden">
|
||||
{/* Header */}
|
||||
{/* HEADER + MAIN (Body+Sidebar) – dieser Block bekommt flex-1 und min-h-0 */}
|
||||
<div className="flex min-h-0 flex-1 flex-col overflow-hidden bg-white px-4 pt-5 pb-4 dark:bg-gray-800 sm:p-6 sm:pb-4">
|
||||
{/* Header (nicht scrollend) */}
|
||||
<div
|
||||
className={clsx(
|
||||
'flex',
|
||||
@ -232,9 +231,7 @@ export function Modal({
|
||||
|
||||
<div
|
||||
className={clsx(
|
||||
isAlert
|
||||
? 'mt-3 sm:mt-0 sm:text-left'
|
||||
: 'mt-3 sm:mt-4',
|
||||
isAlert ? 'mt-3 sm:mt-0 sm:text-left' : 'mt-3 sm:mt-4',
|
||||
!isAlert && 'w-full',
|
||||
)}
|
||||
>
|
||||
@ -262,21 +259,21 @@ export function Modal({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* MAIN: Body + Sidebar – nimmt den Rest der Höhe ein */}
|
||||
{/* MAIN: Body + Sidebar – bekommt min-h-0 und nur der Body scrollt */}
|
||||
{(bodyContent || sidebar) && (
|
||||
<div
|
||||
className={clsx(
|
||||
'mt-6 flex flex-col min-h-0',
|
||||
'mt-6 flex min-h-0 flex-1 flex-col',
|
||||
sidebar
|
||||
? 'sm:mt-8 sm:flex-row sm:items-stretch sm:gap-6'
|
||||
: 'overflow-y-auto' // nur ohne Sidebar soll der Body global scrollen
|
||||
: undefined,
|
||||
)}
|
||||
>
|
||||
{/* Linker Inhalt (Details / Formulare) */}
|
||||
{/* Linker Inhalt (scrollbar) */}
|
||||
{bodyContent && (
|
||||
<div
|
||||
className={clsx(
|
||||
'flex-1 min-h-0 overflow-y-auto text-left',
|
||||
'min-h-0 flex-1 overflow-y-auto text-left',
|
||||
!sidebar && 'mx-auto max-w-2xl',
|
||||
)}
|
||||
>
|
||||
@ -284,9 +281,9 @@ export function Modal({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Rechte Sidebar (QR + Verlauf) */}
|
||||
{/* Rechte Sidebar (nicht scrollbar, eigene interne Scroller möglich) */}
|
||||
{sidebar && (
|
||||
<aside className="sm:min-h-0 sm:overflow-hidden sm:w-60 lg:w-80 sm:shrink-0">
|
||||
<aside className="sm:min-h-0 sm:w-60 sm:shrink-0 sm:overflow-hidden lg:w-80">
|
||||
{sidebar}
|
||||
</aside>
|
||||
)}
|
||||
@ -294,7 +291,7 @@ export function Modal({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
{/* Footer (immer sichtbar, außerhalb des scrollbaren Bereichs) */}
|
||||
{footer ? (
|
||||
footer
|
||||
) : hasActions ? (
|
||||
@ -326,10 +323,8 @@ export function Modal({
|
||||
) : null}
|
||||
</DialogPanel>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default Modal;
|
||||
|
||||
|
||||
@ -171,7 +171,7 @@ export default function Table<T>(props: TableProps<T>) {
|
||||
return (
|
||||
<div className="relative overflow-visible rounded-lg border border-gray-200 bg-white shadow-sm dark:border-white/10 dark:bg-gray-900/40">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full table-fixed divide-y divide-gray-200 text-left text-sm dark:divide-white/10">
|
||||
<table className="w-full table-fixed divide-y divide-gray-200 text-left text-sm dark:divide-white/10">
|
||||
<thead className="bg-gray-50 dark:bg-gray-800/60">
|
||||
<tr>
|
||||
{selectable && (
|
||||
|
||||
736
package-lock.json
generated
736
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -21,7 +21,7 @@
|
||||
"@prisma/client": "^7.1.0",
|
||||
"@zxing/browser": "^0.1.5",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"next": "16.0.3",
|
||||
"next": "^16.1.4",
|
||||
"next-auth": "^4.24.13",
|
||||
"pg": "^8.16.3",
|
||||
"postcss": "^8.5.6",
|
||||
@ -38,11 +38,11 @@
|
||||
"@types/pg": "^8.15.6",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"baseline-browser-mapping": "^2.8.32",
|
||||
"baseline-browser-mapping": "^2.9.17",
|
||||
"dotenv": "^17.2.3",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.0.3",
|
||||
"prisma": "^7.1.0",
|
||||
"prisma": "^6.19.2",
|
||||
"tailwindcss": "^4.1.17",
|
||||
"tsx": "^4.20.6",
|
||||
"typescript": "^5.9.3"
|
||||
|
||||
BIN
public/avatars/nw083118-1765801010828.png
Normal file
BIN
public/avatars/nw083118-1765801010828.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.0 KiB |
Loading…
x
Reference in New Issue
Block a user