ci: trigga på master + formatfix inför Gitea Actions
CI / Typecheck, test & build (push) Failing after 2s
CI / Typecheck, test & build (push) Failing after 2s
This commit is contained in:
@@ -40,7 +40,10 @@ interface BudgetSummary {
|
||||
export default function HomeScreen() {
|
||||
const inventory = useQuery({
|
||||
queryKey: ["inventory"],
|
||||
queryFn: () => api<{ items: InventoryItem[]; trustStatus?: "up_to_date" | "needs_check" | "uncertain" }>("/v1/inventory?limit=100"),
|
||||
queryFn: () =>
|
||||
api<{ items: InventoryItem[]; trustStatus?: "up_to_date" | "needs_check" | "uncertain" }>(
|
||||
"/v1/inventory?limit=100",
|
||||
),
|
||||
});
|
||||
const expiring = useQuery({
|
||||
queryKey: ["inventory-expiring"],
|
||||
|
||||
@@ -54,42 +54,42 @@ export default function RootLayout() {
|
||||
>
|
||||
<AnalyticsProvider>
|
||||
<StatusBar style="dark" />
|
||||
<Stack
|
||||
screenOptions={{
|
||||
headerStyle: { backgroundColor: colors.background },
|
||||
headerTintColor: colors.text,
|
||||
headerShadowVisible: false,
|
||||
contentStyle: { backgroundColor: colors.background },
|
||||
}}
|
||||
>
|
||||
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="(auth)/login" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="(auth)/register" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="(auth)/forgot-password" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="(auth)/reset-password" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="(auth)/verify-email" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="onboarding" options={{ headerShown: false, gestureEnabled: false }} />
|
||||
<Stack.Screen name="recipe/[id]" options={{ title: "" }} />
|
||||
<Stack.Screen
|
||||
name="cooking/[id]"
|
||||
options={{ title: "", presentation: "fullScreenModal" }}
|
||||
/>
|
||||
<Stack.Screen name="scan-review/[jobId]" options={{ title: t("scan.review.title") }} />
|
||||
<Stack.Screen name="meal-review/[jobId]" options={{ title: t("scan.meal.title") }} />
|
||||
<Stack.Screen name="shopping" options={{ title: t("shopping.title") }} />
|
||||
<Stack.Screen name="meal-boxes" options={{ title: t("mealbox.title") }} />
|
||||
<Stack.Screen name="household" options={{ title: t("home.household") }} />
|
||||
<Stack.Screen name="memory" options={{ title: t("memory.title") }} />
|
||||
<Stack.Screen name="profile" options={{ title: t("profile.title") }} />
|
||||
<Stack.Screen
|
||||
name="paywall"
|
||||
options={{ title: t("paywall.title"), presentation: "modal" }}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="log-meal"
|
||||
options={{ title: t("myday.logMeal"), presentation: "modal" }}
|
||||
/>
|
||||
<Stack.Screen name="barcode" options={{ title: "Streckkod" }} />
|
||||
<Stack
|
||||
screenOptions={{
|
||||
headerStyle: { backgroundColor: colors.background },
|
||||
headerTintColor: colors.text,
|
||||
headerShadowVisible: false,
|
||||
contentStyle: { backgroundColor: colors.background },
|
||||
}}
|
||||
>
|
||||
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="(auth)/login" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="(auth)/register" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="(auth)/forgot-password" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="(auth)/reset-password" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="(auth)/verify-email" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="onboarding" options={{ headerShown: false, gestureEnabled: false }} />
|
||||
<Stack.Screen name="recipe/[id]" options={{ title: "" }} />
|
||||
<Stack.Screen
|
||||
name="cooking/[id]"
|
||||
options={{ title: "", presentation: "fullScreenModal" }}
|
||||
/>
|
||||
<Stack.Screen name="scan-review/[jobId]" options={{ title: t("scan.review.title") }} />
|
||||
<Stack.Screen name="meal-review/[jobId]" options={{ title: t("scan.meal.title") }} />
|
||||
<Stack.Screen name="shopping" options={{ title: t("shopping.title") }} />
|
||||
<Stack.Screen name="meal-boxes" options={{ title: t("mealbox.title") }} />
|
||||
<Stack.Screen name="household" options={{ title: t("home.household") }} />
|
||||
<Stack.Screen name="memory" options={{ title: t("memory.title") }} />
|
||||
<Stack.Screen name="profile" options={{ title: t("profile.title") }} />
|
||||
<Stack.Screen
|
||||
name="paywall"
|
||||
options={{ title: t("paywall.title"), presentation: "modal" }}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="log-meal"
|
||||
options={{ title: t("myday.logMeal"), presentation: "modal" }}
|
||||
/>
|
||||
<Stack.Screen name="barcode" options={{ title: "Streckkod" }} />
|
||||
</Stack>
|
||||
</AnalyticsProvider>
|
||||
</PersistQueryClientProvider>
|
||||
|
||||
@@ -60,10 +60,10 @@ export default function CookingScreen() {
|
||||
|
||||
const cook = useMutation({
|
||||
mutationFn: (body: unknown) =>
|
||||
api<{ sessionId: string; mealBoxMutations?: Array<{ mealBoxId: string; deltaPortions: number; frozen: boolean }> }>(
|
||||
`/v1/recipes/${id}/cook`,
|
||||
{ method: "POST", body },
|
||||
),
|
||||
api<{
|
||||
sessionId: string;
|
||||
mealBoxMutations?: Array<{ mealBoxId: string; deltaPortions: number; frozen: boolean }>;
|
||||
}>(`/v1/recipes/${id}/cook`, { method: "POST", body }),
|
||||
onSuccess: async (data) => {
|
||||
await queryClient.invalidateQueries({ queryKey: ["inventory"] });
|
||||
await queryClient.invalidateQueries({ queryKey: ["day"] });
|
||||
@@ -125,7 +125,9 @@ export default function CookingScreen() {
|
||||
<Card>
|
||||
<Heading>✅ {t("cooked.title")}</Heading>
|
||||
<Body>{recipe.titleSv}</Body>
|
||||
<Small>{t("cooked.portionsCooked")}: {portionsCooked}</Small>
|
||||
<Small>
|
||||
{t("cooked.portionsCooked")}: {portionsCooked}
|
||||
</Small>
|
||||
</Card>
|
||||
<Small>{t("mealbox.guidanceNote")}</Small>
|
||||
<Button
|
||||
@@ -230,17 +232,21 @@ export default function CookingScreen() {
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Button label={t("common.skip")} variant="ghost" onPress={() =>
|
||||
cook.mutate({
|
||||
portionsCooked,
|
||||
mealBoxPortions,
|
||||
actualPortionsEaten: defaultEaten,
|
||||
leftoverEstimatePortions: defaultLeftovers,
|
||||
mealBoxFrozen: false,
|
||||
deductInventory: true,
|
||||
mealType: "dinner",
|
||||
})
|
||||
} />
|
||||
<Button
|
||||
label={t("common.skip")}
|
||||
variant="ghost"
|
||||
onPress={() =>
|
||||
cook.mutate({
|
||||
portionsCooked,
|
||||
mealBoxPortions,
|
||||
actualPortionsEaten: defaultEaten,
|
||||
leftoverEstimatePortions: defaultLeftovers,
|
||||
mealBoxFrozen: false,
|
||||
deductInventory: true,
|
||||
mealType: "dinner",
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Button label={t("common.back")} variant="ghost" onPress={() => setFinishing(false)} />
|
||||
</Screen>
|
||||
);
|
||||
|
||||
@@ -75,8 +75,7 @@ export default function MealBoxesScreen() {
|
||||
{boxes.length === 0 && <EmptyState text={t("mealbox.empty")} />}
|
||||
{boxes.map((box) => {
|
||||
const urgent = box.recommendedUseBy <= today;
|
||||
const canUndo =
|
||||
!!box.cookingSessionId && new Date(box.createdAt).getTime() > cutoff;
|
||||
const canUndo = !!box.cookingSessionId && new Date(box.createdAt).getTime() > cutoff;
|
||||
return (
|
||||
<Card key={box.id}>
|
||||
<Row style={{ justifyContent: "space-between" }}>
|
||||
@@ -109,18 +108,14 @@ export default function MealBoxesScreen() {
|
||||
onPress={() => {
|
||||
const sessionId = box.cookingSessionId;
|
||||
if (!sessionId) return;
|
||||
Alert.alert(
|
||||
t("mealbox.undoConfirmTitle"),
|
||||
t("mealbox.undoConfirmBody"),
|
||||
[
|
||||
{ text: t("common.cancel"), style: "cancel" },
|
||||
{
|
||||
text: t("common.undo"),
|
||||
style: "destructive",
|
||||
onPress: () => undo.mutate(sessionId),
|
||||
},
|
||||
],
|
||||
);
|
||||
Alert.alert(t("mealbox.undoConfirmTitle"), t("mealbox.undoConfirmBody"), [
|
||||
{ text: t("common.cancel"), style: "cancel" },
|
||||
{
|
||||
text: t("common.undo"),
|
||||
style: "destructive",
|
||||
onPress: () => undo.mutate(sessionId),
|
||||
},
|
||||
]);
|
||||
}}
|
||||
/>
|
||||
</Row>
|
||||
|
||||
@@ -5,8 +5,19 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { api } from "@/lib/api";
|
||||
import { t } from "@/lib/i18n";
|
||||
import {
|
||||
Body, Button, Card, ErrorView, Heading, Input,
|
||||
LoadingView, Row, Screen, Small, Spacer, Tag, Title,
|
||||
Body,
|
||||
Button,
|
||||
Card,
|
||||
ErrorView,
|
||||
Heading,
|
||||
Input,
|
||||
LoadingView,
|
||||
Row,
|
||||
Screen,
|
||||
Small,
|
||||
Spacer,
|
||||
Tag,
|
||||
Title,
|
||||
} from "@/components/ui";
|
||||
import { spacing } from "@/lib/theme";
|
||||
|
||||
@@ -17,9 +28,16 @@ import { spacing } from "@/lib/theme";
|
||||
*/
|
||||
const MEAL_TYPES = ["breakfast", "lunch", "dinner", "snack"] as const;
|
||||
|
||||
interface MealComponent { name: string; estimatedGrams: number | null; confidence: number }
|
||||
interface MealComponent {
|
||||
name: string;
|
||||
estimatedGrams: number | null;
|
||||
confidence: number;
|
||||
}
|
||||
interface ScanJob {
|
||||
id: string; status: string; scanType: string; error: string | null;
|
||||
id: string;
|
||||
status: string;
|
||||
scanType: string;
|
||||
error: string | null;
|
||||
result: {
|
||||
kcalRange?: { min: number; max: number; mostLikely: number } | null;
|
||||
components?: MealComponent[];
|
||||
@@ -97,7 +115,9 @@ export default function MealReviewScreen() {
|
||||
{kcal ? (
|
||||
<>
|
||||
<Title>≈ {Math.round(kcal.mostLikely)} kcal</Title>
|
||||
<Small>{Math.round(kcal.min)}–{Math.round(kcal.max)} kcal</Small>
|
||||
<Small>
|
||||
{Math.round(kcal.min)}–{Math.round(kcal.max)} kcal
|
||||
</Small>
|
||||
</>
|
||||
) : (
|
||||
<Body>{t("scan.meal.noEstimate")}</Body>
|
||||
|
||||
@@ -5,11 +5,7 @@ import { api } from "@/lib/api";
|
||||
import { useAuth } from "@/lib/auth";
|
||||
import { useAnalytics } from "@/lib/analytics";
|
||||
import { t } from "@/lib/i18n";
|
||||
import {
|
||||
onboardingStarted,
|
||||
onboardingStepCompleted,
|
||||
onboardingSkipped,
|
||||
} from "@app/analytics";
|
||||
import { onboardingStarted, onboardingStepCompleted, onboardingSkipped } from "@app/analytics";
|
||||
import {
|
||||
Body,
|
||||
Button,
|
||||
@@ -113,7 +109,11 @@ export default function OnboardingScreen() {
|
||||
precisionMode: mode,
|
||||
},
|
||||
});
|
||||
track(onboardingStepCompleted({ properties: { step: "a", goals, primaryGoal, precisionMode: mode } }));
|
||||
track(
|
||||
onboardingStepCompleted({
|
||||
properties: { step: "a", goals, primaryGoal, precisionMode: mode },
|
||||
}),
|
||||
);
|
||||
setOnboardingStep(res.step);
|
||||
setLayer("b");
|
||||
// Let user into the app – Step B will be shown contextually later
|
||||
|
||||
@@ -53,7 +53,11 @@ export default function PaywallScreen() {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
track(paywallViewed({ properties: { plan: entitlements.data?.plan, status: entitlements.data?.status } }));
|
||||
track(
|
||||
paywallViewed({
|
||||
properties: { plan: entitlements.data?.plan, status: entitlements.data?.status },
|
||||
}),
|
||||
);
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const trialDaysLeft =
|
||||
|
||||
@@ -47,11 +47,16 @@ export default function ReconciliationScreen() {
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ["reconciliation-candidates"],
|
||||
queryFn: () => api<{ candidates: Candidate[] }>("/v1/reconciliations/start", { method: "POST", body: {} }),
|
||||
queryFn: () =>
|
||||
api<{ candidates: Candidate[] }>("/v1/reconciliations/start", { method: "POST", body: {} }),
|
||||
});
|
||||
|
||||
const resolveMutation = useMutation({
|
||||
mutationFn: (input: { itemId: string; action: Candidate["suggestedAction"]; quantity?: number }) =>
|
||||
mutationFn: (input: {
|
||||
itemId: string;
|
||||
action: Candidate["suggestedAction"];
|
||||
quantity?: number;
|
||||
}) =>
|
||||
api<{ itemId: string; action: string; quantity: number; verifiedByUser: boolean }>(
|
||||
`/v1/reconciliations/items/${input.itemId}/resolve`,
|
||||
{ method: "POST", body: { action: input.action, quantity: input.quantity } },
|
||||
@@ -100,9 +105,7 @@ export default function ReconciliationScreen() {
|
||||
<Card>
|
||||
<Row style={{ justifyContent: "space-between" }}>
|
||||
<Heading>{candidate!.displayName}</Heading>
|
||||
<Body>
|
||||
{formatQuantity(candidate!.quantity, candidate!.unit)}
|
||||
</Body>
|
||||
<Body>{formatQuantity(candidate!.quantity, candidate!.unit)}</Body>
|
||||
</Row>
|
||||
<Small>{candidate!.locationName}</Small>
|
||||
<Spacer size={spacing.xs} />
|
||||
|
||||
@@ -39,7 +39,8 @@ export default function ScanDiffReviewScreen() {
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ["scan-diff", jobId],
|
||||
queryFn: () => api<{ rows: DiffRow[] }>(`/v1/scans/${jobId}/diff`, { method: "POST", body: {} }),
|
||||
queryFn: () =>
|
||||
api<{ rows: DiffRow[] }>(`/v1/scans/${jobId}/diff`, { method: "POST", body: {} }),
|
||||
});
|
||||
|
||||
const apply = useMutation({
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Linking,
|
||||
Pressable,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { Linking, Pressable, StyleSheet, Text, View } from "react-native";
|
||||
import { Body, Button, Screen, Small, Title } from "@/components/ui";
|
||||
import type { TextStyle } from "react-native";
|
||||
import { acceptConsent } from "@/lib/api";
|
||||
@@ -55,10 +49,7 @@ export function ConsentScreen({ onAccepted }: ConsentScreenProps) {
|
||||
</View>
|
||||
|
||||
<View style={{ gap: spacing.md, paddingBottom: spacing.lg }}>
|
||||
<Pressable
|
||||
onPress={() => setChecked((v) => !v)}
|
||||
style={styles.checkboxRow}
|
||||
>
|
||||
<Pressable onPress={() => setChecked((v) => !v)} style={styles.checkboxRow}>
|
||||
<View style={[styles.box, checked && styles.boxChecked]}>
|
||||
{checked && <Text style={styles.checkmark}>✓</Text>}
|
||||
</View>
|
||||
|
||||
@@ -23,7 +23,13 @@ export function useFirstScanCoach() {
|
||||
return { visible, showIfNeeded, dismiss };
|
||||
}
|
||||
|
||||
export function FirstScanCoach({ visible, onDismiss }: { visible: boolean; onDismiss: () => void }) {
|
||||
export function FirstScanCoach({
|
||||
visible,
|
||||
onDismiss,
|
||||
}: {
|
||||
visible: boolean;
|
||||
onDismiss: () => void;
|
||||
}) {
|
||||
return (
|
||||
<Modal visible={visible} transparent animationType="fade">
|
||||
<View
|
||||
|
||||
@@ -2,12 +2,7 @@ import { createContext, useContext, useEffect, useMemo, useRef, useState } from
|
||||
import { Platform } from "react-native";
|
||||
import Constants from "expo-constants";
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import {
|
||||
type AnalyticsEvent,
|
||||
type Tracker,
|
||||
createHttpSend,
|
||||
createTracker,
|
||||
} from "@app/analytics";
|
||||
import { type AnalyticsEvent, type Tracker, createHttpSend, createTracker } from "@app/analytics";
|
||||
import { API_BASE } from "./api";
|
||||
import { useAuth } from "./auth";
|
||||
import { BRAND } from "./brand";
|
||||
|
||||
@@ -4,16 +4,9 @@
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
"include": ["**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user