daymark source

App.tsx

import { useEffect, useState, type ReactNode } from 'react';
import { StatusBar, useWindowDimensions } from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { BarChart, LineChart } from '@plocks/charts';
import { Gradient, Input, Progress, Ring, ScrollArea, SafeArea, Block, Icon, PlocksProvider, Text, type BlockProps } from '@plocks/ui';

const C = {
  background: '#F5F8F8',
  surface: '#FFFFFF',
  ink: '#17353A',
  muted: '#72878A',
  teal: '#00A9A6',
  tealDark: '#087B83',
  tealPale: '#E5F7F5',
  line: '#E6EEEE',
  purple: '#7566DA',
  purplePale: '#F1EEFF',
  coral: '#E96972',
  coralPale: '#FFF0F1',
  blue: '#4296CE',
  bluePale: '#EAF5FC',
  orange: '#F5A557',
  white: '#FFFFFF',
};
const STORAGE = 'plocks-daymark-demo:v2';
const GOAL = 10000;
const WEEK = [
  { steps: 8120, zone: 31, calories: 2185, distance: 3.7, sleep: 438, resting: 63 },
  { steps: 5305, zone: 18, calories: 1970, distance: 2.4, sleep: 416, resting: 65 },
  { steps: 10420, zone: 42, calories: 2375, distance: 4.8, sleep: 486, resting: 61 },
  { steps: 7040, zone: 24, calories: 2108, distance: 3.2, sleep: 462, resting: 62 },
  { steps: 9340, zone: 35, calories: 2280, distance: 4.3, sleep: 453, resting: 62 },
  { steps: 6150, zone: 21, calories: 2015, distance: 2.8, sleep: 429, resting: 64 },
  { steps: 6420, zone: 24, calories: 2140, distance: 2.9, sleep: 462, resting: 62 },
];
const HEART = [62, 59, 57, 56, 57, 61, 69, 82, 76, 70, 74, 98, 112, 87, 78, 74, 91, 107, 83, 75, 71, 67, 64, 62];
const INITIAL_WEIGHTS = [171.2, 170.8, 170.5, 170.6, 170.1, 169.7, 169.8, 169.3, 169.1, 168.9, 168.6, 168.8, 168.4];
const SLEEP_STAGES = [
  { label: 'Deep', value: 19, color: '#4B4BA8' },
  { label: 'Light', value: 28, color: '#A89CF0' },
  { label: 'REM', value: 21, color: '#7566DA' },
  { label: 'Awake', value: 6, color: '#E5E5FA' },
  { label: 'Light', value: 19, color: '#A89CF0' },
  { label: 'REM', value: 7, color: '#7566DA' },
];

const minutesLabel = (minutes: number) => `${Math.floor(minutes / 60)}h ${String(minutes % 60).padStart(2, '0')}m`;
const shortDate = (date: Date) => date.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' });

function Surface({ children, ...props }: { children: ReactNode } & Omit<BlockProps, 'children'>) {
  return (
    <Block bg={C.surface} radius={24} borderWidth={1} borderColor={C.line} p={20} gap={0} shadow="xs" {...props}>
      {children}
    </Block>
  );
}

function SectionTitle({
  icon,
  color,
  background,
  title,
  action,
  onAction,
}: {
  icon: string;
  color: string;
  background: string;
  title: string;
  action?: string;
  onAction?: () => void;
}) {
  return (
    <Block direction="row" align="center" gap={11} mb={17}>
      <Block gap={0} w={37} h={37} radius={12} bg={background} align="center" justify="center">
        <Icon name={icon} color={color} size={21} />
      </Block>
      <Text c={C.ink} fw="bold" size={18} flex={1}>
        {title}
      </Text>
      {action && onAction && (
        <Block gap={0} onPress={onAction} accessibilityRole="button" py={7} pl={8}>
          <Text c={color} fw="bold" size={13}>
            {action}
          </Text>
        </Block>
      )}
    </Block>
  );
}

