view react_games/src/Todo.tsx @ 279:b3b547563ec7

Add Google connector service and agent wiki Implement the C/Seobeo Google Drive and Gmail connector with encrypted OAuth storage, Zenbu authentication, browser testing, AI tool discovery, chunked HTTP decoding, and Bazel coverage. Consolidate repository guidance into progressive wiki documentation and enforce arena-first allocation for new first-party C code. Co-authored-by: Copilot <[email protected]> Copilot-Session: 84c338fd-0939-4bb3-b7f3-1062eb213e5d
author MrJuneJune <me@mrjunejune.com>
date Mon, 17 Aug 2026 22:22:36 -0700
parents fb9bcd3145cb
children
line wrap: on
line source

import ReactDOM from 'react-dom/client';
import { ActionDispatch, CSSProperties, Dispatch, memo, SetStateAction, startTransition, useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react';

// Thank you chatGPT for colors lmao....
const colors = {
  bg: '#0b1220',
  surface: '#0f172a',
  card: '#111827',
  border: '#1f2937',
  text: '#e5e7eb',
  muted: '#9ca3af',
  accent: '#4f46e5',
  accent2: '#10b981',
  danger: '#ef4444',
};

const shadow = '0 10px 20px rgba(0,0,0,.25)';

type styleComponents = 
  | "page"
  | "card"
  | "header"
  | "title"
  | "form"
  | "input"
  | "btnPrimary"
  | "btnSuccess"
  | "btnDanger"
  | "btnSm"
  | "filterBar"
  | "filterBtn"
  | "filterActive"
  | "list"
  | "todo"
  | "todoBusy"
  | "todoText"
  | "todoCompleted"
  | "badge"
  | "badgeDot"
  | "actions"
  | "btn"
  | "page"
  | "page"
  | "page"
  | "page"
  | "page";

const styles: Record<styleComponents, CSSProperties> = {
  page: {
    minHeight: '100vh',
    display: 'grid',
    placeItems: 'center',
    padding: 32,
    background: colors.bg,
    color: colors.text,
    fontFamily:
      "system-ui, -apple-system, Segoe UI, Roboto, 'Helvetica Neue', Arial, 'Apple Color Emoji','Segoe UI Emoji'",
  },
  card: {
    minWidth: 400,
    maxWidth: '92vw',
    background: colors.surface,
    border: `1px solid ${colors.border}`,
    borderRadius: 16,
    boxShadow: shadow,
    padding: '24px 24px 20px',
  },
  header: {
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'space-between',
    gap: 12,
    marginBottom: 16,
  },
  title: { margin: 0, fontSize: 24, letterSpacing: 0.2 },

  form: { display: 'flex', gap: 10, marginBottom: 14 },
  input: {
    flex: 1,
    height: 40,
    padding: '0 12px',
    border: `1px solid ${colors.border}`,
    borderRadius: 10,
    background: colors.card,
    color: colors.text,
    outline: 'none',
  },

  btn: {
    appearance: 'none',
    border: `1px solid ${colors.border}`,
    background: colors.card,
    color: colors.text,
    padding: '10px 14px',
    borderRadius: 10,
    cursor: 'pointer',
    fontSize: 14,
    userSelect: 'none',
    transition: 'transform .05s ease, background .15s, border-color .15s',
  },
  btnPrimary: { background: colors.accent, borderColor: colors.accent, color: '#fff' },
  btnSuccess: { background: colors.accent2, borderColor: colors.accent2, color: '#fff' },
  btnDanger:  { background: colors.danger,  borderColor: colors.danger,  color: '#fff' },
  btnSm: { padding: '8px 10px', fontSize: 13, borderRadius: 8 },

  filterBar: { display: 'flex', gap: 8, marginTop: 8, marginBottom: 18 },
  filterBtn: {
    appearance: 'none',
    border: `1px solid ${colors.border}`,
    background: colors.card,
    color: colors.text,
    padding: '8px 12px',
    fontSize: 13,
    borderRadius: 9999,
    cursor: 'pointer',
  },
  filterActive: {
    background: '#1a2140',            // subtle “active” fill
    borderColor: '#2a3a8a',
  },

  list: { display: 'grid', gap: 10 },

  todo: {
    display: 'grid',
    gridTemplateColumns: '1fr auto auto',
    gap: 10,
    alignItems: 'center',
    background: colors.card,
    border: `1px solid ${colors.border}`,
    borderRadius: 12,
    padding: '12px 12px',
  },
  todoBusy: { opacity: 0.75 },

  todoText: { display: 'flex', alignItems: 'center', gap: 8, fontSize: 16 },
  todoCompleted: { textDecoration: 'line-through', color: colors.muted },

  badge: {
    display: 'inline-flex',
    alignItems: 'center',
    gap: 6,
    padding: '2px 8px',
    fontSize: 12,
    borderRadius: 9999,
    border: `1px solid ${colors.border}`,
  },
  badgeDot: {
    width: 8,
    height: 8,
    borderRadius: 9999,
    display: 'inline-block',
  },
  actions: { display: 'flex', gap: 8 },
};

enum TodoStatus {
  IN_PROGRESS,
  COMPLETED,
  DELETED,
}

interface Todo {
  id: string;
  value: string;
  status: TodoStatus;
  isOptimistic: boolean;
}

interface TodoComponentProp {
  todo: Todo;
  handleOnClickButton: (todoId: string, status: TodoStatus, type: TodoActionEnum) => void;
}

const TodoComponent = memo(({ todo, handleOnClickButton }: TodoComponentProp) => {

  const statusLabel =
    todo.status === TodoStatus.IN_PROGRESS ? 'In progress' :
    todo.status === TodoStatus.COMPLETED   ? 'Completed'   : 'Deleted';

  const badgeDotColor =
    todo.status === TodoStatus.IN_PROGRESS ? '#f59e0b' :
    todo.status === TodoStatus.COMPLETED   ? '#10b981' :
                                             '#6b7280';

  return (
    <div style={{ ...styles.todo, ...(todo.isOptimistic ? styles.todoBusy : null) }} aria-busy={todo.isOptimistic}>
      <div
        style={{
          ...styles.todoText,
          ...(todo.status === TodoStatus.COMPLETED ? styles.todoCompleted : null),
        }}
      >
        <span style={styles.badge}>
          <span style={{ ...styles.badgeDot, background: badgeDotColor }} />
          {statusLabel}
        </span>
        <span style={ todo.isOptimistic ?{ fontSize: 12, color: colors.muted } : {}}>{todo.value}</span>
      </div>

      <div style={styles.actions}>
        {todo.status !== TodoStatus.COMPLETED && (
          <button style={{ ...styles.btn, ...styles.btnSm, ...styles.btnSuccess }} 
            onClick={() => handleOnClickButton(todo.id, TodoStatus.COMPLETED, TodoActionEnum.COMPLETED)}>
            Complete
          </button>
        )}
        {todo.status !== TodoStatus.DELETED && (
          <button style={{ ...styles.btn, ...styles.btnSm, ...styles.btnDanger }}
            onClick={() => handleOnClickButton(todo.id, TodoStatus.DELETED, TodoActionEnum.REMOVED)}>
            Delete
          </button>
        )}
      </div>
    </div>
  );
});

const todoTableCss: CSSProperties = { display: 'grid', gridTemplateColumns: '1fr', gap: 10, width: '100%' };

enum TodoActionEnum {
  ADD,
  COMPLETED,
  REMOVED,
  CONFRIM_FROM_BE,
  REVERT_FROM_BE,
  HYDRATE,
}

type TodoAction =
  | { type: TodoActionEnum.ADD; todo: Todo }
  | { type: TodoActionEnum.COMPLETED; todoId: string }
  | { type: TodoActionEnum.REMOVED; todoId: string }
  | { type: TodoActionEnum.CONFRIM_FROM_BE; todoId: string }
  | { type: TodoActionEnum.REVERT_FROM_BE; todoId: string }
  | { type: TodoActionEnum.HYDRATE; todos: Todo[]  };

const todoReducer = (state: Todo[], action: TodoAction): Todo[] => {
  switch (action.type) {
    case TodoActionEnum.ADD:
      return [...state, action.todo];
    case TodoActionEnum.COMPLETED:
      return state.map((t) => (t.id === action.todoId ? { ...t, status: TodoStatus.COMPLETED } : t));
    case TodoActionEnum.REMOVED:
      return state.map((t) => (t.id === action.todoId ? { ...t, status: TodoStatus.DELETED } : t));
    case TodoActionEnum.CONFRIM_FROM_BE:
      return state.map((t) => (t.id === action.todoId ? { ...t, isOptimistic: false } : t));
    case TodoActionEnum.REVERT_FROM_BE:
      return state.filter((t) => t.id !== action.todoId);
    case TodoActionEnum.HYDRATE:
      return action.todos.map((todo) => ({
          ...todo,
          isOptimistic: false,
        }));
    default:
      return state;
  }
};

type StatusBuckets = Record<TodoStatus, Todo[]>;

interface TodoPageComponentProp {
  userKeyword: string;
}

const TodoPageComponent = ({userKeyword}: TodoPageComponentProp) => {
  const [todos, dispatch] = useReducer(todoReducer, []);
  const [todoTypes, setTodoTypes] = useState<TodoStatus>(TodoStatus.IN_PROGRESS);
  const todoInputRef = useRef<HTMLInputElement>(null);

  useEffect(()=>{
    const ctrl = new AbortController();
    (async () => {
      try {
        const res = await fetch(
          `/api/v1/todo?key=${userKeyword}`, { signal: ctrl.signal });
        if (!res.ok) return;
        const todos = (await res.json()).todos;
        dispatch({ type: TodoActionEnum.HYDRATE, todos })
      } catch (e) {
        console.log(e);
      }
    })();
    return () => ctrl.abort();
  },[]);

  const buckets = useMemo<StatusBuckets>(() => {
    const r: StatusBuckets = {
      [TodoStatus.IN_PROGRESS]: [],
      [TodoStatus.COMPLETED]: [],
      [TodoStatus.DELETED]: [],
    };
    for (const t of todos) r[t.status].push(t);
    return r;
  }, [todos]);

  const visibleTodos = buckets[todoTypes];

  const handleSubmit = useCallback((event: React.FormEvent) => {
    event.preventDefault();
    const todoInput = todoInputRef.current;
    if (!todoInput) return;
    const value = todoInput.value.trim();
    if (!value) return;

    const newTodo: Todo = {
      id: crypto.randomUUID(),
      value,
      status: TodoStatus.IN_PROGRESS,
      isOptimistic: true,
    };
    dispatch({ type: TodoActionEnum.ADD, todo: newTodo });
    todoInput.value = '';

    startTransition(async () => {
      try {
        const res = await fetch(`/api/v1/todo?key=${userKeyword}`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ newTodo }),
        });
        if (res.ok) dispatch({ type: TodoActionEnum.CONFRIM_FROM_BE, todoId: newTodo.id });
        else dispatch({ type: TodoActionEnum.REVERT_FROM_BE, todoId: newTodo.id });
      } catch {
        dispatch({ type: TodoActionEnum.REVERT_FROM_BE, todoId: newTodo.id });
      }
    });
  }, []);

  const onClickUpdate = useCallback(
    (todoId: string, status: TodoStatus, type: TodoActionEnum) => {
    dispatch({ type, todoId } as TodoAction);
    startTransition(async () => {
      try {
        const res = await fetch(`/api/v1/todo?key=${userKeyword}`, {
          method: 'PATCH',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ todoId, status }),
        });
        if (res.ok) dispatch({ type: TodoActionEnum.CONFRIM_FROM_BE, todoId });
        else dispatch({ type: TodoActionEnum.REVERT_FROM_BE, todoId });
      } catch {
        dispatch({ type: TodoActionEnum.REVERT_FROM_BE, todoId });
      }
    })
  }, [dispatch]);

  return (
    <>
      <div style={styles.header}>
        <h1 style={styles.title}>TODO!</h1>
        <div style={styles.filterBar} role="tablist" aria-label="Filter">
          <button
            style={{ ...styles.filterBtn, ...(todoTypes === TodoStatus.IN_PROGRESS ? styles.filterActive : null) }}
            onClick={() => setTodoTypes(TodoStatus.IN_PROGRESS)}
          >
            In progress
          </button>
          <button
            style={{ ...styles.filterBtn, ...(todoTypes === TodoStatus.DELETED ? styles.filterActive : null) }}
            onClick={() => setTodoTypes(TodoStatus.DELETED)}
          >
            Deleted
          </button>
          <button
            style={{ ...styles.filterBtn, ...(todoTypes === TodoStatus.COMPLETED ? styles.filterActive : null) }}
            onClick={() => setTodoTypes(TodoStatus.COMPLETED)}
          >
            Completed
          </button>
        </div>
      </div>

      <form style={styles.form} onSubmit={handleSubmit}>
        <input ref={todoInputRef} style={styles.input} placeholder="Add a task…" />
        <button style={{ ...styles.btn, ...styles.btnPrimary }} type="submit">
          Add
        </button>
      </form>

      <div style={todoTableCss}>
        {visibleTodos.map((todo) => (
          <TodoComponent key={todo.id} todo={todo} handleOnClickButton={onClickUpdate} />
        ))}
      </div>
    </>
  );
};

interface SetUserKeywordInputProp {
  setUserKeyword: Dispatch<SetStateAction<string>>;
}

const SetUserKeywordInput = (
  {setUserKeyword}: SetUserKeywordInputProp
) => {
  const inputRef = useRef<HTMLInputElement>(null);

  const handleSubmit = useCallback(() => {
    const inputEle = inputRef.current;
    if (!inputEle || !inputEle.value.trim()) return;
    setUserKeyword(inputEle.value.trim());
    const params = new URLSearchParams(window.location.search);
    params.set('key', inputEle.value.trim());
    history.replaceState(null, '', `${location.pathname}?${params}${location.hash}`);
    inputEle.value = "";
  }, [setUserKeyword])

  return (
   <>
     <h1 style={styles.title}>
       What is your keyword? 
     </h1>
     <h3>
       If it doesn't exist, it will start a new TODO list and used the keyword to encrypt the values.
     </h3>
     <form style={styles.form} onSubmit={handleSubmit}>
       <input ref={inputRef} style={styles.input} placeholder="What is your Keyword?" />
       <button style={{ ...styles.btn, ...styles.btnPrimary }} type="submit">
         Confirm 
       </button>
     </form>
   </>
  )
}

const Todo = () => {
  const [userKeyword, setUserKeyword] = useState<string>(() => {
    const params = new URLSearchParams(window.location.search);
    console.log()
    return params.get("key")  || "";
  });

  return (
    <div style={styles.page}>
      <div style={styles.card}>
        {
          userKeyword ?
            (<TodoPageComponent userKeyword={userKeyword}/>) : 
            (<SetUserKeywordInput setUserKeyword={setUserKeyword}/>)
        }
      </div>
    </div>
  )

}

ReactDOM.createRoot(document.getElementById('root')!).render(<Todo />);