lettergrid source

App.tsx

import { useState } from 'react';
import type { ReactNode } from 'react';
import { StatusBar } from 'react-native';
import { SafeAreaProvider } from 'react-native-safe-area-context';

import {
  Block,
  Button,
  Card,
  Column,
  Input,
  PlocksProvider,
  SafeArea,
  ScrollArea,
  Text,
  Title,
} from '@plocks/ui';

type Palette = {
  background: string;
  surface: string;
  ink: string;
  muted: string;
  accent: string;
  border: string;
};

function AppFrame({
  name,
  subtitle,
  palette,
  children,
  dark = false,
  showHeader = true,
}: {
  name: string;
  subtitle: string;
  palette: Palette;
  children: ReactNode;
  dark?: boolean;
  showHeader?: boolean;
}) {
  return (
    <SafeAreaProvider>
      <PlocksProvider theme={{ colorScheme: dark ? 'dark' : 'light' }}>
        <SafeArea gap={0} flex={1} bg={palette.background}>
          <StatusBar barStyle={dark ? 'light-content' : 'dark-content'} />
          <ScrollArea
            contentProps={{ w: '100%', maw: 720, alignSelf: 'center', p: 20, pb: 64, gap: 18 }}
          >
            {showHeader && (
              <Column gap="xs">
                <Text c={palette.accent} size={11} fw="bold" lts={2}>
                  PLOCKS EXAMPLE
                </Text>
                <Title order={1} c={palette.ink} size={34}>
                  {name}
                </Title>
                <Text c={palette.muted}>{subtitle}</Text>
              </Column>
            )}
            {children}
          </ScrollArea>
        </SafeArea>
      </PlocksProvider>
    </SafeAreaProvider>
  );
}

function Panel({
  children,
  palette,
  onPress,
}: {
  children: ReactNode;
  palette: Palette;
  onPress?: () => void;
}) {
  return (
    <Card
      onPress={onPress}
      bg={palette.surface}
      borderColor={palette.border}
      borderWidth={1}
      radius="lg"
      padding="lg"
    >
      <Column gap="sm">{children}</Column>
    </Card>
  );
}

function Action({
  title,
  palette,
  onPress,
  outline = false,
  disabled = false,
}: {
  title: string;
  palette: Palette;
  onPress: () => void;
  outline?: boolean;
  disabled?: boolean;
}) {
  return (
    <Button
      title={title}
      onPress={onPress}
      disabled={disabled}
      size="sm"
      variant={outline ? 'outline' : 'filled'}
      color={palette.accent}
      textColor={outline ? palette.accent : palette.background}
    />
  );
}

function Actions({ children }: { children: ReactNode }) {
  return (
    <Block direction="row" wrap="wrap" align="center" gap={8}>
      {children}
    </Block>
  );
}

const P: Palette = {
  background: '#F6F6F3',
  surface: '#FFFFFF',
  ink: '#202124',
  muted: '#6D7275',
  accent: '#538D4E',
  border: '#D9DCE0',
};
const WORDS = ['PLANT', 'LIGHT', 'STONE', 'BREAD', 'TRAIN', 'OCEAN', 'MUSIC'];
const pick = () => WORDS[Math.floor(Math.random() * WORDS.length)];
function marks(guess: string, answer: string) {
  const result = Array(5).fill('absent') as string[];
  const pool = answer.split('');
  for (let i = 0; i < 5; i++)
    if (guess[i] === answer[i]) {
      result[i] = 'correct';
      pool[i] = '';
    }
  for (let i = 0; i < 5; i++)
    if (result[i] !== 'correct') {
      const index = pool.indexOf(guess[i]);
      if (index >= 0) {
        result[i] = 'present';
        pool[index] = '';
      }
    }
  return result;
}
export default function App() {
  const [answer, setAnswer] = useState(pick);
  const [guess, setGuess] = useState('');
  const [guesses, setGuesses] = useState<string[]>([]);
  const [message, setMessage] = useState('Guess the five-letter word in six tries.');
  const won = guesses.includes(answer),
    ended = won || guesses.length === 6;
  function submit() {
    const word = guess.trim().toUpperCase();
    if (word.length !== 5 || !/^[A-Z]{5}$/.test(word)) {
      setMessage('Enter exactly five letters.');
      return;
    }
    setGuesses((old) => [...old, word]);
    setGuess('');
    setMessage(
      word === answer
        ? 'You got it!'
        : guesses.length === 5
          ? `The word was ${answer}.`
          : 'Keep trying.',
    );
  }
  function reset() {
    setAnswer(pick());
    setGuess('');
    setGuesses([]);
    setMessage('New puzzle ready.');
  }
  return (
    <AppFrame name="Lettergrid" subtitle="A small word puzzle with familiar color clues." palette={P}>
      <Panel palette={P}>
        <Text c={P.ink}>{message}</Text>
      </Panel>
      <Block gap={6} align="center">
        {Array.from({ length: 6 }, (_, row) => (
          <Block key={row} direction="row" gap={6}>
            {Array.from({ length: 5 }, (_, col) => {
              const letter =
                guesses[row]?.[col] ??
                (row === guesses.length && !ended ? guess.toUpperCase()[col] : '') ??
                '';
              const mark = guesses[row] ? marks(guesses[row], answer)[col] : '';
              const bg =
                mark === 'correct'
                  ? '#538D4E'
                  : mark === 'present'
                    ? '#B59F3B'
                    : mark
                      ? '#787C7E'
                      : P.surface;
              return (
                <Block
                  gap={0}
                  key={col}
                  w={51}
                  h={51}
                  justify="center"
                  align="center"
                  borderWidth={2}
                  borderColor={mark ? bg : P.border}
                  bg={bg}
                >
                  <Text c={mark ? '#FFF' : P.ink} fw="bold" size={24}>
                    {letter}
                  </Text>
                </Block>
              );
            })}
          </Block>
        ))}
      </Block>
      {!ended && (
        <Actions>
          <Block gap={0} flex={1} miw={190}>
            <Input
              label="Your guess"
              value={guess}
              maxLength={5}
              autoCapitalize="characters"
              onChangeText={setGuess}
            />
          </Block>
          <Action title="Guess" palette={P} onPress={submit} />
        </Actions>
      )}
      <Action title="New puzzle" palette={P} outline onPress={reset} />
    </AppFrame>
  );
}

index.ts

import { registerRootComponent } from 'expo';

import App from './App';

registerRootComponent(App);