chore: build
This commit is contained in:
@@ -1066,6 +1066,16 @@ export interface AppSettings {
|
|||||||
delivery_mode: DeliveryModeConfig;
|
delivery_mode: DeliveryModeConfig;
|
||||||
shop_name: string;
|
shop_name: string;
|
||||||
contact_telegram: string;
|
contact_telegram: string;
|
||||||
|
admin_color_primary: string;
|
||||||
|
admin_color_secondary: string;
|
||||||
|
admin_color_success: string;
|
||||||
|
admin_color_danger: string;
|
||||||
|
admin_color_warning: string;
|
||||||
|
client_color_primary: string;
|
||||||
|
client_color_secondary: string;
|
||||||
|
client_color_success: string;
|
||||||
|
client_color_danger: string;
|
||||||
|
client_color_warning: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getSettings = async (): Promise<{
|
export const getSettings = async (): Promise<{
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import React, { createContext, useContext, useState, useEffect, type ReactNode } from "react";
|
import React, { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from "react";
|
||||||
|
import { AppState } from "react-native";
|
||||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||||
import { darkColors, lightColors, type Colors } from "../theme/colors";
|
import { darkColors, lightColors, type Colors } from "../theme/colors";
|
||||||
|
import { getSettings } from "../api/api_admin";
|
||||||
|
|
||||||
type ThemeMode = "dark" | "light";
|
type ThemeMode = "dark" | "light";
|
||||||
|
|
||||||
@@ -12,16 +14,48 @@ interface ThemeContextType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const STORAGE_KEY = "@theme_mode";
|
const STORAGE_KEY = "@theme_mode";
|
||||||
|
const COLORS_CACHE_KEY = "@admin_colors_cache";
|
||||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||||
|
|
||||||
export function ThemeProvider({ children }: { children: ReactNode }) {
|
export function ThemeProvider({ children }: { children: ReactNode }) {
|
||||||
const [mode, setMode] = useState<ThemeMode>("dark");
|
const [mode, setMode] = useState<ThemeMode>("dark");
|
||||||
|
const [colorOverrides, setColorOverrides] = useState<Partial<Colors>>({});
|
||||||
|
|
||||||
|
const fetchAdminColors = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const result = await getSettings();
|
||||||
|
if (result.success && result.settings) {
|
||||||
|
const s = result.settings;
|
||||||
|
const overrides: Partial<Colors> = {};
|
||||||
|
if (s.admin_color_primary) overrides.accent = s.admin_color_primary;
|
||||||
|
if (s.admin_color_secondary) overrides.secondary = s.admin_color_secondary;
|
||||||
|
if (s.admin_color_success) overrides.success = s.admin_color_success;
|
||||||
|
if (s.admin_color_danger) overrides.danger = s.admin_color_danger;
|
||||||
|
if (s.admin_color_warning) overrides.warning = s.admin_color_warning;
|
||||||
|
setColorOverrides(overrides);
|
||||||
|
AsyncStorage.setItem(COLORS_CACHE_KEY, JSON.stringify(overrides)).catch(() => {});
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
AsyncStorage.getItem(STORAGE_KEY).then((val) => {
|
AsyncStorage.getItem(STORAGE_KEY).then((val) => {
|
||||||
if (val === "light" || val === "dark") setMode(val);
|
if (val === "light" || val === "dark") setMode(val);
|
||||||
});
|
});
|
||||||
}, []);
|
AsyncStorage.getItem(COLORS_CACHE_KEY).then((val) => {
|
||||||
|
if (val) {
|
||||||
|
try { setColorOverrides(JSON.parse(val)); } catch {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
fetchAdminColors();
|
||||||
|
}, [fetchAdminColors]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const sub = AppState.addEventListener("change", (state) => {
|
||||||
|
if (state === "active") fetchAdminColors();
|
||||||
|
});
|
||||||
|
return () => sub.remove();
|
||||||
|
}, [fetchAdminColors]);
|
||||||
|
|
||||||
const toggleTheme = () => {
|
const toggleTheme = () => {
|
||||||
const next = mode === "dark" ? "light" : "dark";
|
const next = mode === "dark" ? "light" : "dark";
|
||||||
@@ -29,8 +63,11 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
|
|||||||
AsyncStorage.setItem(STORAGE_KEY, next);
|
AsyncStorage.setItem(STORAGE_KEY, next);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const baseColors = mode === "dark" ? darkColors : lightColors;
|
||||||
|
const colors: Colors = { ...baseColors, ...colorOverrides };
|
||||||
|
|
||||||
const value: ThemeContextType = {
|
const value: ThemeContextType = {
|
||||||
colors: mode === "dark" ? darkColors : lightColors,
|
colors,
|
||||||
mode,
|
mode,
|
||||||
toggleTheme,
|
toggleTheme,
|
||||||
isDark: mode === "dark",
|
isDark: mode === "dark",
|
||||||
|
|||||||
@@ -1194,6 +1194,16 @@ export default function SettingsScreen() {
|
|||||||
shop_name: "Milieu-Nantais",
|
shop_name: "Milieu-Nantais",
|
||||||
contact_telegram: "",
|
contact_telegram: "",
|
||||||
points_reward: null,
|
points_reward: null,
|
||||||
|
admin_color_primary: "#7c3aed",
|
||||||
|
admin_color_secondary: "#22d3ee",
|
||||||
|
admin_color_success: "#4ade80",
|
||||||
|
admin_color_danger: "#ef4444",
|
||||||
|
admin_color_warning: "#f59e0b",
|
||||||
|
client_color_primary: "#7c3aed",
|
||||||
|
client_color_secondary: "#22d3ee",
|
||||||
|
client_color_success: "#4ade80",
|
||||||
|
client_color_danger: "#ef4444",
|
||||||
|
client_color_warning: "#f59e0b",
|
||||||
});
|
});
|
||||||
const [showApiKey, setShowApiKey] = useState(false);
|
const [showApiKey, setShowApiKey] = useState(false);
|
||||||
const [showIpnSecret, setShowIpnSecret] = useState(false);
|
const [showIpnSecret, setShowIpnSecret] = useState(false);
|
||||||
@@ -2182,6 +2192,66 @@ export default function SettingsScreen() {
|
|||||||
)}
|
)}
|
||||||
</AccordionSection>
|
</AccordionSection>
|
||||||
|
|
||||||
|
{/* ============================================ */}
|
||||||
|
{/* 🎨 COULEURS DE L'INTERFACE */}
|
||||||
|
{/* ============================================ */}
|
||||||
|
<AccordionSection title="Couleurs de l'interface" colors={colors} s={s}>
|
||||||
|
<Text style={[s.hint, { paddingTop: spacing.s, paddingHorizontal: spacing.l }]}>
|
||||||
|
Personnalisez les couleurs de l'espace admin et de l'app client / site web.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{(["admin", "client"] as const).map((scope) => (
|
||||||
|
<View key={scope}>
|
||||||
|
<Text style={[s.rowLabel, { paddingHorizontal: spacing.l, paddingTop: spacing.l, paddingBottom: spacing.xs }]}>
|
||||||
|
{scope === "admin" ? "Espace admin" : "App client & site web"}
|
||||||
|
</Text>
|
||||||
|
{([
|
||||||
|
{ label: "Principale", key: `${scope}_color_primary` as const },
|
||||||
|
{ label: "Secondaire", key: `${scope}_color_secondary` as const },
|
||||||
|
{ label: "Succès", key: `${scope}_color_success` as const },
|
||||||
|
{ label: "Danger", key: `${scope}_color_danger` as const },
|
||||||
|
{ label: "Avertissement", key: `${scope}_color_warning` as const },
|
||||||
|
] as { label: string; key: keyof typeof settings }[]).map(({ label, key }) => (
|
||||||
|
<View key={key as string} style={{ paddingHorizontal: spacing.l, paddingBottom: spacing.m }}>
|
||||||
|
<Text style={[s.rowDesc, { marginBottom: spacing.xs }]}>{label}</Text>
|
||||||
|
<View style={{ flexDirection: "row", alignItems: "center", gap: spacing.m }}>
|
||||||
|
<View style={{
|
||||||
|
width: 36, height: 36,
|
||||||
|
borderRadius: borderRadius.sm,
|
||||||
|
backgroundColor: (settings[key] as string) || "#7c3aed",
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.border,
|
||||||
|
}} />
|
||||||
|
<TextInput
|
||||||
|
style={[s.input, { flex: 1 }]}
|
||||||
|
value={settings[key] as string}
|
||||||
|
onChangeText={(v) => setSettings((p) => ({ ...p, [key]: v }))}
|
||||||
|
placeholder="#7c3aed"
|
||||||
|
placeholderTextColor={colors.textMuted}
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoCorrect={false}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View style={{ flexDirection: "row", gap: spacing.xs, marginTop: spacing.xs, flexWrap: "wrap" }}>
|
||||||
|
{["#7c3aed", "#2563eb", "#0891b2", "#059669", "#d97706", "#dc2626", "#db2777", "#f59e0b", "#4ade80", "#ef4444", "#22d3ee", "#0f172a"].map((color) => (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={color}
|
||||||
|
onPress={() => setSettings((p) => ({ ...p, [key]: color }))}
|
||||||
|
style={{
|
||||||
|
width: 26, height: 26, borderRadius: 13,
|
||||||
|
backgroundColor: color,
|
||||||
|
borderWidth: (settings[key] as string) === color ? 2.5 : 1,
|
||||||
|
borderColor: (settings[key] as string) === color ? colors.textPrimary : colors.border,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</AccordionSection>
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={s.saveButton}
|
style={s.saveButton}
|
||||||
onPress={handleSave}
|
onPress={handleSave}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export const darkColors = {
|
|||||||
accent: "#7c3aed",
|
accent: "#7c3aed",
|
||||||
accentDark: "#6d28d9",
|
accentDark: "#6d28d9",
|
||||||
accentLight: "#8b5cf6",
|
accentLight: "#8b5cf6",
|
||||||
|
secondary: "#22d3ee",
|
||||||
|
|
||||||
// Status
|
// Status
|
||||||
success: "#4ade80",
|
success: "#4ade80",
|
||||||
@@ -62,6 +63,7 @@ export const lightColors: Colors = {
|
|||||||
accent: "#7c3aed",
|
accent: "#7c3aed",
|
||||||
accentDark: "#6d28d9",
|
accentDark: "#6d28d9",
|
||||||
accentLight: "#8b5cf6",
|
accentLight: "#8b5cf6",
|
||||||
|
secondary: "#0891b2",
|
||||||
|
|
||||||
// Status
|
// Status
|
||||||
success: "#16a34a",
|
success: "#16a34a",
|
||||||
|
|||||||
@@ -290,7 +290,7 @@ function ConsultationHistorique() {
|
|||||||
<p
|
<p
|
||||||
className={`stat-value ${penaltyCount >= 3 ? "value-danger" : penaltyCount > 0 ? "value-warning" : ""}`}
|
className={`stat-value ${penaltyCount >= 3 ? "value-danger" : penaltyCount > 0 ? "value-warning" : ""}`}
|
||||||
>
|
>
|
||||||
{penaltyCount}
|
{penaltyCount} €
|
||||||
</p>
|
</p>
|
||||||
<p className="stat-label">Score amendes</p>
|
<p className="stat-label">Score amendes</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -778,6 +778,11 @@ export interface PublicSettings {
|
|||||||
telegram_notifications_enabled: boolean;
|
telegram_notifications_enabled: boolean;
|
||||||
two_fa_enabled: boolean;
|
two_fa_enabled: boolean;
|
||||||
contact_telegram: string;
|
contact_telegram: string;
|
||||||
|
client_color_primary: string;
|
||||||
|
client_color_secondary: string;
|
||||||
|
client_color_success: string;
|
||||||
|
client_color_danger: string;
|
||||||
|
client_color_warning: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getPublicSettings = async (): Promise<PublicSettings> => {
|
export const getPublicSettings = async (): Promise<PublicSettings> => {
|
||||||
@@ -794,6 +799,11 @@ export const getPublicSettings = async (): Promise<PublicSettings> => {
|
|||||||
telegram_notifications_enabled: false,
|
telegram_notifications_enabled: false,
|
||||||
two_fa_enabled: false,
|
two_fa_enabled: false,
|
||||||
contact_telegram: "",
|
contact_telegram: "",
|
||||||
|
client_color_primary: "#7c3aed",
|
||||||
|
client_color_secondary: "#22d3ee",
|
||||||
|
client_color_success: "#4ade80",
|
||||||
|
client_color_danger: "#ef4444",
|
||||||
|
client_color_warning: "#f59e0b",
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
const { data } = await apiClient.get(`${V1}/app-settings`);
|
const { data } = await apiClient.get(`${V1}/app-settings`);
|
||||||
@@ -816,6 +826,11 @@ export const getPublicSettings = async (): Promise<PublicSettings> => {
|
|||||||
data.telegram_notifications_enabled ?? false,
|
data.telegram_notifications_enabled ?? false,
|
||||||
two_fa_enabled: data.two_fa_enabled ?? false,
|
two_fa_enabled: data.two_fa_enabled ?? false,
|
||||||
contact_telegram: data.contact_telegram || "",
|
contact_telegram: data.contact_telegram || "",
|
||||||
|
client_color_primary: data.client_color_primary || "#7c3aed",
|
||||||
|
client_color_secondary: data.client_color_secondary || "#22d3ee",
|
||||||
|
client_color_success: data.client_color_success || "#4ade80",
|
||||||
|
client_color_danger: data.client_color_danger || "#ef4444",
|
||||||
|
client_color_warning: data.client_color_warning || "#f59e0b",
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
return defaults;
|
return defaults;
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import React, { createContext, useContext, useState, useEffect, type ReactNode } from "react";
|
import React, { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from "react";
|
||||||
|
import { AppState } from "react-native";
|
||||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||||
import { darkColors, lightColors, type Colors } from "../theme/colors";
|
import { darkColors, lightColors, type Colors } from "../theme/colors";
|
||||||
|
import { getPublicSettings } from "../api/api";
|
||||||
|
|
||||||
type ThemeMode = "dark" | "light";
|
type ThemeMode = "dark" | "light";
|
||||||
|
|
||||||
@@ -12,16 +14,45 @@ interface ThemeContextType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const STORAGE_KEY = "@theme_mode";
|
const STORAGE_KEY = "@theme_mode";
|
||||||
|
const COLORS_CACHE_KEY = "@client_colors_cache";
|
||||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||||
|
|
||||||
export function ThemeProvider({ children }: { children: ReactNode }) {
|
export function ThemeProvider({ children }: { children: ReactNode }) {
|
||||||
const [mode, setMode] = useState<ThemeMode>("dark");
|
const [mode, setMode] = useState<ThemeMode>("dark");
|
||||||
|
const [colorOverrides, setColorOverrides] = useState<Partial<Colors>>({});
|
||||||
|
|
||||||
|
const fetchClientColors = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const s = await getPublicSettings();
|
||||||
|
const overrides: Partial<Colors> = {};
|
||||||
|
if (s.client_color_primary) overrides.accent = s.client_color_primary;
|
||||||
|
if (s.client_color_secondary) overrides.secondary = s.client_color_secondary;
|
||||||
|
if (s.client_color_success) overrides.success = s.client_color_success;
|
||||||
|
if (s.client_color_danger) overrides.danger = s.client_color_danger;
|
||||||
|
if (s.client_color_warning) overrides.warning = s.client_color_warning;
|
||||||
|
setColorOverrides(overrides);
|
||||||
|
AsyncStorage.setItem(COLORS_CACHE_KEY, JSON.stringify(overrides)).catch(() => {});
|
||||||
|
} catch {}
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
AsyncStorage.getItem(STORAGE_KEY).then((val) => {
|
AsyncStorage.getItem(STORAGE_KEY).then((val) => {
|
||||||
if (val === "light" || val === "dark") setMode(val);
|
if (val === "light" || val === "dark") setMode(val);
|
||||||
});
|
});
|
||||||
}, []);
|
AsyncStorage.getItem(COLORS_CACHE_KEY).then((val) => {
|
||||||
|
if (val) {
|
||||||
|
try { setColorOverrides(JSON.parse(val)); } catch {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
fetchClientColors();
|
||||||
|
}, [fetchClientColors]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const sub = AppState.addEventListener("change", (state) => {
|
||||||
|
if (state === "active") fetchClientColors();
|
||||||
|
});
|
||||||
|
return () => sub.remove();
|
||||||
|
}, [fetchClientColors]);
|
||||||
|
|
||||||
const toggleTheme = () => {
|
const toggleTheme = () => {
|
||||||
const next = mode === "dark" ? "light" : "dark";
|
const next = mode === "dark" ? "light" : "dark";
|
||||||
@@ -29,8 +60,11 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
|
|||||||
AsyncStorage.setItem(STORAGE_KEY, next);
|
AsyncStorage.setItem(STORAGE_KEY, next);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const baseColors = mode === "dark" ? darkColors : lightColors;
|
||||||
|
const colors: Colors = { ...baseColors, ...colorOverrides };
|
||||||
|
|
||||||
const value: ThemeContextType = {
|
const value: ThemeContextType = {
|
||||||
colors: mode === "dark" ? darkColors : lightColors,
|
colors,
|
||||||
mode,
|
mode,
|
||||||
toggleTheme,
|
toggleTheme,
|
||||||
isDark: mode === "dark",
|
isDark: mode === "dark",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export const darkColors = {
|
|||||||
accent: "#7c3aed",
|
accent: "#7c3aed",
|
||||||
accentDark: "#6d28d9",
|
accentDark: "#6d28d9",
|
||||||
accentLight: "#8b5cf6",
|
accentLight: "#8b5cf6",
|
||||||
|
secondary: "#22d3ee",
|
||||||
|
|
||||||
// Status
|
// Status
|
||||||
success: "#4ade80",
|
success: "#4ade80",
|
||||||
@@ -62,6 +63,7 @@ export const lightColors: Colors = {
|
|||||||
accent: "#7c3aed",
|
accent: "#7c3aed",
|
||||||
accentDark: "#6d28d9",
|
accentDark: "#6d28d9",
|
||||||
accentLight: "#8b5cf6",
|
accentLight: "#8b5cf6",
|
||||||
|
secondary: "#0891b2",
|
||||||
|
|
||||||
// Status
|
// Status
|
||||||
success: "#16a34a",
|
success: "#16a34a",
|
||||||
|
|||||||
Reference in New Issue
Block a user