Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
283 changes: 268 additions & 15 deletions client/src/App.jsx

Large diffs are not rendered by default.

38 changes: 38 additions & 0 deletions client/src/components/BreakMode.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,19 @@ export default function BreakMode({ initialGame, onClose, noSnooze, reason, befo
</div>
</div>
</div>
) : phase === "games" ? (
<div className="space-y-8">
<div className="rounded-[40px] p-7 shadow-glow md:p-10" style={{ background: colors.cardBg, border: `1px solid ${colors.cardBorder}` }}>
<div className="flex flex-col gap-5 lg:flex-row lg:items-start lg:justify-between">
<div>
<p className="text-sm font-bold uppercase tracking-[0.22em]" style={{ color: colors.muted }}>Wellby Game Lounge</p>
<h2 className="font-display text-4xl md:text-5xl">Recharge in a way that actually feels fun</h2>
<p className="mt-3 max-w-3xl text-base leading-8" style={{ color: colors.muted }}>{breakMessage}</p>
</div>
<button
onClick={() => setPhase("complete")}
className="rounded-full px-6 py-3 font-bold lg:self-start"
style={{ background: colors.breakBtn, color: colors.breakBtnText }}
</div>
) : phase === "games" ? (
<div className="mx-auto max-w-6xl space-y-6">
Expand Down Expand Up @@ -410,6 +423,31 @@ export default function BreakMode({ initialGame, onClose, noSnooze, reason, befo
Finish break
</button>
</div>
<div className="mt-6 rounded-[28px] px-5 py-4" style={{ background: colors.secondary, border: `1px solid ${colors.cardBorder}` }}>
<p className="text-sm leading-7" style={{ color: colors.muted }}>
Pick a game, settle in for a minute, and then head back when you feel reset.
Right now you&apos;re in <span className="font-bold" style={{ color: colors.secondaryText }}>{GAME_OPTIONS.find((game) => game.id === selectedGame)?.label}</span>.
</p>
</div>
<div className="mt-8 grid gap-5 md:grid-cols-2 xl:grid-cols-4">
{GAME_OPTIONS.map((game) => (
<button
key={game.id}
onClick={() => setSelectedGame(game.id)}
className="min-h-32 rounded-[28px] border px-6 py-7 text-left transition-transform hover:-translate-y-0.5"
style={{
background: selectedGame === game.id ? colors.primary : colors.gameCardBg,
color: selectedGame === game.id ? colors.primaryText : colors.secondaryText,
borderColor: selectedGame === game.id ? colors.primary : colors.gameCardBorder
}}
>
<div className="text-4xl">{game.emoji}</div>
<div className="mt-3 text-lg font-bold">{game.label}</div>
<div className="mt-2 text-sm" style={{ color: selectedGame === game.id ? colors.primaryText : colors.muted }}>
{selectedGame === game.id ? "Currently active" : "Open this game"}
</div>
</button>
))}
</div>

<div className="mt-8 grid gap-4 md:grid-cols-2 xl:grid-cols-4">
Expand Down
47 changes: 46 additions & 1 deletion client/src/components/SettingsPage.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useContext } from "react";
import React,{ useContext } from "react";
import { ThemeContext } from "../context/ThemeContext.jsx";
import LeafIcon from "./LeafIcon.jsx";
import { EXTENSION_PROMPT_INTERVAL_OPTIONS } from "../lib/constants.js";
import { hexToRgba } from "../lib/color.js";

const THEME_OPTIONS = [
Expand All @@ -13,6 +14,8 @@ const THEME_OPTIONS = [
export default function SettingsPage({
fatigueOptIn,
onToggleFatigue,
extensionPromptInterval,
onSetExtensionPromptInterval,
fatigueStatus,
mode,
onToggleMode,
Expand Down Expand Up @@ -130,6 +133,48 @@ export default function SettingsPage({
{fatigueOptIn ? "Enabled" : "Disabled"}
</button>
</div>

<div className="mt-4 rounded-[24px] p-4" style={{ background: colors.secondary }}>
<div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
<div>
<h2 className="font-bold">Browser check-in timer</h2>
<p className="mt-2 text-sm leading-7" style={{ color: colors.muted }}>
Pick how often the tiny Wellby tab should expand on other websites and ask for a stress check-in.
</p>
</div>
<div
className="rounded-full px-4 py-2 text-sm font-bold"
style={{ background: colors.sidebarBg, color: colors.wordmark }}
>
{extensionPromptInterval} min
</div>
</div>

<div className="mt-4 grid gap-3 md:grid-cols-2">
{EXTENSION_PROMPT_INTERVAL_OPTIONS.map((option) => (
<button
key={option.value}
onClick={() => onSetExtensionPromptInterval(option.value)}
className="rounded-[22px] border p-4 text-left"
style={{
background:
extensionPromptInterval === option.value ? colors.navActiveBg : colors.cardBg,
borderColor:
extensionPromptInterval === option.value ? colors.navActiveBorder : colors.cardBorder,
color:
extensionPromptInterval === option.value ? colors.navActive : colors.secondaryText
}}
>
<div className="flex items-center justify-between gap-3">
<span className="font-bold">{option.label}</span>
{extensionPromptInterval === option.value ? (
<span className="text-sm font-bold">Selected</span>
) : null}
</div>
</button>
))}
</div>
</div>
</section>
</div>
</div>
Expand Down
53 changes: 43 additions & 10 deletions client/src/components/games/ChessGame.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useContext, useEffect, useMemo, useState } from "react";
import React,{ useContext, useEffect, useMemo, useState } from "react";
import { Chess } from "chess.js";
import { ThemeContext } from "../../context/ThemeContext.jsx";

Expand Down Expand Up @@ -81,6 +81,11 @@ export default function ChessGame({ onExit }) {
const [captured, setCaptured] = useState({ white: [], black: [] });

const board = useMemo(() => game.board().flat(), [game]);
const winnerText = game.isGameOver()
? game.isCheckmate()
? `You ${game.turn() === "b" ? "win" : "lose"}!`
: "Game over."
: null;

useEffect(() => {
if (mode !== "ai" || game.turn() !== "b" || game.isGameOver()) {
Expand Down Expand Up @@ -146,14 +151,16 @@ export default function ChessGame({ onExit }) {

return (
<div className="rounded-[28px] p-5" style={{ background: colors.cardBg, color: colors.secondaryText, border: `1px solid ${colors.cardBorder}` }}>
<div className="mb-4 flex items-center justify-between">
<div>
<h3 className="font-display text-2xl">Chess</h3>
<p className="text-sm">Pass-and-play or a simple depth-3 AI opponent.</p>
</div>
<button onClick={onExit} className="rounded-full px-4 py-2 text-sm font-bold" style={{ background: colors.breakBtn, color: colors.breakBtnText }}>
<div className="mb-6 flex flex-col gap-4">
<button onClick={onExit} className="self-end rounded-full px-4 py-2 text-sm font-bold" style={{ background: colors.breakBtn, color: colors.breakBtnText }}>
End Break & Return to Work
</button>
<div className="rounded-[24px] px-6 py-5 text-center" style={{ background: colors.cardBg, border: `2px solid ${colors.primary}` }}>
<h3 className="font-display text-4xl">Chess</h3>
<p className="mt-2 text-sm" style={{ color: colors.muted }}>
Play pass-and-play or challenge a lighter AI opponent.
</p>
</div>
</div>
<div className="mb-4 flex gap-3">
<button
Expand All @@ -177,8 +184,8 @@ export default function ChessGame({ onExit }) {
Two-player
</button>
</div>
<div className="grid gap-4 lg:grid-cols-[auto_220px]">
<div className="grid w-full max-w-[520px] grid-cols-8 overflow-hidden rounded-[24px] shadow-lg">
<div className="grid gap-4 xl:grid-cols-[minmax(0,1fr)_220px]">
<div className="mx-auto grid w-full max-w-[460px] grid-cols-8 overflow-hidden rounded-[24px] shadow-lg">
{board.map((piece, index) => {
const isLight = (Math.floor(index / 8) + index) % 2 === 0;
const square = algebraic(index);
Expand All @@ -190,7 +197,7 @@ export default function ChessGame({ onExit }) {
<button
key={square}
onClick={() => handleSquareClick(index)}
className="aspect-square text-3xl transition"
className="aspect-square text-[2rem] transition"
style={{
background: isLight ? colors.secondary : colors.primary,
color: isLight ? colors.secondaryText : colors.primaryText,
Expand Down Expand Up @@ -224,6 +231,32 @@ export default function ChessGame({ onExit }) {
</button>
</aside>
</div>
<div
className="mt-5 rounded-[24px] px-5 py-4 text-center"
style={{
background: winnerText
? winnerText.includes("win")
? colors.pillGoodBg
: colors.pillWarnBg
: colors.secondary,
color: winnerText
? winnerText.includes("win")
? colors.pillGoodText
: colors.pillWarnText
: colors.secondaryText,
border: `1px solid ${
winnerText
? winnerText.includes("win")
? colors.pillGoodBorder
: colors.pillWarnBorder
: colors.cardBorder
}`
}}
>
<div className="text-lg font-extrabold">
{winnerText ?? `Turn: ${game.turn() === "w" ? "White" : "Black"}`}
</div>
</div>
</div>
);
}
43 changes: 34 additions & 9 deletions client/src/components/games/SnakeGame.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export default function SnakeGame({ onExit }) {
const [food, setFood] = useState(() => randomFood(INITIAL_SNAKE));
const [score, setScore] = useState(0);
const [gameOver, setGameOver] = useState(false);
const [isRunning, setIsRunning] = useState(false);
const highScores = readStorage(STORAGE_KEYS.gameScores, { snake: 0 });

useEffect(() => {
Expand Down Expand Up @@ -57,7 +58,7 @@ export default function SnakeGame({ onExit }) {
}, []);

useEffect(() => {
if (gameOver) {
if (gameOver || !isRunning) {
return;
}

Expand Down Expand Up @@ -98,7 +99,7 @@ export default function SnakeGame({ onExit }) {
}, 140);

return () => clearInterval(interval);
}, [food, gameOver, highScores, score]);
}, [food, gameOver, highScores, isRunning, score]);

useEffect(() => {
const canvas = canvasRef.current;
Expand Down Expand Up @@ -126,31 +127,55 @@ export default function SnakeGame({ onExit }) {
setFood(randomFood(INITIAL_SNAKE));
setScore(0);
setGameOver(false);
setIsRunning(false);
}

return (
<div className="rounded-[28px] p-6 md:p-7" style={{ background: colors.cardBg, color: colors.secondaryText, border: `1px solid ${colors.cardBorder}` }}>
<div className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div>
<h3 className="font-display text-2xl">Snake</h3>
<p className="text-sm">High score: {readStorage(STORAGE_KEYS.gameScores, { snake: 0 }).snake ?? 0}</p>
</div>
<button onClick={onExit} className="rounded-full px-4 py-2 text-sm font-bold" style={{ background: colors.breakBtn, color: colors.breakBtnText }}>
<div className="mb-6 flex flex-col gap-4">
<button onClick={onExit} className="self-end rounded-full px-4 py-2 text-sm font-bold" style={{ background: colors.breakBtn, color: colors.breakBtnText }}>
End Break & Return to Work
</button>
<div className="rounded-[24px] px-6 py-5 text-center" style={{ background: colors.cardBg, border: `2px solid ${colors.primary}` }}>
<h3 className="font-display text-4xl">Snake</h3>
<p className="mt-2 text-sm font-bold uppercase tracking-[0.16em]" style={{ color: colors.muted }}>
High score: {readStorage(STORAGE_KEYS.gameScores, { snake: 0 }).snake ?? 0}
</p>
<p className="mt-2 text-sm" style={{ color: colors.muted }}>
Glide through the grid, eat the food, and keep your streak alive.
</p>
</div>
</div>
<div className="flex justify-center rounded-[28px] px-4 py-6" style={{ background: colors.secondary }}>
<canvas ref={canvasRef} width={320} height={320} className="rounded-[24px] shadow-inner" />
</div>
<div
className="mt-5 rounded-[24px] px-5 py-4 text-center"
style={{
background: gameOver ? colors.pillDangerBg : colors.secondary,
color: gameOver ? colors.pillDangerText : colors.secondaryText,
border: `1px solid ${gameOver ? colors.pillDangerBorder : colors.cardBorder}`
}}
>
<div className="text-lg font-extrabold">
{gameOver ? "You lose!" : isRunning ? "Stay sharp and keep climbing." : "Press start when you're ready."}
</div>
</div>
<div className="mt-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<p className="font-bold">Score: {score}</p>
{gameOver ? (
<div className="flex flex-wrap items-center gap-3">
<span className="text-sm font-bold">Ready to get back to it?</span>
<button onClick={restart} className="rounded-full px-4 py-2 text-sm font-bold" style={{ background: colors.primary, color: colors.primaryText }}>
Play again
</button>
</div>
) : !isRunning ? (
<div className="flex flex-wrap items-center gap-3">
<span className="text-sm" style={{ color: colors.muted }}>Use arrow keys or WASD</span>
<button onClick={() => setIsRunning(true)} className="rounded-full px-5 py-2 text-sm font-bold" style={{ background: colors.primary, color: colors.primaryText }}>
Start
</button>
</div>
) : (
<span className="text-sm" style={{ color: colors.muted }}>Use arrow keys or WASD</span>
)}
Expand Down
Loading