Prepare Kalendartool for MPM and disable invites
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
* - "Kalender erstellen" oeffnet ein Erstellen-Modal (POST /api/calendars).
|
||||
* - Alle Modals sind per X-Icon, ESC und Hintergrund-Klick schliessbar.
|
||||
*/
|
||||
import { appFetch } from '@/lib/client-fetch';
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Button, Card, ErrorMessage, Input, Label } from '@/components/ui/ui';
|
||||
@@ -66,7 +67,7 @@ export function CalendarManager({
|
||||
setIsPending(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await fetch('/api/calendars', {
|
||||
const response = await appFetch('/api/calendars', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(values),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
/** Formular zum Erstellen eines Kalenders (nur Admin). */
|
||||
import { appFetch } from '@/lib/client-fetch';
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Button, Card, ErrorMessage, Input, Label } from '@/components/ui/ui';
|
||||
@@ -17,7 +18,7 @@ export function CreateCalendarForm() {
|
||||
|
||||
const formData = new FormData(event.currentTarget);
|
||||
try {
|
||||
const response = await fetch('/api/calendars', {
|
||||
const response = await appFetch('/api/calendars', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
/** Formular zum Bearbeiten/Deaktivieren eines Kalenders (Admin). */
|
||||
import { appFetch } from '@/lib/client-fetch';
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Button, Card, ErrorMessage, Input, Label } from '@/components/ui/ui';
|
||||
@@ -29,7 +30,7 @@ export function EditCalendarForm({
|
||||
|
||||
const formData = new FormData(event.currentTarget);
|
||||
try {
|
||||
const response = await fetch(`/api/calendars/${calendarId}`, {
|
||||
const response = await appFetch(`/api/calendars/${calendarId}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
* Dashboard verwendet (DRY). Schliessen via X-Icon, ESC oder
|
||||
* Hintergrund-Klick; waehrend des Speicherns ist Schliessen blockiert.
|
||||
*/
|
||||
import { appFetch } from '@/lib/client-fetch';
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Button, ErrorMessage, Input, Label } from '@/components/ui/ui';
|
||||
@@ -61,7 +62,7 @@ export function EditCalendarModal({
|
||||
|
||||
const formData = new FormData(event.currentTarget);
|
||||
try {
|
||||
const response = await fetch(`/api/calendars/${calendar.id}`, {
|
||||
const response = await appFetch(`/api/calendars/${calendar.id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* Invite-Verwaltung (plan.md Abschnitt 3): Link erstellen, Liste mit
|
||||
* Status (Aktiv/Abgelaufen/Verwendet) und Zaehleruebersicht.
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Button, Card, ErrorMessage, Badge } from '@/components/ui/ui';
|
||||
|
||||
export interface InviteRow {
|
||||
id: string;
|
||||
expiresAt: Date | null;
|
||||
usedAt: Date | null;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export function InviteManager({
|
||||
calendarId,
|
||||
invites,
|
||||
}: {
|
||||
calendarId: string;
|
||||
invites: InviteRow[];
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [lastInviteUrl, setLastInviteUrl] = useState<string | null>(null);
|
||||
|
||||
async function createInvite() {
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await fetch(`/api/calendars/${calendarId}/invites`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ calendarId }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const data = (await response.json()) as { error?: string };
|
||||
setError(data.error ?? 'Erstellen fehlgeschlagen.');
|
||||
return;
|
||||
}
|
||||
const data = (await response.json()) as { inviteUrl: string };
|
||||
setLastInviteUrl(data.inviteUrl);
|
||||
router.refresh();
|
||||
} catch {
|
||||
setError('Netzwerkfehler. Bitte erneut versuchen.');
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const active = invites.filter(
|
||||
(invite) =>
|
||||
invite.usedAt === null &&
|
||||
(invite.expiresAt === null || invite.expiresAt > now),
|
||||
).length;
|
||||
const expired = invites.filter(
|
||||
(invite) =>
|
||||
invite.usedAt === null &&
|
||||
invite.expiresAt !== null &&
|
||||
invite.expiresAt <= now,
|
||||
).length;
|
||||
const used = invites.filter((invite) => invite.usedAt !== null).length;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold">Einladungen</h2>
|
||||
<Button type="button" variant="secondary" disabled={busy} onClick={createInvite}>
|
||||
{busy ? 'Erstelle …' : '+ Benutzer einladen'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ErrorMessage message={error} />
|
||||
|
||||
{lastInviteUrl && (
|
||||
<div className="mb-4 rounded-md bg-slate-100 px-3 py-2 text-sm text-slate-800 dark:bg-slate-800 dark:text-slate-200">
|
||||
<p className="mb-1 font-medium">Neuer Invite-Link (einmalig anzeigen):</p>
|
||||
<code className="break-all text-xs">{lastInviteUrl}</code>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mb-4 flex gap-4 text-sm">
|
||||
<Badge variant="success">Aktiv: {active}</Badge>
|
||||
<Badge variant="warning">Abgelaufen: {expired}</Badge>
|
||||
<Badge variant="neutral">Verwendet: {used}</Badge>
|
||||
</div>
|
||||
|
||||
{invites.length === 0 ? (
|
||||
<p className="text-sm text-slate-500">Noch keine Einladungen erstellt.</p>
|
||||
) : (
|
||||
<ul className="space-y-1 text-sm text-slate-600">
|
||||
{invites.slice(0, 10).map((invite) => (
|
||||
<li key={invite.id} className="flex justify-between rounded bg-slate-50 px-3 py-1.5">
|
||||
<span>Erstellt {invite.createdAt.toLocaleDateString('de-DE')}</span>
|
||||
<span>
|
||||
{invite.usedAt
|
||||
? 'Verwendet'
|
||||
: invite.expiresAt && invite.expiresAt <= now
|
||||
? 'Abgelaufen'
|
||||
: 'Aktiv'}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* Einladungs-Sektion fuer Modals: laedt die Einladungen eines
|
||||
* Kalenders per GET /api/calendars/[id]/invites und ermoeglicht das
|
||||
* Erstellen neuer Invite-Links (POST, gleicher Endpoint).
|
||||
*
|
||||
* Der Klartext-Link wird nur einmal direkt nach dem Erstellen
|
||||
* angezeigt (in der DB liegt nur der Hash - plan.md Abschnitt 9).
|
||||
* Wird als eigenstaendige Komponente gefuehrt, damit sie sowohl im
|
||||
* Bearbeiten-Modal als auch kuenftig anderswo verwendet werden kann.
|
||||
*/
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Button, ErrorMessage, Badge } from '@/components/ui/ui';
|
||||
|
||||
/** Einladung, wie der GET-Endpoint sie liefert (Datumswerte als ISO). */
|
||||
interface InviteApiRow {
|
||||
id: string;
|
||||
expiresAt: string | null;
|
||||
usedAt: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export function InviteSection({ calendarId }: { calendarId: string }) {
|
||||
const [invites, setInvites] = useState<InviteApiRow[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [lastInviteUrl, setLastInviteUrl] = useState<string | null>(null);
|
||||
|
||||
/** Laedt die Einladungen des Kalenders. */
|
||||
const loadInvites = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await fetch(`/api/calendars/${calendarId}/invites`);
|
||||
if (!response.ok) {
|
||||
const data = (await response.json().catch(() => ({}))) as { error?: string };
|
||||
setError(data.error ?? 'Einladungen konnten nicht geladen werden.');
|
||||
return;
|
||||
}
|
||||
const data = (await response.json()) as { invites: InviteApiRow[] };
|
||||
setInvites(data.invites);
|
||||
} catch {
|
||||
setError('Netzwerkfehler. Bitte erneut versuchen.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [calendarId]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadInvites();
|
||||
}, [loadInvites]);
|
||||
|
||||
/** Erstellt einen neuen Invite-Link und zeigt ihn einmalig an. */
|
||||
async function createInvite() {
|
||||
setIsCreating(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await fetch(`/api/calendars/${calendarId}/invites`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ calendarId }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const data = (await response.json().catch(() => ({}))) as { error?: string };
|
||||
setError(data.error ?? 'Erstellen fehlgeschlagen.');
|
||||
return;
|
||||
}
|
||||
const data = (await response.json()) as { inviteUrl: string };
|
||||
setLastInviteUrl(data.inviteUrl);
|
||||
await loadInvites();
|
||||
} catch {
|
||||
setError('Netzwerkfehler. Bitte erneut versuchen.');
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
}
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const active = invites.filter(
|
||||
(invite) =>
|
||||
invite.usedAt === null &&
|
||||
(invite.expiresAt === null || new Date(invite.expiresAt) > now),
|
||||
).length;
|
||||
const expired = invites.filter(
|
||||
(invite) =>
|
||||
invite.usedAt === null &&
|
||||
invite.expiresAt !== null &&
|
||||
new Date(invite.expiresAt) <= now,
|
||||
).length;
|
||||
const used = invites.filter((invite) => invite.usedAt !== null).length;
|
||||
|
||||
return (
|
||||
<section aria-label="Einladungen">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h3 className="text-base font-semibold">Einladungen</h3>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
disabled={isCreating}
|
||||
onClick={createInvite}
|
||||
>
|
||||
{isCreating ? 'Erstelle …' : '+ Benutzer einladen'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ErrorMessage message={error} />
|
||||
|
||||
{lastInviteUrl && (
|
||||
<div className="mb-3 rounded-md bg-slate-100 px-3 py-2 text-sm text-slate-800 dark:bg-slate-800 dark:text-slate-200">
|
||||
<p className="mb-1 font-medium">Neuer Invite-Link (einmalig anzeigen):</p>
|
||||
<code className="break-all text-xs">{lastInviteUrl}</code>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoading && (
|
||||
<div className="mb-3 flex gap-3">
|
||||
<Badge variant="success">Aktiv: {active}</Badge>
|
||||
<Badge variant="warning">Abgelaufen: {expired}</Badge>
|
||||
<Badge variant="neutral">Verwendet: {used}</Badge>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLoading ? (
|
||||
<p className="text-sm text-slate-500">Einladungen werden geladen …</p>
|
||||
) : invites.length === 0 ? (
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400">
|
||||
Noch keine Einladungen erstellt.
|
||||
</p>
|
||||
) : (
|
||||
<ul className="space-y-1 text-sm text-slate-600 dark:text-slate-300">
|
||||
{invites.slice(0, 10).map((invite) => {
|
||||
const isUsed = invite.usedAt !== null;
|
||||
const isExpired =
|
||||
!isUsed && invite.expiresAt !== null && new Date(invite.expiresAt) <= now;
|
||||
return (
|
||||
<li
|
||||
key={invite.id}
|
||||
className="flex justify-between rounded bg-slate-50 px-3 py-1.5 dark:bg-slate-800"
|
||||
>
|
||||
<span>
|
||||
Erstellt {new Date(invite.createdAt).toLocaleDateString('de-DE')}
|
||||
</span>
|
||||
<span>
|
||||
{isUsed
|
||||
? 'Verwendet'
|
||||
: isExpired
|
||||
? 'Abgelaufen'
|
||||
: 'Aktiv'}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +1,17 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* Mitgliederverwaltung fuer Modals (DB-basiertes Einladungssystem).
|
||||
* Mitgliederverwaltung fuer Kalender.
|
||||
*
|
||||
* Statt Invite-Links werden bestehende Benutzer direkt per E-Mail
|
||||
* ueber die Datenbank gesucht und dem Kalender zugeordnet:
|
||||
* Vorhandene Benutzer werden direkt per E-Mail ueber die Datenbank
|
||||
* gesucht und dem Kalender zugeordnet:
|
||||
* - Suchfeld mit Live-Vorschlaegen (GET /api/users/search?q=...)
|
||||
* - Hinzufuegen per Klick (POST /api/calendars/[id]/members)
|
||||
* - Mitgliederliste mit Entfernen-Button (DELETE ...?memberId=...)
|
||||
*
|
||||
* Nur fuer Admins gerendert; der Server prueft die Berechtigung final.
|
||||
*/
|
||||
import { appFetch } from '@/lib/client-fetch';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { ErrorMessage } from '@/components/ui/ui';
|
||||
|
||||
@@ -44,7 +45,7 @@ export function MemberSection({ calendarId }: { calendarId: string }) {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await fetch(`/api/calendars/${calendarId}/members`);
|
||||
const response = await appFetch(`/api/calendars/${calendarId}/members`);
|
||||
if (!response.ok) {
|
||||
const data = (await response.json().catch(() => ({}))) as { error?: string };
|
||||
setError(data.error ?? 'Mitglieder konnten nicht geladen werden.');
|
||||
@@ -75,7 +76,7 @@ export function MemberSection({ calendarId }: { calendarId: string }) {
|
||||
setAddingEmail(email);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await fetch(`/api/calendars/${calendarId}/members`, {
|
||||
const response = await appFetch(`/api/calendars/${calendarId}/members`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email }),
|
||||
@@ -100,7 +101,7 @@ export function MemberSection({ calendarId }: { calendarId: string }) {
|
||||
setRemovingId(memberId);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await fetch(
|
||||
const response = await appFetch(
|
||||
`/api/calendars/${calendarId}/members?memberId=${encodeURIComponent(memberId)}`,
|
||||
{ method: 'DELETE' },
|
||||
);
|
||||
@@ -130,7 +131,7 @@ export function MemberSection({ calendarId }: { calendarId: string }) {
|
||||
searchTimer.current = setTimeout(async () => {
|
||||
setIsSearching(true);
|
||||
try {
|
||||
const response = await fetch(
|
||||
const response = await appFetch(
|
||||
`/api/users/search?q=${encodeURIComponent(nextQuery.trim())}`,
|
||||
);
|
||||
if (response.ok) {
|
||||
@@ -237,4 +238,4 @@ export function MemberSection({ calendarId }: { calendarId: string }) {
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
* Fehler werden inline angezeigt. Der eigene Account ist geschuetzt
|
||||
* (Server lehnt ab).
|
||||
*/
|
||||
import { appFetch } from '@/lib/client-fetch';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Button, Card, ErrorMessage, Badge } from '@/components/ui/ui';
|
||||
import { ModalShell } from '@/components/ui/ModalShell';
|
||||
@@ -32,7 +33,7 @@ export function UserManagement({ currentUserId }: { currentUserId: string }) {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await fetch('/api/users', { cache: 'no-store' });
|
||||
const response = await appFetch('/api/users', { cache: 'no-store' });
|
||||
if (!response.ok) {
|
||||
const data = (await response.json().catch(() => null)) as
|
||||
| { error?: string }
|
||||
@@ -65,7 +66,7 @@ export function UserManagement({ currentUserId }: { currentUserId: string }) {
|
||||
setPendingId(userId);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await fetch(`/api/users/${userId}`, {
|
||||
const response = await appFetch(`/api/users/${userId}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ role: nextRole }),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
/** Logout-Button: beendet die Session per API und laedt neu. */
|
||||
import { appFetch } from '@/lib/client-fetch';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Button } from '@/components/ui/ui';
|
||||
|
||||
@@ -8,7 +9,7 @@ export function LogoutButton() {
|
||||
const router = useRouter();
|
||||
|
||||
async function logout() {
|
||||
await fetch('/api/auth/logout', { method: 'POST' });
|
||||
await appFetch('/api/auth/logout', { method: 'POST' });
|
||||
router.push('/login');
|
||||
router.refresh();
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
* - Loeschen ausschliesslich durch Admins (Server prueft final)
|
||||
* - Legende: eigene vs. fremde Reservierungen
|
||||
*/
|
||||
import { appFetch } from '@/lib/client-fetch';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { ModalShell } from '@/components/ui/ModalShell';
|
||||
import { Button, ErrorMessage, Input, Label } from '@/components/ui/ui';
|
||||
@@ -189,7 +190,7 @@ export function CalendarModal({
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await fetch(
|
||||
const response = await appFetch(
|
||||
`/api/calendars/${calendarId}/reservations?from=${encodeURIComponent(window.from)}&to=${encodeURIComponent(window.to)}`,
|
||||
);
|
||||
if (!response.ok) {
|
||||
@@ -231,7 +232,7 @@ export function CalendarModal({
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/calendars/${calendarId}/reservations`, {
|
||||
const response = await appFetch(`/api/calendars/${calendarId}/reservations`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@@ -261,7 +262,7 @@ export function CalendarModal({
|
||||
setDeletingId(reservationId);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await fetch(`/api/reservations/${reservationId}`, {
|
||||
const response = await appFetch(`/api/reservations/${reservationId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (!response.ok) {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
* Der Zustand wird in localStorage persistiert, damit die Wahl
|
||||
* ueber Seitenwechsel hinweg erhalten bleibt.
|
||||
*/
|
||||
import { appFetch } from '@/lib/client-fetch';
|
||||
import { useEffect, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
@@ -115,7 +116,7 @@ export function Sidebar({
|
||||
async function logout() {
|
||||
setIsLoggingOut(true);
|
||||
try {
|
||||
await fetch('/api/auth/logout', { method: 'POST' });
|
||||
await appFetch('/api/auth/logout', { method: 'POST' });
|
||||
router.push('/login');
|
||||
router.refresh();
|
||||
} finally {
|
||||
@@ -140,7 +141,7 @@ export function Sidebar({
|
||||
Kalendartool
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
{process.env.NEXT_PUBLIC_MPM_AUTH_MODE !== 'true' && <button
|
||||
type="button"
|
||||
onClick={toggleCollapsed}
|
||||
aria-expanded={!isCollapsed}
|
||||
@@ -148,7 +149,7 @@ export function Sidebar({
|
||||
className="rounded-md p-2 text-slate-500 transition-colors hover:bg-slate-100 hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200"
|
||||
>
|
||||
{isCollapsed ? '»' : '«'}
|
||||
</button>
|
||||
</button>}
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 space-y-1 overflow-y-auto p-2">
|
||||
@@ -231,4 +232,4 @@ export function Sidebar({
|
||||
)}
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
* Wird aus der Sidebar ueber das Profil-Badge geoeffnet; nach dem
|
||||
* Speichern aktualisiert router.refresh() die Sidebar sofort.
|
||||
*/
|
||||
import { appFetch } from '@/lib/client-fetch';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Button, ErrorMessage, Input, Label, Badge } from '@/components/ui/ui';
|
||||
@@ -35,7 +36,7 @@ export function ProfileModal({ onClose }: { onClose: () => void }) {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await fetch('/api/me/profile', { cache: 'no-store' });
|
||||
const response = await appFetch('/api/me/profile', { cache: 'no-store' });
|
||||
if (!response.ok) {
|
||||
throw new Error('Profil konnte nicht geladen werden.');
|
||||
}
|
||||
@@ -66,7 +67,7 @@ export function ProfileModal({ onClose }: { onClose: () => void }) {
|
||||
const username = String(formData.get('username') ?? '').trim();
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/me/profile', {
|
||||
const response = await appFetch('/api/me/profile', {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username: username || null }),
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
* geprueft und der Benutzer bei Ueberschreitung informiert.
|
||||
* Der Server prueft Kollisionen; 409 wird verstaendlich angezeigt.
|
||||
*/
|
||||
import { appFetch } from '@/lib/client-fetch';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Button, Card, ErrorMessage, Input, Label } from '@/components/ui/ui';
|
||||
@@ -133,7 +134,7 @@ export function NewReservationButton({
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/calendars/${calendarId}/reservations`, {
|
||||
const response = await appFetch(`/api/calendars/${calendarId}/reservations`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
|
||||
Reference in New Issue
Block a user