Skip to content
Draft
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
3 changes: 3 additions & 0 deletions web/public/fullscreen-close.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions web/public/fullscreen-open.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
113 changes: 0 additions & 113 deletions web/public/index.html

This file was deleted.

28 changes: 28 additions & 0 deletions web/src/app/(main)/index.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
.container {
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
flex-wrap: wrap;
justify-content: space-between;
align-items: stretch;
align-content: stretch;
position: relative;
}

.game {
flex: 1;
}

.embed {
width: 100%;
height: 100%;
}

.directions {
position: absolute;
right: 8px;
z-index: 1;
top: 8px;
font-size: clamp(0.8rem, 1vw, 1rem);
}
12 changes: 12 additions & 0 deletions web/src/app/(main)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
import '@/styles/globals.css'

export const metadata = {
title: 'Quadradius',
description: 'Quadradius is a two-player turn-based strategy game from 2007 made in Flash. A game of skill, luck, and deception, with the gameplay of checkerboards on steroids. Originally created by Jimmi Heiserman and Brad Kayal.',
};

export const viewport = {
width: 'device-width',
initialScale: 1,
};

export default function MainLayout({
children,
}: {
Expand Down
27 changes: 26 additions & 1 deletion web/src/app/(main)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,30 @@
'use client';

import styles from './index.module.css';
import Ruffle from '@/components/ruffle/ruffle';
import FullscreenToggle from '@/components/ui/fullscreen-toggle/fullscreen-toggle';
import Footer from "@/components/ui/footer/footer";
import { useUnloadWarning } from '@/hooks/use-unload-warning';


export default function Home() {
useUnloadWarning();

return (
<h1>Quadradius Web App</h1>
<>
<div className={styles.directions}>
<a href="/directions.html" target="_blank">How to play &amp; Powerup Cheatsheet</a>
</div>
<FullscreenToggle />

<Ruffle />

<div className={styles.container}>
<object className={styles.game}>
<embed src="./quadradius_lobby.swf" className={styles.embed} />
</object>
<Footer />
</div>
</>
)
}
31 changes: 31 additions & 0 deletions web/src/components/ruffle/ruffle-config.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
'use client';

import { useEffect } from 'react';

export default function RuffleConfig() {
useEffect(() => {
const host = window.location.host;
const protocol = window.location.protocol == 'http:' ? 'ws:' : 'wss:';

window.RufflePlayer = window.RufflePlayer || {};
window.RufflePlayer.config = {
autoplay: "on",
contextMenu: "off",
logLevel: "debug",
socketProxy: [
{
host: "127.0.0.1",
port: 3000,
proxyUrl: `${protocol}//${host}/websocket/lobby`,
},
{
host: "127.0.0.1",
port: 3001,
proxyUrl: `${protocol}//${host}/websocket/game`,
},
],
};
}, []);

return null;
}
27 changes: 27 additions & 0 deletions web/src/components/ruffle/ruffle-loader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
'use client';

import Script from 'next/script';
import { useSearchParams } from 'next/navigation';
import config from '@/configurations/config.json';

export default function RuffleLoader() {
const searchParams = useSearchParams();

const queryVersion = searchParams.get('ruffle_version');

const version =
queryVersion ??
config.ruffleVersion ??
'';

const ruffleUrl = version
? `https://unpkg.com/@ruffle-rs/ruffle@${version}/ruffle.js`
: 'https://unpkg.com/@ruffle-rs/ruffle';

return (
<Script
src={ruffleUrl}
strategy="afterInteractive"
/>
);
}
14 changes: 14 additions & 0 deletions web/src/components/ruffle/ruffle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Suspense } from 'react';
import RuffleConfig from './ruffle-config';
import RuffleLoader from './ruffle-loader';

export default function Ruffle() {
return (
<>
<RuffleConfig />
<Suspense fallback={null}>
<RuffleLoader />
</Suspense>
</>
);
}
7 changes: 7 additions & 0 deletions web/src/components/ui/footer/footer.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.footer {

@Fruktus Fruktus Jun 4, 2026

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've modified it so that it stays on top of the swf instead of being below it, since it did not fit in the viewport most of the time.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now it is the same as it was - a part of game container

padding: 0.3em;
color: #444444;
text-align: center;
font-size: clamp(0.8rem, 1vw, 1rem);
}

9 changes: 9 additions & 0 deletions web/src/components/ui/footer/footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import styles from './footer.module.css';

export default function Footer() {
return (
<footer className={styles.footer}>
Made possible by the <a href="https://github.com/Fruktus/QuadradiusPreservationProject">Quadradius Preservation Project</a>.
</footer>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
.fullscreen {
position: absolute;
left: 10px;
top: 10px;
z-index: 10;
}

.toggle {
position: relative;
width: 32px;
height: 32px;
background: rgba(255, 255, 255, 0.7);
border: 1px solid #ccc;
border-radius: 4px;
padding: 5px;
cursor: pointer;
display: none;
align-items: center;
justify-content: center;
opacity: 0.25;
}

.toggle img {
inset: 10px;
}

.toggle:hover {
background: rgba(255, 255, 255, 0.9);
opacity: 0.8;
}

.toggle img {
padding: 4px;
}

@media (hover: none) {
.toggle {
display: flex;
}
}
44 changes: 44 additions & 0 deletions web/src/components/ui/fullscreen-toggle/fullscreen-toggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
'use client';

import Image from 'next/image';
import { useEffect, useRef } from 'react';
import styles from './fullscreen-toggle.module.css';

export default function FullscreenToggle() {
const openIconRef = useRef<HTMLImageElement>(null);
const closeIconRef = useRef<HTMLImageElement>(null);

useEffect(() => {
const openIcon = openIconRef.current;
const closeIcon = closeIconRef.current;
if (!openIcon || !closeIcon) return;

closeIcon.style.display = 'none';

const updateIcons = (isFullscreen: boolean) => {
openIcon.style.display = isFullscreen ? 'none' : 'block';
closeIcon.style.display = isFullscreen ? 'block' : 'none';
};

const handleFullscreenChange = () => updateIcons(!!document.fullscreenElement);
document.addEventListener('fullscreenchange', handleFullscreenChange);
return () => document.removeEventListener('fullscreenchange', handleFullscreenChange);
}, []);

const handleClick = () => {
if (!document.fullscreenElement) {
void document.documentElement.requestFullscreen();
} else {
void document.exitFullscreen();
}
};

return (
<div className={styles.fullscreen}>
<button className={styles.toggle} title="Toggle fullscreen" onClick={handleClick}>
<Image ref={openIconRef} src="/fullscreen-open.svg" alt="Fullscreen open icon" fill />
<Image ref={closeIconRef} src="/fullscreen-close.svg" alt="Fullscreen close icon" fill />
</button>
</div>
);
}
1 change: 1 addition & 0 deletions web/src/configurations/config.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"ruffleVersion": "0.2.0-nightly.2026.4.21",
"discordChannel": "https://discord.com/channels/1197227968112627802",
"discordRegistration": "https://discord.com/channels/1197227968112627802/1297809215066341396"
}
Loading
Loading