Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/components/auth/login-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useState, useRef } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { enableDemoMode } from "@/lib/demo";

interface LoginFormProps {
onSendOtp: (phone: string) => Promise<{ request_id: string }>;
Expand Down Expand Up @@ -132,6 +133,19 @@ export function LoginForm({ onSendOtp, onVerifyOtp }: LoginFormProps) {
</Button>
</form>
)}

<div className="border-t pt-4 text-center">
<button
type="button"
className="text-sm text-muted-foreground underline-offset-4 hover:text-foreground hover:underline"
onClick={() => {
enableDemoMode();
window.location.reload();
}}
>
Demo ohne Login →
</button>
</div>
</div>
);
}
16 changes: 16 additions & 0 deletions src/components/layout/app-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useNavigate } from "react-router";
import { Plus } from "lucide-react";
import { Header } from "./header";
import { ROUTES } from "@/lib/constants";
import { disableDemoMode, isDemoMode } from "@/lib/demo";

interface AppShellProps {
children: React.ReactNode;
Expand All @@ -10,9 +11,24 @@ interface AppShellProps {

export function AppShell({ children, showFab = false }: AppShellProps) {
const navigate = useNavigate();
const demo = isDemoMode();

return (
<div className="flex min-h-screen flex-col">
{demo && (
<div className="flex items-center justify-between gap-3 bg-primary px-4 py-2 text-xs text-primary-foreground">
<span>Demo-Modus — Aenderungen werden nicht gespeichert</span>
<button
className="font-semibold underline-offset-2 hover:underline"
onClick={() => {
disableDemoMode();
window.location.reload();
}}
>
Beenden
</button>
</div>
)}
<Header />
<main className="flex-1 px-4 py-4">{children}</main>
{showFab && (
Expand Down
43 changes: 34 additions & 9 deletions src/hooks/use-auth.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { useCallback, useEffect, useState } from "react";
import type { Session, User } from "@supabase/supabase-js";
import { supabase } from "@/lib/supabase";
import {
DEMO_PROFILE,
DEMO_SESSION,
DEMO_USER,
disableDemoMode,
isDemoMode,
} from "@/lib/demo";
import type { Profile } from "@/types";

interface AuthState {
Expand All @@ -12,13 +19,25 @@ interface AuthState {
}

export function useAuth() {
const [state, setState] = useState<AuthState>({
session: null,
user: null,
profile: null,
loading: true,
isNewUser: false,
});
const demo = isDemoMode();

const [state, setState] = useState<AuthState>(() =>
demo
? {
session: DEMO_SESSION,
user: DEMO_USER,
profile: DEMO_PROFILE,
loading: false,
isNewUser: false,
}
: {
session: null,
user: null,
profile: null,
loading: true,
isNewUser: false,
},
);

const fetchProfile = useCallback(async (userId: string) => {
const { data } = await supabase
Expand All @@ -30,6 +49,7 @@ export function useAuth() {
}, []);

useEffect(() => {
if (demo) return;
supabase.auth.getSession().then(async ({ data: { session } }) => {
if (session?.user) {
const profile = await fetchProfile(session.user.id);
Expand Down Expand Up @@ -69,7 +89,7 @@ export function useAuth() {
});

return () => subscription.unsubscribe();
}, [fetchProfile]);
}, [fetchProfile, demo]);

const sendOtp = useCallback(async (phone: string) => {
const { data, error } = await supabase.functions.invoke("send-otp", {
Expand Down Expand Up @@ -117,8 +137,13 @@ export function useAuth() {
);

const signOut = useCallback(async () => {
if (demo) {
disableDemoMode();
window.location.reload();
return;
}
await supabase.auth.signOut();
}, []);
}, [demo]);

return {
...state,
Expand Down
119 changes: 99 additions & 20 deletions src/hooks/use-listings.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { supabase } from "@/lib/supabase";
import { DEMO_LISTINGS, isDemoMode } from "@/lib/demo";
import type { Listing, ListingInsert, ListingUpdate, ListingType } from "@/types";

interface UseListingsOptions {
Expand All @@ -8,13 +9,26 @@ interface UseListingsOptions {

export function useListings(options: UseListingsOptions = {}) {
const { typeFilter = "alle" } = options;
const [listings, setListings] = useState<Listing[]>([]);
const [loading, setLoading] = useState(true);
const demo = isDemoMode();

const filterDemo = (all: Listing[]) =>
typeFilter === "alle" ? all : all.filter((l) => l.type === typeFilter);

const [listings, setListings] = useState<Listing[]>(() =>
demo ? filterDemo(DEMO_LISTINGS) : [],
);
const [loading, setLoading] = useState(!demo);
const [error, setError] = useState<string | null>(null);
const [newCount, setNewCount] = useState(0);
const pendingRef = useRef<Listing[]>([]);

const fetchListings = useCallback(async () => {
if (demo) {
setListings(filterDemo(DEMO_LISTINGS));
setLoading(false);
return;
}

setLoading(true);
setError(null);

Expand All @@ -38,13 +52,15 @@ export function useListings(options: UseListingsOptions = {}) {
setLoading(false);
setNewCount(0);
pendingRef.current = [];
}, [typeFilter]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [typeFilter, demo]);

useEffect(() => {
fetchListings();
}, [fetchListings]);

useEffect(() => {
if (demo) return;
const channel = supabase
.channel("listings-realtime")
.on(
Expand Down Expand Up @@ -83,7 +99,7 @@ export function useListings(options: UseListingsOptions = {}) {
return () => {
supabase.removeChannel(channel);
};
}, []);
}, [demo]);

const showPending = useCallback(() => {
setListings((prev) => {
Expand All @@ -101,6 +117,37 @@ export function useListings(options: UseListingsOptions = {}) {

const createListing = useCallback(
async (listing: Omit<ListingInsert, "user_id">, userId: string) => {
if (demo) {
const now = new Date().toISOString();
const mock: Listing = {
id: crypto.randomUUID(),
user_id: userId,
type: listing.type,
origin_label: listing.origin_label,
origin_city: listing.origin_city,
origin_lat: listing.origin_lat,
origin_lng: listing.origin_lng,
destination_label: listing.destination_label,
destination_city: listing.destination_city,
destination_lat: listing.destination_lat,
destination_lng: listing.destination_lng,
departure_at: listing.departure_at,
seats: listing.seats,
notes: listing.notes ?? null,
created_at: now,
updated_at: now,
};
DEMO_LISTINGS.push(mock);
setListings((prev) =>
[...prev, mock].sort(
(a, b) =>
new Date(a.departure_at).getTime() -
new Date(b.departure_at).getTime(),
),
);
return mock;
}

const { data, error } = await supabase
.from("listings")
.insert({ ...listing, user_id: userId })
Expand All @@ -117,11 +164,26 @@ export function useListings(options: UseListingsOptions = {}) {
);
return data;
},
[],
[demo],
);

const updateListing = useCallback(
async (id: string, updates: ListingUpdate) => {
if (demo) {
const idx = DEMO_LISTINGS.findIndex((l) => l.id === id);
if (idx === -1) throw new Error("Eintrag nicht gefunden");
const updated: Listing = {
...DEMO_LISTINGS[idx]!,
...updates,
updated_at: new Date().toISOString(),
};
DEMO_LISTINGS[idx] = updated;
setListings((prev) =>
prev.map((l) => (l.id === id ? updated : l)),
);
return updated;
}

const { data, error } = await supabase
.from("listings")
.update(updates)
Expand All @@ -132,25 +194,42 @@ export function useListings(options: UseListingsOptions = {}) {
if (error) throw error;
return data;
},
[],
[demo],
);

const deleteListing = useCallback(async (id: string) => {
const { error } = await supabase.from("listings").delete().eq("id", id);
if (error) throw error;
setListings((prev) => prev.filter((l) => l.id !== id));
}, []);
const deleteListing = useCallback(
async (id: string) => {
if (demo) {
const idx = DEMO_LISTINGS.findIndex((l) => l.id === id);
if (idx !== -1) DEMO_LISTINGS.splice(idx, 1);
setListings((prev) => prev.filter((l) => l.id !== id));
return;
}
const { error } = await supabase.from("listings").delete().eq("id", id);
if (error) throw error;
setListings((prev) => prev.filter((l) => l.id !== id));
},
[demo],
);

const getListing = useCallback(async (id: string) => {
const { data, error } = await supabase
.from("listings")
.select("*")
.eq("id", id)
.single();
const getListing = useCallback(
async (id: string) => {
if (demo) {
const found = DEMO_LISTINGS.find((l) => l.id === id);
if (!found) throw new Error("Eintrag nicht gefunden");
return found;
}
const { data, error } = await supabase
.from("listings")
.select("*")
.eq("id", id)
.single();

if (error) throw error;
return data;
}, []);
if (error) throw error;
return data;
},
[demo],
);

const groupedByCity = listings.reduce<Record<string, Listing[]>>(
(acc, listing) => {
Expand Down
Loading
Loading