feat(mobile): veckoplan-skärm scaffold (månadsheader + veckostrip + dagsvy) + flik
CI / Typecheck, test & build (push) Successful in 1m34s

This commit is contained in:
Sven (AAMOS AI)
2026-08-14 01:47:33 +07:00
parent 435af14bc4
commit e6c5d3bb08
2 changed files with 140 additions and 0 deletions
+7
View File
@@ -86,6 +86,13 @@ export default function TabsLayout() {
tabBarIcon: ({ focused }) => <TabIcon glyph="📊" focused={focused} />,
}}
/>
<Tabs.Screen
name="plan"
options={{
title: t("tabs.plan"),
tabBarIcon: ({ focused }) => <TabIcon glyph="📅" focused={focused} />,
}}
/>
<Tabs.Screen
name="home"
options={{
+133
View File
@@ -0,0 +1,133 @@
import { useMemo, useState } from "react";
import { Pressable, Text, View } from "react-native";
import { getLanguageTag, t } from "@/lib/i18n";
import { colors, radius, spacing } from "@/lib/theme";
import { EmptyState, Screen, Small, Title } from "@/components/ui";
/** Veckoplanerare (spec §25): dagsvy + veckostrip. Data kopplas in i nästa steg. */
/** Lokalt datum YYYY-MM-DD (undviker UTC-skift från toISOString). */
function toKey(d: Date): string {
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
return `${y}-${m}-${day}`;
}
function addDays(d: Date, n: number): Date {
const copy = new Date(d);
copy.setDate(copy.getDate() + n);
return copy;
}
/** Måndag som veckostart. */
function startOfWeekMonday(d: Date): Date {
const copy = new Date(d);
const dow = (copy.getDay() + 6) % 7; // 0 = måndag
copy.setDate(copy.getDate() - dow);
copy.setHours(0, 0, 0, 0);
return copy;
}
function cap(s: string): string {
return s.length ? s[0]!.toUpperCase() + s.slice(1) : s;
}
export default function PlanScreen() {
const locale = getLanguageTag();
const [selectedKey, setSelectedKey] = useState<string>(() => toKey(new Date()));
const todayKey = toKey(new Date());
const selected = useMemo(() => new Date(`${selectedKey}T00:00:00`), [selectedKey]);
const weekDays = useMemo(() => {
const monday = startOfWeekMonday(selected);
return Array.from({ length: 7 }, (_, i) => addDays(monday, i));
}, [selected]);
const monthLabel = cap(
new Intl.DateTimeFormat(locale, { month: "long", year: "numeric" }).format(selected),
);
const dayHeader = cap(
new Intl.DateTimeFormat(locale, { weekday: "long", day: "numeric", month: "long" }).format(
selected,
),
);
const weekdayShort = new Intl.DateTimeFormat(locale, { weekday: "short" });
return (
<Screen>
<View style={{ flexDirection: "row", alignItems: "center", justifyContent: "space-between" }}>
<Pressable onPress={() => setSelectedKey(toKey(addDays(selected, -7)))} hitSlop={8}>
<Text style={{ fontSize: 22, color: colors.textMuted, paddingHorizontal: spacing.sm }}>
</Text>
</Pressable>
<Title>{monthLabel}</Title>
<Pressable onPress={() => setSelectedKey(toKey(addDays(selected, 7)))} hitSlop={8}>
<Text style={{ fontSize: 22, color: colors.textMuted, paddingHorizontal: spacing.sm }}>
</Text>
</Pressable>
</View>
<Pressable
onPress={() => setSelectedKey(todayKey)}
style={{ alignSelf: "flex-start", paddingVertical: spacing.xs }}
>
<Text style={{ color: colors.primary, fontSize: 13, fontWeight: "600" }}>
{t("plan.today")}
</Text>
</Pressable>
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
marginVertical: spacing.sm,
}}
>
{weekDays.map((d) => {
const key = toKey(d);
const isSelected = key === selectedKey;
const isToday = key === todayKey;
return (
<Pressable
key={key}
onPress={() => setSelectedKey(key)}
accessibilityRole="button"
accessibilityState={{ selected: isSelected }}
style={{
flex: 1,
alignItems: "center",
paddingVertical: spacing.xs,
marginHorizontal: 2,
borderRadius: radius.md,
backgroundColor: isSelected
? colors.primary
: isToday
? colors.primarySoft
: "transparent",
}}
>
<Text style={{ fontSize: 12, color: isSelected ? colors.surface : colors.textMuted }}>
{cap(weekdayShort.format(d))}
</Text>
<Text
style={{
fontSize: 16,
fontWeight: isSelected || isToday ? "700" : "500",
color: isSelected ? colors.surface : colors.text,
}}
>
{d.getDate()}
</Text>
</Pressable>
);
})}
</View>
<Small>{dayHeader}</Small>
<EmptyState text={t("plan.emptyDay")} />
</Screen>
);
}