chahinebrini 2f5d0382f0 feat(profile,devices): real DB wiring + Devices-Settings migration
Profile (rebreak-native-ui):
- New hook hooks/useProfileData.ts (143 LOC, 4 hooks):
  useSocialStats, useApprovedDomains, useCooldownHistory, useSosInsights
- app/profile/index.tsx: alle DUMMY_* constants entfernt → live data via hooks
- PATCH /api/profile/me/demographics nun wired in onChange (war TODO-only)
- DELETE /api/profile/me/demographics für revoke-consent
- POST /api/profile/me/diga-banner-dismiss

Devices (rebreak-native-ui):
- New app/devices.tsx push-page: slot-counter, progress-bar, device-list mit
  trash-button (gesperrt für isCurrent)
- New lib/deviceId.ts: persistent device-ID via expo-application
  (getIosIdForVendorAsync / getAndroidId) mit AsyncStorage-UUID-fallback
- New stores/devices.ts: Zustand store (loadDevices, removeDevice, ensureRegistered)
- lib/api.ts: x-device-id + x-platform headers bei jedem Backend-Call
  (skipDeviceHeader option für Bootstrap-register)
- app/settings.tsx: Geräte-Row aktiv (push to /devices) statt soon-flagged
- locales: 14 neue settings.devices_* keys DE+EN

Backend-Status: alle Devices-Endpoints existieren (GET /api/devices, POST /register,
DELETE /:id). Pending: GET /api/profile/me/demographics für reload-state-fetch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 20:47:30 +02:00

144 lines
3.7 KiB
TypeScript

import { useCallback, useEffect, useState } from 'react';
import { apiFetch } from '../lib/api';
import type { CooldownEntry } from '../components/profile/StreakSection';
import type { ApprovedDomain } from '../components/profile/ApprovedDomainsList';
export type SocialStats = {
postsCount: number;
followersCount: number;
};
export type ApprovedDomainsData = {
count: number;
list: ApprovedDomain[];
};
export type CooldownHistoryData = {
items: CooldownEntry[];
nextCursor: string | null;
};
export type SosInsightsData = {
last30Days: { sessions: number; overcome: number; overcomeRate: number };
helpedBy: { breathing: number; game: number; talk: number; other: number };
topEmotion: string | null;
};
type BackendCooldownEntry = {
id: string;
startedAt: string;
cooldownEndsAt: string;
durationMinutes: number;
status: 'active' | 'resolved' | 'cancelled';
resolvedAt: string | null;
cancelledAt: string | null;
reason: string | null;
};
function formatDuration(minutes: number): string {
if (minutes < 60) return `${minutes}min`;
const h = Math.round(minutes / 60);
return `${h}h`;
}
function formatStartedAt(isoString: string): string {
const d = new Date(isoString);
const day = String(d.getDate()).padStart(2, '0');
const month = String(d.getMonth() + 1).padStart(2, '0');
return `${day}.${month}.`;
}
function mapCooldownEntry(raw: BackendCooldownEntry): CooldownEntry {
return {
id: raw.id,
startedAt: formatStartedAt(raw.startedAt),
durationLabel: formatDuration(raw.durationMinutes),
status: raw.status,
reason: raw.reason,
};
}
function useFetchOnce<T>(
url: string,
): { data: T | null; loading: boolean; error: boolean; reload: () => void } {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const [version, setVersion] = useState(0);
useEffect(() => {
if (!url) {
setLoading(false);
return;
}
let cancelled = false;
setLoading(true);
setError(false);
apiFetch<T>(url)
.then((res) => {
if (cancelled) return;
setData(res);
})
.catch(() => {
if (!cancelled) setError(true);
})
.finally(() => {
if (!cancelled) setLoading(false);
});
return () => {
cancelled = true;
};
}, [url, version]);
const reload = useCallback(() => setVersion((v) => v + 1), []);
return { data, loading, error, reload };
}
export function useSocialStats(userId: string | undefined) {
const url = userId ? `/api/social/profile/${userId}` : '';
const { data, loading, error, reload } = useFetchOnce<{
postsCount: number;
followersCount: number;
}>(url);
return {
stats: data
? ({ postsCount: data.postsCount, followersCount: data.followersCount } as SocialStats)
: null,
loading,
error,
reload,
};
}
export function useApprovedDomains() {
const { data, loading, error, reload } = useFetchOnce<ApprovedDomainsData>(
'/api/profile/me/approved-domains',
);
return { domains: data, loading, error, reload };
}
export function useCooldownHistory() {
const { data, loading, error, reload } = useFetchOnce<{
items: BackendCooldownEntry[];
nextCursor: string | null;
}>('/api/profile/me/cooldown-history?limit=20');
const mapped: CooldownHistoryData | null = data
? {
items: data.items.map(mapCooldownEntry),
nextCursor: data.nextCursor,
}
: null;
return { cooldownHistory: mapped, loading, error, reload };
}
export function useSosInsights() {
const { data, loading, error, reload } = useFetchOnce<SosInsightsData>(
'/api/profile/me/sos-insights',
);
return { sosInsights: data, loading, error, reload };
}