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} />,
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="plan"
|
||||
options={{
|
||||
title: t("tabs.plan"),
|
||||
tabBarIcon: ({ focused }) => <TabIcon glyph="📅" focused={focused} />,
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="home"
|
||||
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