function TrendChart({
  values,
  color,
  label,
  height = 150,
}: {
  values: number[];
  color: string;
  label: string;
  height?: number;
}) {
  return (
    <LineChart
      h={height}
      series={[{ data: values.map((value, index) => ({ x: index, y: value })), color }]}
      lineThickness={3.5}
      fill
      fillColor={color}
      showPoints={false}
      xAxis={{ show: false }}
      yAxis={{ show: false }}
      grid={{ show: true, style: 'dashed' }}
      accessibilityLabel={label}
    />
  );
}

function MiniStat({
  icon,
  value,
  label,
  color,
  background,
}: {
  icon: string;
  value: string;
  label: string;
  color: string;
  background: string;
}) {
  return (
    <Surface flex={1} miw={102} p={15}>
      <Block gap={0} w={33} h={33} radius={11} bg={background} align="center" justify="center" mb={12}>
        <Icon name={icon} color={color} size={19} />
      </Block>
      <Text c={C.ink} size={22} fw="bold" numberOfLines={1}>
        {value}
      </Text>
      <Text c={C.muted} size={12} mt={3}>
        {label}
      </Text>
    </Surface>
  );
}

export default function App() {
  const { width } = useWindowDimensions();
  const wide = width >= 760;
  const [dayIndex, setDayIndex] = useState(6);
  const [stepsAdded, setStepsAdded] = useState(0);
  const [water, setWater] = useState(5);
  const [sleepLogged, setSleepLogged] = useState<number | null>(null);
  const [weights, setWeights] = useState(INITIAL_WEIGHTS);
  const [weightRange, setWeightRange] = useState<'4W' | '12W'>('4W');
  const [showWeightLog, setShowWeightLog] = useState(false);
  const [weightDraft, setWeightDraft] = useState('');
  const [showSleepLog, setShowSleepLog] = useState(false);
  const [sleepDraft, setSleepDraft] = useState('');
  const [ready, setReady] = useState(false);

  useEffect(() => {
    AsyncStorage.getItem(STORAGE)
      .then((raw) => {
        if (!raw) return;
        const saved = JSON.parse(raw) as {
          stepsAdded?: number;
          water?: number;
          sleepLogged?: number;
          weights?: number[];
        };
        if (Number.isFinite(saved.stepsAdded) && saved.stepsAdded! >= 0) setStepsAdded(saved.stepsAdded!);
        if (Number.isFinite(saved.water) && saved.water! >= 0) setWater(saved.water!);
        if (Number.isFinite(saved.sleepLogged) && saved.sleepLogged! >= 180 && saved.sleepLogged! <= 900)
          setSleepLogged(saved.sleepLogged!);
        if (Array.isArray(saved.weights)) {
          const valid = saved.weights.filter((value) => Number.isFinite(value) && value >= 60 && value <= 600);
          if (valid.length >= 2) setWeights(valid.slice(-30));
        }
      })
      .catch(() => {})
      .finally(() => setReady(true));
  }, []);
  useEffect(() => {
    if (ready)
      AsyncStorage.setItem(STORAGE, JSON.stringify({ stepsAdded, water, sleepLogged, weights })).catch(() => {});
  }, [ready, stepsAdded, water, sleepLogged, weights]);

  const dates = Array.from({ length: 7 }, (_, i) => {
    const date = new Date();
    date.setHours(12, 0, 0, 0);
    date.setDate(date.getDate() - (6 - i));
    return date;
  });
  const selectedDate = dates[dayIndex];
  const data = WEEK[dayIndex];
  const steps = data.steps + (dayIndex === 6 ? stepsAdded : 0);
  const sleep = dayIndex === 6 && sleepLogged !== null ? sleepLogged : data.sleep;
  const sleepScore = Math.min(98, Math.max(62, Math.round((sleep / 60) * 11.2)));
  const heartValues = HEART.map((value, index) => value + (6 - dayIndex) * (index % 3 === 0 ? 1 : -1));
  const chartWeights = weights.slice(weightRange === '4W' ? -5 : -13);
  const latestWeight = weights[weights.length - 1];
  const weightChange = latestWeight - chartWeights[0];
  const weightValid = Number.isFinite(Number(weightDraft)) && Number(weightDraft) >= 60 && Number(weightDraft) <= 600;
  const sleepHours = Number(sleepDraft);
  const sleepValid = Number.isFinite(sleepHours) && sleepHours >= 3 && sleepHours <= 15;

  const saveWeight = () => {
    if (!weightValid) return;
    setWeights((current) => [...current, Number(Number(weightDraft).toFixed(1))].slice(-30));
    setWeightDraft('');
    setShowWeightLog(false);
  };
  const saveSleep = () => {
    if (!sleepValid) return;
    setSleepLogged(Math.round(sleepHours * 60));
    setDayIndex(6);
    setSleepDraft('');
    setShowSleepLog(false);
  };

  return (
    <SafeAreaProvider>
      <PlocksProvider theme={{ colorScheme: 'light' }}>
        <SafeArea gap={0} flex={1} bg={C.background}>
          <StatusBar barStyle="dark-content" />
          <ScrollArea contentProps={{ w: '100%', maw: 880, alignSelf: 'center', px: 18, pt: 22, pb: 48, gap: 18 }}>
            <Block gap={0} direction="row" justify="space-between" align="center">
              <Block gap={0}>
                <Text c={C.tealDark} fw="bold" size={18} lts={-0.5}>
                  daymark
                </Text>
                <Text c={C.ink} fw="bold" size={31} mt={5}>
                  {dayIndex === 6 ? 'Today' : 'Daily overview'}
                </Text>
                <Text c={C.muted} size={14} mt={2}>
                  {shortDate(selectedDate)}
                </Text>
              </Block>
              <Block gap={0} w={44} h={44} radius={22} bg={C.tealPale} align="center" justify="center">
                <Text c={C.tealDark} fw="bold" size={15}>
                  JS
                </Text>
              </Block>
            </Block>

            <Block direction="row" gap={8} justify="space-between">
              {dates.map((date, i) => (
                <Block
                  gap={0}
                  key={i}
                  onPress={() => setDayIndex(i)}
                  accessibilityRole="button"
                  accessibilityLabel={shortDate(date)}
                  accessibilityState={{ selected: dayIndex === i }}
                  flex={1}
                  miw={0}
                  py={11}
                  radius={15}
                  align="center"
                  bg={dayIndex === i ? C.tealDark : C.surface}
                  borderWidth={1}
                  borderColor={dayIndex === i ? C.tealDark : C.line}
                >
                  <Text c={dayIndex === i ? C.white : C.muted} size={11} fw="semibold">
                    {date.toLocaleDateString('en-US', { weekday: 'short' }).toUpperCase()}
                  </Text>
                  <Text c={dayIndex === i ? C.white : C.ink} size={17} fw="bold" mt={3}>
                    {date.getDate()}
                  </Text>
                </Block>
              ))}
            </Block>

            <Gradient
              colors={[C.tealDark, '#0DB9B2']}
              start={{ x: 0, y: 0 }}
              end={{ x: 1, y: 1 }}
              radius={28}
              p={21}
              overflow="hidden"
            >
              <Block gap={0} direction="row" align="center">
                <Ring
                  value={steps}
                  max={GOAL}
                  size={142}
                  thickness={12}
                  trackColor="#FFFFFF55"
                  progressColor={C.white}
                  labelColor={C.white}
                  subLabel="of goal"
                  subLabelColor="#D9FBF7"
                  accessibilityLabel="Step goal progress"
                />
                <Block gap={0} flex={1} pl={13}>
                  <Text c="#D5F7F4" size={12} fw="bold" lts={1.2}>
                    STEPS
                  </Text>
                  <Text c={C.white} size={37} fw="bold" numberOfLines={1} mt={3}>
                    {steps.toLocaleString()}
                  </Text>
                  <Text c="#D8F7F5" size={14}>
                    of {GOAL.toLocaleString()} daily goal
                  </Text>
                </Block>
              </Block>
              <Block gap={0} h={1} bg="#FFFFFF44" my={15} />
              <Block gap={0} direction="row" align="center" justify="space-between">
                <Text c={C.white} size={13} fw="semibold">
                  {steps >= GOAL
                    ? 'Goal reached — nice work!'
                    : `${Math.max(0, GOAL - steps).toLocaleString()} steps to go`}
                </Text>
                {dayIndex === 6 && (
                  <Block
                    gap={0}
                    onPress={() => setStepsAdded((value) => value + 500)}
                    accessibilityRole="button"
                    px={12}
                    py={8}
                    bg="#FFFFFF27"
                    radius={11}
                  >
                    <Text c={C.white} size={12} fw="bold">
                      + 500 steps
                    </Text>
                  </Block>
                )}
              </Block>
            </Gradient>

            <Block direction="row" gap={10}>
              <MiniStat
                icon="bolt"
                value={`${data.zone} min`}
                label="Zone minutes"
                color={C.orange}
                background="#FFF4E8"
              />
              <MiniStat
                icon="chart-line"
                value={data.distance.toFixed(1)}
                label="Miles"
                color={C.blue}
                background={C.bluePale}
              />
              <MiniStat
                icon="heart"
                value={data.calories.toLocaleString()}
                label="Calories"
                color={C.coral}
                background={C.coralPale}
              />
            </Block>

            <Block direction={wide ? 'row' : 'column'} gap={16} align="stretch">
              <Surface flex={1}>
                <SectionTitle
                  icon="moon"
                  color={C.purple}
                  background={C.purplePale}
                  title="Sleep"
                  action={dayIndex === 6 ? '+ Log' : undefined}
                  onAction={() => {
                    setSleepDraft((sleep / 60).toFixed(1));
                    setShowSleepLog(true);
                  }}
                />
                <Block gap={0} direction="row" align="flex-end" justify="space-between">
                  <Block gap={0}>
                    <Text c={C.ink} size={31} fw="bold">
                      {minutesLabel(sleep)}
                    </Text>
                    <Text c={C.muted} size={13}>
                      Time asleep
                    </Text>
                  </Block>
                  <Block gap={0} align="flex-end">
                    <Text c={C.purple} size={24} fw="bold">
                      {sleepScore}
                    </Text>
                    <Text c={C.muted} size={12}>
                      Sleep score · Good
                    </Text>
                  </Block>
                </Block>
                <Text c={C.muted} size={12} fw="bold" mt={23} mb={9}>
                  SLEEP STAGES
                </Text>
                <Progress.Root size={18} radius={10} aria-label="Sleep stages">
                  {SLEEP_STAGES.map((stage, index) => (
                    <Progress.Section
                      key={index}
                      value={stage.value}
                      color={stage.color}
                      tooltip={`${stage.label} · ${stage.value}%`}
                    />
                  ))}
                </Progress.Root>
                <Block gap={0} direction="row" justify="space-between" mt={11}>
                  {[
                    ['Deep', '#4B4BA8'],
                    ['Light', '#A89CF0'],
                    ['REM', '#7566DA'],
                    ['Awake', '#D9D9F3'],
                  ].map(([label, color]) => (
                    <Block key={label} direction="row" align="center" gap={4}>
                      <Block gap={0} w={7} h={7} radius={4} bg={color} />
                      <Text c={C.muted} size={11}>
                        {label}
                      </Text>
                    </Block>
                  ))}
                </Block>
                <Block gap={0} h={1} bg={C.line} my={19} />
                <Text c={C.ink} size={13} fw="semibold">
                  Past 7 nights
                </Text>
                <BarChart
                  h={120}
                  mt={10}
                  data={WEEK.map((item, index) => ({
                    id: index,
                    category: dates[index].toLocaleDateString('en-US', { weekday: 'short' }),
                    value: index === 6 && sleepLogged !== null ? sleepLogged : item.sleep,
                    color: dayIndex === index ? C.purple : '#DCD7F7',
                  }))}
                  barSpacing={0.65}
                  barBorderRadius={7}
                  xAxis={{ show: true, labelColor: C.muted, labelFontSize: 10 }}
                  yAxis={{ show: false }}
                  grid={{ show: false }}
                  legend={{ show: false }}
                  tooltip={{ show: true, formatter: (point) => `${minutesLabel(point.value)} sleep` }}
                  onDataPointPress={(point) => setDayIndex(Number(point.id))}
                  accessibilityLabel="Sleep duration over the past seven nights"
                />
                {showSleepLog && (
                  <Block gap={0} mt={19} pt={16} borderTopWidth={1} borderTopColor={C.line}>
                    <Text c={C.ink} fw="semibold" size={13}>
                      Hours slept last night
                    </Text>
                    <Block direction="row" gap={8} mt={9}>
                      <Input
                        variant="outline"
                        value={sleepDraft}
                        onChangeText={setSleepDraft}
                        keyboardType="decimal-pad"
                        placeholder="7.7"
                        accessibilityLabel="Hours slept"
                        mb={0}
                        flex={1}
                        miw={0}
                        h={44}
                        radius={11}
                        inputColor={C.ink}
                      />
                      <Block
                        gap={0}
                        onPress={saveSleep}
                        disabled={!sleepValid}
                        accessibilityRole="button"
                        px={17}
                        justify="center"
                        radius={11}
                        bg={sleepValid ? C.purple : C.line}
                      >
                        <Text c={C.white} fw="bold" size={13}>
                          Save
                        </Text>
                      </Block>
                      <Block
                        gap={0}
                        onPress={() => setShowSleepLog(false)}
                        accessibilityRole="button"
                        justify="center"
                        px={6}
                      >
                        <Text c={C.muted} size={13}>
                          Cancel
                        </Text>
                      </Block>
                    </Block>
                  </Block>
                )}
              </Surface>

              <Surface flex={1}>
                <SectionTitle icon="heart" color={C.coral} background={C.coralPale} title="Heart rate" />
                <Block gap={0} direction="row" align="flex-end" justify="space-between">
                  <Block gap={0}>
                    <Text c={C.ink} size={31} fw="bold">
                      {data.resting}{' '}
                      <Text c={C.muted} size={16}>
                        bpm
                      </Text>
                    </Text>
                    <Text c={C.muted} size={13}>
                      Resting heart rate
                    </Text>
                  </Block>
                  <Block gap={0} px={11} py={6} radius={10} bg={C.coralPale}>
                    <Text c={C.coral} size={12} fw="bold">
                      In your range
                    </Text>
                  </Block>
                </Block>
                <Block gap={0} mt={20}>
                  <TrendChart values={heartValues} color={C.coral} label="Heart rate trend" height={180} />
                </Block>
                <Block gap={0} direction="row" justify="space-between" mt={3}>
                  <Text c={C.muted} size={11}>
                    12 AM
                  </Text>
                  <Text c={C.muted} size={11}>
                    6 AM
                  </Text>
                  <Text c={C.muted} size={11}>
                    12 PM
                  </Text>
                  <Text c={C.muted} size={11}>
                    6 PM
                  </Text>
                  <Text c={C.muted} size={11}>
                    NOW
                  </Text>
                </Block>
                <Block gap={0} h={1} bg={C.line} my={21} />
                <Block gap={0} direction="row" justify="space-between">
                  <Block gap={0}>
                    <Text c={C.muted} size={12}>
                      Lowest today
                    </Text>
                    <Text c={C.ink} fw="bold" size={18}>
                      {Math.min(...heartValues)} bpm
                    </Text>
                  </Block>
                  <Block gap={0} align="flex-end">
                    <Text c={C.muted} size={12}>
                      Peak today
                    </Text>
                    <Text c={C.ink} fw="bold" size={18}>
                      {Math.max(...heartValues)} bpm
                    </Text>
                  </Block>
                </Block>
              </Surface>
            </Block>

            <Surface>
              <SectionTitle
                icon="chart-line"
                color={C.blue}
                background={C.bluePale}
                title="Weight"
                action="+ Log weight"
                onAction={() => {
                  setWeightDraft(latestWeight.toFixed(1));
                  setShowWeightLog(true);
                }}
              />
              <Block gap={0} direction="row" justify="space-between" align="flex-end">
                <Block gap={0}>
                  <Text c={C.ink} size={31} fw="bold">
                    {latestWeight.toFixed(1)}{' '}
                    <Text c={C.muted} size={16}>
                      lb
                    </Text>
                  </Text>
                  <Text c={weightChange <= 0 ? C.tealDark : C.coral} size={13} fw="semibold">
                    {weightChange > 0 ? '+' : ''}
                    {weightChange.toFixed(1)} lb over {weightRange === '4W' ? '4 weeks' : '12 weeks'}
                  </Text>
                </Block>
                <Block gap={0} direction="row" radius={12} bg={C.background} p={3}>
                  {(['4W', '12W'] as const).map((range) => (
                    <Block
                      gap={0}
                      key={range}
                      onPress={() => setWeightRange(range)}
                      accessibilityRole="button"
                      accessibilityState={{ selected: weightRange === range }}
                      px={11}
                      py={7}
                      radius={9}
                      bg={weightRange === range ? C.white : 'transparent'}
                    >
                      <Text c={weightRange === range ? C.ink : C.muted} fw="bold" size={12}>
                        {range}
                      </Text>
                    </Block>
                  ))}
                </Block>
              </Block>
              <Block gap={0} mt={13}>
                <TrendChart values={chartWeights} color={C.blue} label="Weight trend" height={180} />
              </Block>
              <Block gap={0} direction="row" justify="space-between" mt={2}>
                <Text c={C.muted} size={11}>
                  {weightRange === '4W' ? '4 weeks ago' : '12 weeks ago'}
                </Text>
                <Text c={C.muted} size={11}>
                  Latest
                </Text>
              </Block>
              {showWeightLog && (
                <Block gap={0} mt={19} pt={16} borderTopWidth={1} borderTopColor={C.line}>
                  <Text c={C.ink} fw="semibold" size={13}>
                    Log a new weight (lb)
                  </Text>
                  <Block direction="row" gap={8} mt={9}>
                    <Input
                      variant="outline"
                      value={weightDraft}
                      onChangeText={setWeightDraft}
                      keyboardType="decimal-pad"
                      placeholder="168.4"
                      accessibilityLabel="Weight in pounds"
                      mb={0}
                      flex={1}
                      miw={0}
                      h={44}
                      radius={11}
                      inputColor={C.ink}
                    />
                    <Block
                      gap={0}
                      onPress={saveWeight}
                      disabled={!weightValid}
                      accessibilityRole="button"
                      px={17}
                      justify="center"
                      radius={11}
                      bg={weightValid ? C.blue : C.line}
                    >
                      <Text c={C.white} fw="bold" size={13}>
                        Save
                      </Text>
                    </Block>
                    <Block
                      gap={0}
                      onPress={() => setShowWeightLog(false)}
                      accessibilityRole="button"
                      justify="center"
                      px={6}
                    >
                      <Text c={C.muted} size={13}>
                        Cancel
                      </Text>
                    </Block>
                  </Block>
                </Block>
              )}
            </Surface>

            <Surface direction="row" align="center" gap={15}>
              <Block gap={0} w={46} h={46} radius={15} bg={C.bluePale} align="center" justify="center">
                <Text c={C.blue} size={25}>
                  💧
                </Text>
              </Block>
              <Block gap={0} flex={1}>
                <Text c={C.ink} size={17} fw="bold">
                  Water
                </Text>
                <Text c={C.muted} size={13}>
                  {water} of 8 cups today
                </Text>
              </Block>
              <Block
                gap={0}
                onPress={() => setWater((value) => Math.min(value + 1, 20))}
                accessibilityRole="button"
                accessibilityLabel="Log a cup of water"
                w={37}
                h={37}
                radius={12}
                bg={C.bluePale}
                align="center"
                justify="center"
              >
                <Icon name="plus" color={C.blue} size={21} />
              </Block>
            </Surface>
            <Text c={C.muted} size={11} ta="center">
              Illustrative health data for this demo. Logged values stay on this device; no wearable is connected.
            </Text>
          </ScrollArea>
        </SafeArea>
      </PlocksProvider>
    </SafeAreaProvider>
  );
}

index.ts

import { registerRootComponent } from 'expo';

import App from './App';

registerRootComponent(App);