feat(mobile): veckoplan-skärm scaffold (månadsheader + veckostrip + dagsvy) + flik
CI / Typecheck, test & build (push) Successful in 1m34s
CI / Typecheck, test & build (push) Successful in 1m34s
This commit is contained in:
@@ -86,6 +86,13 @@ export default function TabsLayout() {
|
|||||||
tabBarIcon: ({ focused }) => <TabIcon glyph="📊" focused={focused} />,
|
tabBarIcon: ({ focused }) => <TabIcon glyph="📊" focused={focused} />,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<Tabs.Screen
|
||||||
|
name="plan"
|
||||||
|
options={{
|
||||||
|
title: t("tabs.plan"),
|
||||||
|
tabBarIcon: ({ focused }) => <TabIcon glyph="📅" focused={focused} />,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Tabs.Screen
|
<Tabs.Screen
|
||||||
name="home"
|
name="home"
|
||||||
options={{
|
options={{
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user