Compare commits

..

No commits in common. "136612da24c8514f15e32d907eec5061c98bfe29" and "b7787961956336318fdf070468b96e856387cf7e" have entirely different histories.

10 changed files with 14 additions and 108 deletions

View File

@ -42,11 +42,9 @@ export const gameController = createController({
theme: pickRandom(collection.entries.filter((e) => e.selected)).id, theme: pickRandom(collection.entries.filter((e) => e.selected)).id,
}); });
upsertGameState(db, newGame); upsertGameState(db, newGame);
// Emit new game event with gameId for spectating
emit({ emit({
type: "new", type: "new",
user, user,
gameId: uuid,
}); });
emit({ emit({
type: "updateStage", type: "updateStage",

View File

@ -233,7 +233,7 @@ export const game = {
if (!isValid(serverGame, x, y)) return; if (!isValid(serverGame, x, y)) return;
if (isQuestionMark[x][y]) return; if (isQuestionMark[x][y]) return;
if (isFlagged[x][y]) return; if (isFlagged[x][y]) return;
if (initial) serverGame.lastClick = [x, y]; serverGame.lastClick = [x, y];
if (mines[x][y]) { if (mines[x][y]) {
serverGame.finished = Date.now(); serverGame.finished = Date.now();

View File

@ -1,2 +1,2 @@
User-agent: * User-agent: *
Allow: / Disallow: /

View File

@ -6,7 +6,6 @@ export type Events =
| { | {
type: "new"; type: "new";
user: string; user: string;
gameId: string;
} }
| { | {
type: "loss"; type: "loss";

View File

@ -24,19 +24,12 @@ const Feed: React.FC = () => {
const data = JSON.parse(event.data) as Events; const data = JSON.parse(event.data) as Events;
const newItems = [...items]; const newItems = [...items];
if (data.type === "new" && data.user !== user) { if (data.type === "new" && data.user !== user) {
// Remove any existing gameStarted items for this user newItems.push({
const filteredItems = newItems.filter(
item => !(item.type === "gameStarted" && item.user === data.user)
);
filteredItems.push({
type: "gameStarted", type: "gameStarted",
user: data.user, user: data.user,
gameId: data.gameId,
id: crypto.randomUUID(), id: crypto.randomUUID(),
decay: Date.now() + 1000 * 3, decay: Date.now() + 1000 * 3,
}); });
setItems(filteredItems);
return;
} }
if (data.type === "loss") { if (data.type === "loss") {
newItems.push({ newItems.push({

View File

@ -5,7 +5,6 @@ import GemsIcon from "../GemIcon";
import type { Rarity as RarityType } from "../../../shared/lootboxes"; import type { Rarity as RarityType } from "../../../shared/lootboxes";
import { Rarity } from "../Rarity"; import { Rarity } from "../Rarity";
import { themes } from "../../themes"; import { themes } from "../../themes";
import { Link } from "wouter";
interface BaseFeedItem { interface BaseFeedItem {
decay: number; decay: number;
@ -15,7 +14,6 @@ interface BaseFeedItem {
interface GameStartedItem extends BaseFeedItem { interface GameStartedItem extends BaseFeedItem {
type: "gameStarted"; type: "gameStarted";
user: string; user: string;
gameId: string;
} }
interface GameFinishedItem extends BaseFeedItem { interface GameFinishedItem extends BaseFeedItem {
@ -61,19 +59,7 @@ const FeedItemWrapper: React.FC<PropsWithChildren> = ({ children }) => {
const FeedItemElement: React.FC<{ item: FeedItem }> = ({ item }) => { const FeedItemElement: React.FC<{ item: FeedItem }> = ({ item }) => {
switch (item.type) { switch (item.type) {
case "gameStarted": case "gameStarted":
return ( return <FeedItemWrapper>{item.user} started a game</FeedItemWrapper>;
<FeedItemWrapper>
<span>
{item.user} started a game -{" "}
<Link
href={`/play/${item.gameId}`}
className="text-white/70 hover:text-white/90 underline underline-offset-2 decoration-white/30 hover:decoration-white/50"
>
spectate
</Link>
</span>
</FeedItemWrapper>
);
case "gameFinished": case "gameFinished":
return ( return (
<FeedItemWrapper> <FeedItemWrapper>

View File

@ -1,42 +0,0 @@
import { Button } from "./Button";
import { useState } from "react";
interface ShareButtonProps {
gameId: string;
className?: string;
}
export const ShareButton: React.FC<ShareButtonProps> = ({ gameId, className }) => {
const [copied, setCopied] = useState(false);
const handleShare = async () => {
const shareUrl = `${window.location.origin}/play/${gameId}`;
try {
await navigator.clipboard.writeText(shareUrl);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch {
// Fallback for browsers that don't support clipboard API
const textArea = document.createElement("textarea");
textArea.value = shareUrl;
document.body.appendChild(textArea);
textArea.select();
document.execCommand("copy");
document.body.removeChild(textArea);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
};
return (
<Button
variant="outline"
onClick={handleShare}
className={className}
title="Share game for spectating"
>
{copied ? "Copied!" : "Share"}
</Button>
);
};

View File

@ -45,9 +45,7 @@ setup().then(() => {
<Route path="/settings" component={Settings} /> <Route path="/settings" component={Settings} />
<Route path="/collection" component={Collection} /> <Route path="/collection" component={Collection} />
<Route path="/store" component={Store} /> <Route path="/store" component={Store} />
<Route path="/profile/:username?"> <Route path="/profile" component={Profile} />
{(params) => <Profile username={params.username} />}
</Route>
</Switch> </Switch>
</AnimatePresence> </AnimatePresence>
</Shell> </Shell>

View File

@ -3,11 +3,9 @@ import { useAtom } from "jotai";
import { gameIdAtom, loginTokenAtom } from "../../atoms"; import { gameIdAtom, loginTokenAtom } from "../../atoms";
import { Button } from "../../components/Button"; import { Button } from "../../components/Button";
import LeaderboardButton from "../../components/LeaderboardButton"; import LeaderboardButton from "../../components/LeaderboardButton";
import { ShareButton } from "../../components/ShareButton";
import { Fragment, startTransition, Suspense, useEffect } from "react"; import { Fragment, startTransition, Suspense, useEffect } from "react";
import { Board } from "../../components/LazyBoard"; import { Board } from "../../components/LazyBoard";
import RegisterButton from "../../components/Auth/RegisterButton"; import RegisterButton from "../../components/Auth/RegisterButton";
import { Link } from "wouter";
interface EndlessProps { interface EndlessProps {
gameId?: string; gameId?: string;
@ -18,7 +16,6 @@ const Endless: React.FC<EndlessProps> = (props) => {
const [loginToken] = useAtom(loginTokenAtom); const [loginToken] = useAtom(loginTokenAtom);
const { data: game } = useWSQuery("game.getGameState", gameId!, !!gameId); const { data: game } = useWSQuery("game.getGameState", gameId!, !!gameId);
const { data: settings } = useWSQuery("user.getSettings", null); const { data: settings } = useWSQuery("user.getSettings", null);
const { data: currentUsername } = useWSQuery("user.getSelf", null);
const startGame = useWSMutation("game.createGame"); const startGame = useWSMutation("game.createGame");
const { data: leaderboard } = useWSQuery("scoreboard.getScoreBoard", 100); const { data: leaderboard } = useWSQuery("scoreboard.getScoreBoard", 100);
const reveal = useWSMutation("game.reveal"); const reveal = useWSMutation("game.reveal");
@ -26,9 +23,6 @@ const Endless: React.FC<EndlessProps> = (props) => {
const placeQuestionMark = useWSMutation("game.placeQuestionMark"); const placeQuestionMark = useWSMutation("game.placeQuestionMark");
const clearTile = useWSMutation("game.clearTile"); const clearTile = useWSMutation("game.clearTile");
// Check if current user is spectating someone else's game
const isSpectating = game && game.user !== currentUsername;
useEffect(() => { useEffect(() => {
if (props.gameId) { if (props.gameId) {
setGameId(props.gameId); setGameId(props.gameId);
@ -47,18 +41,6 @@ const Endless: React.FC<EndlessProps> = (props) => {
Stage {game.stage} Stage {game.stage}
</div> </div>
<div className="grow" /> <div className="grow" />
{isSpectating && (
<div className="flex items-center gap-2 text-white/80">
<span>Spectating</span>
<Link
href={`/profile/${game.user}`}
className="text-white/90 hover:text-purple-400 font-bold transition-colors"
>
{game.user}
</Link>
</div>
)}
<ShareButton gameId={gameId!} />
<LeaderboardButton label="View Leaderboard" /> <LeaderboardButton label="View Leaderboard" />
</div> </div>
<Suspense> <Suspense>

View File

@ -72,35 +72,27 @@ const TouchTooltip = ({
); );
}; };
interface ProfileProps { const Profile: React.FC = () => {
username?: string | undefined;
}
const Profile: React.FC<ProfileProps> = ({ username: targetUsername }) => {
const [availableWeeks, setAvailableWeeks] = useState(16); // Default to 4 months const [availableWeeks, setAvailableWeeks] = useState(16); // Default to 4 months
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const { data: currentUsername } = useWSQuery("user.getSelf", null); const { data: username } = useWSQuery("user.getSelf", null);
// Use targetUsername if provided, otherwise use current user's username
const profileUsername = targetUsername || currentUsername;
const { data: heatmap } = useWSQuery( const { data: heatmap } = useWSQuery(
"user.getHeatmap", "user.getHeatmap",
{ id: profileUsername! }, { id: username! },
!!profileUsername, !!username,
); );
const { data: profile } = useWSQuery( const { data: profile } = useWSQuery(
"user.getProfile", "user.getProfile",
{ id: profileUsername! }, { id: username! },
!!profileUsername, !!username,
); );
const { data: pastGames } = useWSQuery( const { data: pastGames } = useWSQuery(
"game.getGames", "game.getGames",
{ {
user: profileUsername!, user: username!,
page: 0, page: 0,
}, },
!!profileUsername, !!username,
); );
const now = useMemo(() => dayjs(), []); const now = useMemo(() => dayjs(), []);
const maxHeat = heatmap ? Math.max(...heatmap) : 0; const maxHeat = heatmap ? Math.max(...heatmap) : 0;
@ -166,7 +158,7 @@ const Profile: React.FC<ProfileProps> = ({ username: targetUsername }) => {
<div className="grid md:[grid-template-columns:_2fr_3fr] gap-6 px-2 sm:px-0"> <div className="grid md:[grid-template-columns:_2fr_3fr] gap-6 px-2 sm:px-0">
<div className="mx-4 my-8 md:m-8 text-white flex flex-col sm:flex-row self-center"> <div className="mx-4 my-8 md:m-8 text-white flex flex-col sm:flex-row self-center">
<div className="p-2 flex items-center text-2xl mb-2 sm:mb-0"> <div className="p-2 flex items-center text-2xl mb-2 sm:mb-0">
{profileUsername} {username}
</div> </div>
<div className="border-l-0 sm:border-l-white sm:border-l p-2 text-lg"> <div className="border-l-0 sm:border-l-white sm:border-l p-2 text-lg">
<p>Total Games: {profile?.totalGames}</p> <p>Total Games: {profile?.totalGames}</p>