Compare commits
No commits in common. "136612da24c8514f15e32d907eec5061c98bfe29" and "b7787961956336318fdf070468b96e856387cf7e" have entirely different histories.
136612da24
...
b778796195
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,2 @@
|
||||||
User-agent: *
|
User-agent: *
|
||||||
Allow: /
|
Disallow: /
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ export type Events =
|
||||||
| {
|
| {
|
||||||
type: "new";
|
type: "new";
|
||||||
user: string;
|
user: string;
|
||||||
gameId: string;
|
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: "loss";
|
type: "loss";
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue