Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement environment variables #99

Merged
merged 29 commits into from
Dec 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
56de767
Add game title env var
karlromets Dec 27, 2024
586dfd1
Add max file upload env vars
karlromets Dec 27, 2024
29003ce
Fix improper settimeout
karlromets Dec 27, 2024
e0db5e2
Improve file type validation for CSV and JSON uploads
karlromets Dec 27, 2024
c8cf2b3
Add prettier config
karlromets Dec 28, 2024
48ad10d
merge(master): sync latest changes
karlromets Dec 28, 2024
5a8538a
Title screen styling fixes
karlromets Dec 28, 2024
fcb8302
Refactor Title component to use Team, RoomCode, and TitleUrl components
karlromets Dec 28, 2024
e68f64c
Rename title to TitlePage and move it to Title/
karlromets Dec 28, 2024
b0fefd0
Default to opt out of TITLE_URL
karlromets Dec 28, 2024
24f201f
Return null instead of nothing
karlromets Dec 28, 2024
feefcc8
Update tailwind config
karlromets Dec 28, 2024
eea21cc
merge(master): sync latest changes
karlromets Dec 29, 2024
27e4467
Players in Team now scrollable on overflow
karlromets Dec 29, 2024
0982c4c
Update README.md
karlromets Dec 29, 2024
7990b05
Remove title timeout
karlromets Dec 29, 2024
37aa7e4
Styling changes to title page
karlromets Dec 29, 2024
4c35685
Resolve merge conflicts
karlromets Dec 29, 2024
6b7fa16
chore(conflicts): sync with master branch
karlromets Dec 30, 2024
7224daf
fix(tests): Update team id
karlromets Dec 30, 2024
d32f2fd
chore(deps): add playwright as a dev dependency
karlromets Dec 30, 2024
5a471aa
chore(deps): remove playwright (already exists) & add npm test script
karlromets Dec 30, 2024
d4a5a11
fix(backend): prevent panic/crash on invalid session format
karlromets Dec 30, 2024
495db95
fix(frontend): preserve spectator session format in pong messages
karlromets Dec 30, 2024
e21448a
fix(frontend): ensure final round timer initializes correctly
karlromets Dec 30, 2024
0fb952c
fix(e2e): remove extra quit press
karlromets Dec 30, 2024
dc8ce4f
fix(e2e): improve test reliability with proper timeouts and retries
karlromets Dec 30, 2024
18ef572
fix(backend): align error codes with translation system
karlromets Dec 30, 2024
1c72d0c
fix(backend): use var instead of string
karlromets Dec 30, 2024
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
9 changes: 9 additions & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# URL where your instance is hosted
# When set, it will display "Join at [your-url]" on the title screen
NEXT_PUBLIC_TITLE_URL=''

# Maximum size in megabytes for logo image uploads
NEXT_PUBLIC_MAX_IMAGE_UPLOAD_SIZE_MB=2

# Maximum size in megabytes for CSV file uploads
NEXT_PUBLIC_MAX_CSV_UPLOAD_SIZE_MB=2
karlromets marked this conversation as resolved.
Show resolved Hide resolved
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ backend/tmp/
backend/public/
backend/Cold-Friendly-Feud
backend/famf.db
test-results/
13 changes: 13 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"printWidth": 120,
"useTabs": false,
"tabWidth": 2,
"trailingComma": "es5",
"semi": true,
"singleQuote": true,
"bracketSpacing": true,
"arrowParens": "always",
"jsxSingleQuote": false,
"bracketSameLine": false,
"endOfLine": "lf"
}
10 changes: 9 additions & 1 deletion backend/api/display.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,15 @@ import (

func GameWindow(client *Client, event *Event) GameError {
session := strings.Split(event.Session, ":")
roomCode, _ := session[0], session[1]
if len(session) < 2 {
return GameError{code: PARSE_ERROR}
}

roomCode := session[0]
if roomCode == "" {
return GameError{code: PARSE_ERROR}
}

s := store
room, storeError := s.getRoom(client, roomCode)
if storeError.code != "" {
Expand Down
7 changes: 7 additions & 0 deletions backend/api/ping.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package api

import (
"time"
"strings"
)

func Pong(client *Client, event *Event) GameError {
Expand All @@ -10,6 +11,12 @@ func Pong(client *Client, event *Event) GameError {
if storeError.code != "" {
return storeError
}

// Check if this is a spectator session (ends with :0)
if strings.HasSuffix(event.Session, ":0") {
return GameError{} // Silently accept pongs from spectators
}

player, ok := room.Game.RegisteredPlayers[event.ID]
if ! ok {
return GameError{code: PLAYER_NOT_FOUND}
Expand Down
28 changes: 28 additions & 0 deletions components/Title/RoomCode.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useTranslation } from 'react-i18next';

const RoomCode = ({ code }) => {
if (!code) return null;

const { i18n, t } = useTranslation();

return (
<div className="flex flex-row justify-center items-center text-center">
<div className="flex flex-col items-center rounded-xl bg-secondary-500 shadow-lg transition-transform xl:p-4 2xl:p-6 h-fit">
<div className={`${process.env.NEXT_PUBLIC_TITLE_URL ? '' : 'p-8'}`}>
<p className="text-6xl font-bold text-foreground" id="roomCodeText">{code}</p>
<p className="text-xl font-semibold text-foreground">{t('Room Code')}</p>
</div>
{process.env.NEXT_PUBLIC_TITLE_URL && (
<>
<hr className="w-full my-2 border-t-2 border-secondary-900" />
<span className="text-4xl pt-0 p-6 text-foreground rounded-xl bg-secondary-500 transition-transform duration-200">
{t('Join at')} <span className="font-bold">{process.env.NEXT_PUBLIC_TITLE_URL}</span>
</span>
</>
)}
</div>
</div>
);
};

export default RoomCode;
22 changes: 22 additions & 0 deletions components/Title/Team.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const Team = ({team, players}) => {
return (
<div className="flex flex-col text-center bg-secondary-500 rounded-xl shadow-lg overflow-hidden h-full">
<p className="text-4xl text-foreground font-bold bg-secondary-700 rounded-t-xl py-2 shadow-sm" id="team-name">
{team}
</p>
<div className="flex-1 min-h-0 relative">
<div className="absolute inset-0 overflow-y-auto">
<div className="flex flex-wrap flex-row justify-center px-2">
{players.map((m, index) => (
<div key={`${m}-${index}`} className="bg-primary-200 m-2 rounded-lg w-20 xl:w-28 p-2">
<p className="font-bold text-foreground overflow-hidden text-ellipsis whitespace-nowrap">{m}</p>
</div>
))}
</div>
</div>
</div>
</div>
);
};

export default Team;
90 changes: 90 additions & 0 deletions components/Title/TitlePage.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import 'tailwindcss/tailwind.css';
import TitleLogo from '../title-logo';
import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import Team from './Team';
import RoomCode from './RoomCode';

export default function TitlePage(props) {
const { i18n, t } = useTranslation();
const [titleSize, setTitleSize] = useState('10%');

useEffect(() => {
const handleResize = () => {
if (props.game.settings.logo_url) {
setTitleSize(window.innerWidth * 0.75);
} else {
setTitleSize(
window.innerWidth *
(window.innerWidth < 640
? 0.8
: window.innerWidth < 1024
? 0.8
: window.innerWidth < 1280
? 0.7
: window.innerWidth < 1536
? 0.75
: 0.75)
);
}
};

handleResize();
window.addEventListener('resize', handleResize);

return () => {
window.removeEventListener('resize', handleResize);
};
}, [props.game.settings.logo_url]);

function returnTeamMates(team) {
let players = [];
console.debug(props.game);
Object.keys(props.game.registeredPlayers).forEach((k) => {
console.debug(k);
if (props.game.registeredPlayers[k].team === team) {
players.push(props.game.registeredPlayers[k].name);
}
});
console.debug(players);
return players;
}

return (
<div className="bg-gradient-to-t items-center justify-center from-primary-900 flex flex-col via-primary-200 to-primary-900 min-h-screen min-w-screen py-5">
{/* Logo Section */}
<div
style={{
width: titleSize,
transition: 'width 2s',
}}
className="align-middle inline-block"
>
<div className="flex justify-center w-full ">
{props.game.settings.logo_url ? (
<img
className="w-full h-[300px] min-h-[200px] object-contain"
src={`${props.game.settings.logo_url}`}
size={titleSize}
alt="Game logo"
/>
) : (
<TitleLogo insert={props.game.title_text} size={titleSize} />
)}
</div>
</div>

<div
className="grid grid-cols-3 gap-4 h-[200px] 2xl:h-[250px]"
style={{
width: titleSize,
transition: 'width 2s',
}}
>
<Team team={props.game.teams[0].name} players={returnTeamMates(0)} />
<RoomCode code={props.game.room} />
<Team team={props.game.teams[1].name} players={returnTeamMates(1)} />
</div>
</div>
);
}
90 changes: 55 additions & 35 deletions components/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import LanguageSwitcher from "./language";
import CSVLoader from "./Admin/csv-loader";
import { Buffer } from "buffer";
import { BSON } from "bson";
import { handleCsvFile, handleJsonFile } from "utils/files";
import { ERROR_CODES } from "i18n/errorCodes";

function debounce(callback, wait = 400) {
Expand Down Expand Up @@ -236,9 +237,7 @@ function TitleLogoUpload(props) {
var file = document.getElementById("logoUpload").files[0];

if (file) {
const fileSize = Math.round(file.size / 1024);
// 2MB
if (fileSize > 2098) {
if (file.size > process.env.NEXT_PUBLIC_MAX_IMAGE_UPLOAD_SIZE_MB * 1024 * 1024) {
console.error("Logo image is too large");
props.setError(
t(ERROR_CODES.IMAGE_TOO_LARGE, { message: "2MB" })
Expand Down Expand Up @@ -364,6 +363,18 @@ function FinalRoundPointTotals(props) {
);
}

function isValidFileType(file, allowedTypes) {
const fileName = file.name.toLowerCase();
const fileExtension = fileName.split('.').pop();

if(!allowedTypes[fileExtension]) {
return false;
}

const mimePattern = allowedTypes[fileExtension].pattern;
return mimePattern.test(file.type);
}

export default function Admin(props) {
const { i18n, t } = useTranslation();

Expand Down Expand Up @@ -537,6 +548,7 @@ export default function Admin(props) {
))}
</select>
) : null}
{/* Image Upload */}
<div id="gamePickerFileUploadButton" className="image-upload w-6">
<label htmlFor="gamePickerFileUpload">
<svg
Expand All @@ -546,48 +558,56 @@ export default function Admin(props) {
<path d="M224 136V0H24C10.7 0 0 10.7 0 24v464c0 13.3 10.7 24 24 24h336c13.3 0 24-10.7 24-24V160H248c-13.2 0-24-10.8-24-24zm65.18 216.01H224v80c0 8.84-7.16 16-16 16h-32c-8.84 0-16-7.16-16-16v-80H94.82c-14.28 0-21.41-17.29-11.27-27.36l96.42-95.7c6.65-6.61 17.39-6.61 24.04 0l96.42 95.7c10.15 10.07 3.03 27.36-11.25 27.36zM377 105L279.1 7c-4.5-4.5-10.6-7-17-7H256v128h128v-6.1c0-6.3-2.5-12.4-7-16.9z" />
</svg>
</label>
{/* CSV Upload */}
<input
className="hidden"
type="file"
accept=".json, .csv"
id="gamePickerFileUpload"
onChange={(e) => {
var file = document.getElementById("gamePickerFileUpload").files[0];
console.debug(file);
if (file?.type === "application/json") {
if (file) {
var reader = new FileReader();
reader.readAsText(file, "utf-8");
reader.onload = function(evt) {
let data = JSON.parse(evt.target.result);
console.debug(data);
// TODO some error checking for invalid game data
send({ action: "load_game", data: data });
};
reader.onerror = function(evt) {
console.error("error reading file");
setError(t(ERROR_CODES.PARSE_ERROR));
};

if (file) {
if (file.size > process.env.NEXT_PUBLIC_MAX_CSV_UPLOAD_SIZE_MB * 1024 * 1024) {
console.error("This csv file is too large");
props.setError(
t(ERROR_CODES.CSV_TOO_LARGE),
);
return;
}
} else if (file?.type === "text/csv") {
var reader = new FileReader();
reader.readAsText(file, "utf-8");
reader.onload = function(evt) {
let lineCount = evt.target.result.split("\n");
if (lineCount.length > 30) {
setError(t(ERROR_CODES.CSV_TOO_LARGE));
} else {
setCsvFileUpload(file);
setCsvFileUploadText(evt.target.result);
}
};
reader.onerror = function(evt) {
console.error("error reading file");
setError(t(ERROR_CODES.PARSE_ERROR));
};
} else {
}

const allowedTypes = {
'json': {
pattern: /^application\/(json|.*\+json)$/,
handler: (file) => handleJsonFile(file, {
setError,
t,
send
})
},
'csv': {
pattern: /^(text\/csv|application\/(vnd\.ms-excel|csv|x-csv|text-csv))$/,
handler: (file) => handleCsvFile(file, {
setError,
t,
setCsvFileUpload,
setCsvFileUploadText
})
}
};

const fileType = isValidFileType(file, allowedTypes);
if (!fileType) {
setError(t(ERROR_CODES.UNKNOWN_FILE_TYPE));
return;
}

const fileExtension = file.name.toLowerCase().split('.').pop();
allowedTypes[fileExtension].handler(file);

console.debug(file);

// allow same file to be selected again
document.getElementById("gamePickerFileUpload").value = null;
}}
Expand Down
2 changes: 1 addition & 1 deletion components/title-logo.js
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ export default function TitleLogo(props) {
</svg>
`;
return (
<div id="titleLogoImg" className="w-full">
<div id="titleLogoImg" className="-mt-16 w-full">
<div dangerouslySetInnerHTML={{ __html: logo }} />
</div>
);
Expand Down
Loading