Compare commits

..

1 commit

Author SHA1 Message Date
Renovate Bot
0e374ce1f1 Update dependency @vitest/eslint-plugin to v1.1.36
Some checks failed
renovate/artifacts Artifact file update failure
Testing / test (pull_request) Has been cancelled
2025-03-06 08:01:18 +00:00
13 changed files with 596 additions and 928 deletions

View file

@ -12,35 +12,20 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Deploy to OCI Registry - name: Set repository name to lowercase
uses: https://git.smgames.club/SevenOfAces/PublishImage@main run: echo "REPO_NAME=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
with:
registry-url: 'git.smgames.club'
registry-token: ${{ secrets.DOCKER_TOKEN }}
architectures: 'amd64,arm64'
tags: 'latest'
name: 'auto'
dockerfile: './Dockerfile'
owner: 'SevenOfAces'
# - name: Set repository name to lowercase - name: Set up Docker Buildx
# run: echo "REPO_NAME=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV uses: docker/setup-buildx-action@v3
# - name: Set up Docker - name: Authenticate with registry
# uses: https://github.com/docker-practice/actions-setup-docker@master run: docker login git.smgames.club -u ${{ github.repository_owner }} -p ${{ secrets.DOCKER_TOKEN }}
# timeout-minutes: 12
# - name: Set up Docker Buildx - name: Build and push multi-architecture Docker image
# uses: docker/setup-buildx-action@v3 run: |
docker buildx create --use
# - name: Authenticate with registry docker buildx build \
# run: docker login git.smgames.club -u ${{ github.repository_owner }} -p ${{ secrets.DOCKER_TOKEN }} --platform linux/amd64,linux/arm64 \
--tag git.smgames.club/${{ env.REPO_NAME }}:latest \
# - name: Build and push multi-architecture Docker image --push \
# run: | .
# docker buildx create --use
# docker buildx build \
# --platform linux/amd64,linux/arm64 \
# --tag git.smgames.club/${{ env.REPO_NAME }}:latest \
# --push \
# .

View file

@ -1,4 +1,4 @@
name: Run Tests name: Testing
on: on:
push: push:
@ -8,121 +8,21 @@ on:
branches: branches:
- '*' - '*'
env:
NODE_VERSION: 23
APT_PACKAGES: "xvfb libnss3 libatk-bridge2.0-0 libxkbcommon-x11-0 libgtk-3-0 libgbm1 libasound2 libxss1"
jobs: jobs:
install: test:
name: Prepare environment runs-on: docker
runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v2
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v2
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: '16'
cache: 'npm'
- name: Cache APT dependencies - name: Install dependencies
id: cache-apt run: npm ci
uses: actions/cache@v4
with:
path: /var/cache/apt/archives
key: apt-${{ runner.os }}-${{ env.APT_PACKAGES }}
- name: Install APT dependencies (if cache miss) - name: Run Unit tests
if: steps.cache-apt.outputs.cache-hit != 'true' run: npm run test:unit
run: sudo apt-get update && sudo apt-get install -y ${{ env.APT_PACKAGES }}
- name: Install NPM dependencies
run: npm ci
- name: Cache Cypress binary
uses: actions/cache@v4
with:
path: ~/.cache/Cypress
key: cypress-${{ runner.os }}-${{ env.NODE_VERSION }}-${{ hashFiles('package-lock.json') }}
- name: Cache Node modules
uses: actions/cache@v4
with:
path: node_modules
key: node-modules-${{ runner.os }}-${{ env.NODE_VERSION }}-${{ hashFiles('package-lock.json') }}
- name: Install Cypress
run: npx cypress install
- name: Build project
run: npm run build
- name: Save build artifact
uses: actions/upload-artifact@v3
with:
name: build
path: dist
unit-tests:
name: Run unit tests
runs-on: ubuntu-latest
needs: install
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Download build artifact
uses: actions/download-artifact@v3
with:
name: build
path: dist
- name: Restore Node modules cache
uses: actions/cache@v4
with:
name: node-modules
key: node-modules-${{ runner.os }}-${{ env.NODE_VERSION }}-${{ hashFiles('package-lock.json') }}
path: node_modules
- name: Run unit tests
run: npm run test:unit
cypress-tests:
name: Run E2E tests (${{ matrix.browser }})
runs-on: ubuntu-latest
needs: install
strategy:
fail-fast: false
matrix:
browser: [chrome, firefox, edge]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Download build artifact
uses: actions/download-artifact@v3
with:
name: build
path: dist
- name: Install APT dependencies
run: sudo apt-get update && sudo apt-get install -y ${{ env.APT_PACKAGES }}
- name: Cypress run (${{ matrix.browser }})
uses: https://github.com/cypress-io/github-action@v6
with:
start: npm start
browser: ${{ matrix.browser }}

View file

@ -1,8 +1,8 @@
// https://on.cypress.io/api // https://on.cypress.io/api
describe('Site Sanity Check', () => { describe('My First Test', () => {
it('visits the app root url', () => { it('visits the app root url', () => {
cy.visit('/') cy.visit('/')
cy.contains('h2', 'Development Projects') cy.contains('h1', 'You did it!')
}) })
}) })

759
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -3,19 +3,19 @@
"version": "0.0.0", "version": "0.0.0",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "run-p type-check \"build-only {@}\" --", "build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview", "preview": "vite preview",
"test:unit": "vitest --passWithNoTests", "test:unit": "vitest --passWithNoTests",
"test:e2e": "start-server-and-test 'vite preview' http://localhost:4173 'cypress run --e2e'", "test:e2e": "start-server-and-test preview http://localhost:4173 'cypress run --e2e'",
"test:e2e:dev": "start-server-and-test 'vite dev --port 4173' http://localhost:4173 'cypress open --e2e'", "test:e2e:dev": "start-server-and-test 'vite dev --port 4173' http://localhost:4173 'cypress open --e2e'",
"build-only": "vite build", "build-only": "vite build",
"type-check": "vue-tsc --build --force", "type-check": "vue-tsc --build --force",
"lint": "eslint . --fix", "lint": "eslint . --fix",
"format": "prettier --write src/", "format": "prettier --write src/",
"start": "npm run dev --host" "start": "npm run dev --host"
}, },
"dependencies": { "dependencies": {
"@tsparticles/slim": "^3.5.0", "@tsparticles/slim": "^3.5.0",
"@tsparticles/vue3": "^3.0.1", "@tsparticles/vue3": "^3.0.1",
@ -33,7 +33,7 @@
"@types/jsdom": "^21.1.7", "@types/jsdom": "^21.1.7",
"@types/node": "^22.9.0", "@types/node": "^22.9.0",
"@vitejs/plugin-vue": "^5.1.4", "@vitejs/plugin-vue": "^5.1.4",
"@vitest/eslint-plugin": "1.1.37", "@vitest/eslint-plugin": "1.1.36",
"@vue/eslint-config-prettier": "^10.1.0", "@vue/eslint-config-prettier": "^10.1.0",
"@vue/eslint-config-typescript": "^14.1.3", "@vue/eslint-config-typescript": "^14.1.3",
"@vue/test-utils": "^2.4.6", "@vue/test-utils": "^2.4.6",

View file

@ -1,35 +1,35 @@
<script setup lang="ts"> <script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router' import { RouterLink, RouterView } from 'vue-router';
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue';
// Theme state and available themes // Theme state and available themes
const theme = ref(localStorage.getItem('theme') || 'mocha') const theme = ref(localStorage.getItem("theme") || "mocha");
const availableThemes = [ const availableThemes = [
'mocha', "mocha",
'latte', "latte",
'yule-night', "yule-night",
'yule-day', "yule-day",
'midsummer-twilight', "midsummer-twilight",
'midsummer-daylight', "midsummer-daylight",
'fireworks-night', "fireworks-night",
'parade-day', "parade-day",
'harvest-twilight', "harvest-twilight",
'golden-hour', "golden-hour",
'stargazer', "stargazer",
'daydreamer', "daydreamer",
] ];
// Apply theme // Apply theme
onMounted(() => { onMounted(() => {
document.documentElement.setAttribute('data-theme', theme.value) document.documentElement.setAttribute("data-theme", theme.value);
}) });
// Change theme function // Change theme function
const changeTheme = (newTheme: string) => { const changeTheme = (newTheme: string) => {
theme.value = newTheme theme.value = newTheme;
document.documentElement.setAttribute('data-theme', newTheme) document.documentElement.setAttribute("data-theme", newTheme);
localStorage.setItem('theme', newTheme) localStorage.setItem("theme", newTheme);
} };
</script> </script>
<template> <template>
@ -48,11 +48,11 @@ const changeTheme = (newTheme: string) => {
</div> </div>
<!-- Theme Selector --> <!-- Theme Selector -->
<div class="theme-selector"> <div class="theme-selector">
<label for="theme-switcher">Theme:</label> <label for="theme-switcher">Theme:</label>
<select id="theme-switcher" v-model="theme" @change="changeTheme(theme)"> <select id="theme-switcher" v-model="theme" @change="changeTheme(theme)">
<option v-for="t in availableThemes" :key="t" :value="t">{{ t }}</option> <option v-for="t in availableThemes" :key="t" :value="t">{{ t }}</option>
</select> </select>
</div> </div>
</nav> </nav>
</header> </header>

View file

@ -1,5 +1,5 @@
/* Catppuccin Mocha */ /* Catppuccin Mocha */
:root[data-theme='mocha'] { :root[data-theme="mocha"] {
--color-surface0: #1e1e2e; /* Base */ --color-surface0: #1e1e2e; /* Base */
--color-surface1: #313244; /* Mantle */ --color-surface1: #313244; /* Mantle */
--color-surface2: #45475a; /* Surface */ --color-surface2: #45475a; /* Surface */
@ -26,7 +26,7 @@
} }
/* Catppuccin Latte */ /* Catppuccin Latte */
:root[data-theme='latte'] { :root[data-theme="latte"] {
--color-surface0: #eff1f5; /* Base */ --color-surface0: #eff1f5; /* Base */
--color-surface1: #e6e9ef; /* Mantle */ --color-surface1: #e6e9ef; /* Mantle */
--color-surface2: #ccd0da; /* Surface */ --color-surface2: #ccd0da; /* Surface */
@ -52,7 +52,7 @@
--color-overlay2: #9ca0b0; --color-overlay2: #9ca0b0;
} }
:root[data-theme='yule-night'] { :root[data-theme="yule-night"] {
--color-surface0: #1b1d28; /* Deep midnight */ --color-surface0: #1b1d28; /* Deep midnight */
--color-surface1: #252936; /* Frosty steel */ --color-surface1: #252936; /* Frosty steel */
--color-surface2: #343a48; /* Snow shadow */ --color-surface2: #343a48; /* Snow shadow */
@ -62,7 +62,7 @@
--color-border: #475266; /* Frosty edges */ --color-border: #475266; /* Frosty edges */
} }
:root[data-theme='yule-day'] { :root[data-theme="yule-day"] {
--color-surface0: #f5f3ed; /* Fresh snow */ --color-surface0: #f5f3ed; /* Fresh snow */
--color-surface1: #ece7df; /* Frosty beige */ --color-surface1: #ece7df; /* Frosty beige */
--color-surface2: #dcd3c3; /* Hearth ash */ --color-surface2: #dcd3c3; /* Hearth ash */
@ -72,7 +72,7 @@
--color-border: #9d9684; /* Frosted wood */ --color-border: #9d9684; /* Frosted wood */
} }
:root[data-theme='midsummer-twilight'] { :root[data-theme="midsummer-twilight"] {
--color-surface0: #241f36; /* Starry violet */ --color-surface0: #241f36; /* Starry violet */
--color-surface1: #2e2746; /* Dusky purple */ --color-surface1: #2e2746; /* Dusky purple */
--color-surface2: #403659; /* Twilight shadow */ --color-surface2: #403659; /* Twilight shadow */
@ -82,7 +82,7 @@
--color-border: #6b5a89; /* Lavender dusk */ --color-border: #6b5a89; /* Lavender dusk */
} }
:root[data-theme='midsummer-daylight'] { :root[data-theme="midsummer-daylight"] {
--color-surface0: #faf8eb; /* Bright sunlight */ --color-surface0: #faf8eb; /* Bright sunlight */
--color-surface1: #f2e7c4; /* Sunlit field */ --color-surface1: #f2e7c4; /* Sunlit field */
--color-surface2: #e6d399; /* Wheat gold */ --color-surface2: #e6d399; /* Wheat gold */
@ -92,7 +92,7 @@
--color-border: #a38a5b; /* Golden shadows */ --color-border: #a38a5b; /* Golden shadows */
} }
:root[data-theme='fireworks-night'] { :root[data-theme="fireworks-night"] {
--color-surface0: #0a0e1a; /* Starry sky */ --color-surface0: #0a0e1a; /* Starry sky */
--color-surface1: #121b32; /* Midnight blue */ --color-surface1: #121b32; /* Midnight blue */
--color-surface2: #1f2945; /* Smoke cloud */ --color-surface2: #1f2945; /* Smoke cloud */
@ -102,7 +102,7 @@
--color-border: #3b4e7e; /* Steel blue */ --color-border: #3b4e7e; /* Steel blue */
} }
:root[data-theme='parade-day'] { :root[data-theme="parade-day"] {
--color-surface0: #fafafa; /* White fabric */ --color-surface0: #fafafa; /* White fabric */
--color-surface1: #eaeaea; /* Pale silver */ --color-surface1: #eaeaea; /* Pale silver */
--color-surface2: #c9d3e3; /* Cerulean mist */ --color-surface2: #c9d3e3; /* Cerulean mist */
@ -112,7 +112,7 @@
--color-border: #8795b4; /* Cloud blue */ --color-border: #8795b4; /* Cloud blue */
} }
:root[data-theme='harvest-twilight'] { :root[data-theme="harvest-twilight"] {
--color-surface0: #1d1b13; /* Shadowed wheat field */ --color-surface0: #1d1b13; /* Shadowed wheat field */
--color-surface1: #29231a; /* Earthen soil */ --color-surface1: #29231a; /* Earthen soil */
--color-surface2: #4b3b27; /* Golden dusk */ --color-surface2: #4b3b27; /* Golden dusk */
@ -122,7 +122,7 @@
--color-border: #5d4633; /* Bark brown */ --color-border: #5d4633; /* Bark brown */
} }
:root[data-theme='golden-hour'] { :root[data-theme="golden-hour"] {
--color-surface0: #fef6e6; /* Golden wheat */ --color-surface0: #fef6e6; /* Golden wheat */
--color-surface1: #fdecc8; /* Honey glow */ --color-surface1: #fdecc8; /* Honey glow */
--color-surface2: #fcd399; /* Pumpkin yellow */ --color-surface2: #fcd399; /* Pumpkin yellow */
@ -132,7 +132,7 @@
--color-border: #a88a5f; /* Field shadows */ --color-border: #a88a5f; /* Field shadows */
} }
:root[data-theme='stargazer'] { :root[data-theme="stargazer"] {
--color-surface0: #0d1321; /* Midnight sky */ --color-surface0: #0d1321; /* Midnight sky */
--color-surface1: #1c2533; /* Cloudy night */ --color-surface1: #1c2533; /* Cloudy night */
--color-surface2: #283142; /* Subtle twilight */ --color-surface2: #283142; /* Subtle twilight */
@ -142,7 +142,7 @@
--color-border: #3e506a; /* Lunar blue */ --color-border: #3e506a; /* Lunar blue */
} }
:root[data-theme='daydreamer'] { :root[data-theme="daydreamer"] {
--color-surface0: #f9f9fc; /* Light paper */ --color-surface0: #f9f9fc; /* Light paper */
--color-surface1: #eceef3; /* Morning mist */ --color-surface1: #eceef3; /* Morning mist */
--color-surface2: #d7dcea; /* Overcast sky */ --color-surface2: #d7dcea; /* Overcast sky */
@ -151,3 +151,4 @@
--color-accent-hover: #81a1c1; /* Brighter sky blue */ --color-accent-hover: #81a1c1; /* Brighter sky blue */
--color-border: #b2c4d4; /* Subtle frost */ --color-border: #b2c4d4; /* Subtle frost */
} }

View file

@ -20,9 +20,7 @@ body {
a { a {
text-decoration: none; text-decoration: none;
color: var(--color-accent); color: var(--color-accent);
transition: transition: color 0.3s, background-color 0.3s;
color 0.3s,
background-color 0.3s;
} }
a:hover { a:hover {
@ -50,9 +48,7 @@ header nav a {
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
font-size: 1rem; font-size: 1rem;
color: var(--color-accent); color: var(--color-accent);
transition: transition: color 0.3s, background-color 0.3s;
color 0.3s,
background-color 0.3s;
} }
header nav a:hover { header nav a:hover {
@ -108,9 +104,7 @@ header nav a.router-link-exact-active {
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
transition: transition: background-color 0.3s, color 0.3s;
background-color 0.3s,
color 0.3s;
} }
.logout-button:hover { .logout-button:hover {
@ -306,9 +300,7 @@ main {
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
transition: transition: background 0.3s ease, transform 0.2s ease;
background 0.3s ease,
transform 0.2s ease;
} }
.close-button:hover { .close-button:hover {
@ -338,9 +330,7 @@ main {
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
text-align: center; text-align: center;
cursor: pointer; cursor: pointer;
transition: transition: transform 0.2s, box-shadow 0.2s;
transform 0.2s,
box-shadow 0.2s;
} }
.server-card:hover { .server-card:hover {
@ -439,9 +429,7 @@ main {
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
transition: transition: background-color 0.3s ease, transform 0.2s ease;
background-color 0.3s ease,
transform 0.2s ease;
} }
.close-button:hover { .close-button:hover {
@ -471,9 +459,7 @@ main {
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
transition: transition: background 0.3s, transform 0.2s ease;
background 0.3s,
transform 0.2s ease;
} }
.faq-button:hover { .faq-button:hover {
@ -615,9 +601,7 @@ main {
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
transition: transition: background 0.3s ease, transform 0.2s ease;
background 0.3s ease,
transform 0.2s ease;
margin-top: 1rem; margin-top: 1rem;
} }
@ -695,9 +679,7 @@ main {
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
font-size: 1rem; font-size: 1rem;
transition: transition: background 0.3s, transform 0.2s;
background 0.3s,
transform 0.2s;
} }
.login .btn-login:hover { .login .btn-login:hover {

View file

@ -1,3 +1,3 @@
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;

View file

@ -1,21 +1,21 @@
import axios from 'axios' import axios from "axios";
const API_BASE = 'http://localhost:3000' // Update with your backend address const API_BASE = "http://localhost:3000"; // Update with your backend address
export const register = async (username: string, password: string) => { export const register = async (username: string, password: string) => {
return axios.post(`${API_BASE}/register`, { username, password }) return axios.post(`${API_BASE}/register`, { username, password });
} };
export const login = async (username: string, password: string) => { export const login = async (username: string, password: string) => {
const response = await axios.post(`${API_BASE}/login`, { username, password }) const response = await axios.post(`${API_BASE}/login`, { username, password });
const token = response.data.token const token = response.data.token;
localStorage.setItem('token', token) // Save token for authenticated requests localStorage.setItem("token", token); // Save token for authenticated requests
return token return token;
} };
export const getProtectedData = async () => { export const getProtectedData = async () => {
const token = localStorage.getItem('token') const token = localStorage.getItem("token");
return axios.get(`${API_BASE}/protected`, { return axios.get(`${API_BASE}/protected`, {
headers: { Authorization: `Bearer ${token}` }, headers: { Authorization: `Bearer ${token}` },
}) });
} };

View file

@ -1,5 +1,6 @@
<template> <template>
<div class="landing-page"> <div class="landing-page">
<section class="sections"> <section class="sections">
<!-- Family Content --> <!-- Family Content -->
<div class="section-box"> <div class="section-box">
@ -10,9 +11,7 @@
<h3>Sharkey</h3> <h3>Sharkey</h3>
<p> <p>
Twitter alternative without a flood of Nazis.<br /><br /> Twitter alternative without a flood of Nazis.<br /><br />
We manually approve all members + verify them outside of the net, too. Fam & friends We manually approve all members + verify them outside of the net, too. Fam & friends only to make an account in our space, but you can use any Sharkey/Mastodon/etc instance to register elsewhere and talk to us.
only to make an account in our space, but you can use any Sharkey/Mastodon/etc
instance to register elsewhere and talk to us.
</p> </p>
<a href="https://social.smgames.club/" class="link">Go to Platform</a> <a href="https://social.smgames.club/" class="link">Go to Platform</a>
</div> </div>
@ -20,8 +19,8 @@
<div class="card"> <div class="card">
<h3>Game Servers</h3> <h3>Game Servers</h3>
<p> <p>
Privately hosted servers for Minecraft, Garry's Mod, TF2, Terraria, and more. Clicking Privately hosted servers for Minecraft, Garry's Mod, TF2, Terraria, and more.
each game shows instructions to get on. Clicking each game shows instructions to get on.
</p> </p>
<a href="/servers" class="link">See Our Game Servers</a> <a href="/servers" class="link">See Our Game Servers</a>
</div> </div>

View file

@ -5,51 +5,37 @@
<!-- Overview Section --> <!-- Overview Section -->
<div class="overview"> <div class="overview">
<p> <p>
Welcome to our projects page! We create <strong>websites</strong>, Welcome to our projects page! We create <strong>websites</strong>, <strong>video games</strong>,
<strong>video games</strong>, <strong>addons for games</strong>, <strong>addons for games</strong>, <strong>avatars and worlds for VRChat</strong>, and
<strong>avatars and worlds for VRChat</strong>, and <strong>game assets</strong>. Explore <strong>game assets</strong>. Explore our ongoing projects below.
our ongoing projects below.
</p> </p>
<button @click="showFaqModal = true" class="faq-button">FAQ</button> <button @click="showFaqModal = true" class="faq-button">FAQ</button>
</div> </div>
<!-- Filter Buttons --> <!-- Filter Buttons -->
<div class="filter-bar"> <div class="filter-bar">
<button <button v-for="tag in tags" :key="tag" @click="filterByTag(tag)" :class="{ active: selectedTag === tag }">
v-for="tag in tags"
:key="tag"
@click="filterByTag(tag)"
:class="{ active: selectedTag === tag }"
>
{{ tag }} {{ tag }}
</button> </button>
</div> </div>
<!-- Projects Grid --> <!-- Projects Grid -->
<div class="project-grid"> <div class="project-grid">
<div v-for="project in filteredProjects" :key="project.name" class="project-card"> <div
v-for="project in filteredProjects"
:key="project.name"
class="project-card"
>
<h3>{{ project.name }}</h3> <h3>{{ project.name }}</h3>
<p>{{ project.description }}</p> <p>{{ project.description }}</p>
<ul class="tags"> <ul class="tags">
<li v-for="tag in project.tags" :key="tag" class="tag">{{ tag }}</li> <li v-for="tag in project.tags" :key="tag" class="tag">{{ tag }}</li>
</ul> </ul>
<div class="links"> <div class="links">
<p v-if="project.links.public"> <p v-if="project.links.public"><strong>Public:</strong> <a :href="project.links.public" target="_blank">{{ project.links.public }}</a></p>
<strong>Public:</strong> <p v-if="project.links.local"><strong>Local:</strong> <a :href="project.links.local" target="_blank">{{ project.links.local }}</a></p>
<a :href="project.links.public" target="_blank">{{ project.links.public }}</a> <p v-if="project.links.testing"><strong>Testing:</strong> <a :href="project.links.testing" target="_blank">{{ project.links.testing }}</a></p>
</p> <p v-if="project.links.wiki"><strong>Wiki:</strong> <a :href="project.links.wiki" target="_blank">{{ project.links.wiki }}</a></p>
<p v-if="project.links.local">
<strong>Local:</strong>
<a :href="project.links.local" target="_blank">{{ project.links.local }}</a>
</p>
<p v-if="project.links.testing">
<strong>Testing:</strong>
<a :href="project.links.testing" target="_blank">{{ project.links.testing }}</a>
</p>
<p v-if="project.links.wiki">
<strong>Wiki:</strong>
<a :href="project.links.wiki" target="_blank">{{ project.links.wiki }}</a>
</p>
</div> </div>
</div> </div>
</div> </div>
@ -58,70 +44,58 @@
<div v-if="showFaqModal" class="modal-overlay" @click="closeFaqModal"> <div v-if="showFaqModal" class="modal-overlay" @click="closeFaqModal">
<div class="modal-content" @click.stop> <div class="modal-content" @click.stop>
<h2>Frequently Asked Questions</h2> <h2>Frequently Asked Questions</h2>
<hr /> <hr>
<ul class="faq-list"> <ul class="faq-list">
<li> <li>
<strong>"Do you need an idea person?"</strong> <strong>"Do you need an idea person?"</strong>
<p>No. Not now, and likely not ever.</p> <p>No. Not now, and likely not ever.</p>
</li> </li>
<hr /> <hr>
<li> <li>
<strong>"Do you need an asset creator? What would you pay me?"</strong> <strong>"Do you need an asset creator? What would you pay me?"</strong>
<p> <p>
This applies to graphics, models, sounds, music, and similar work. Suggesting random This applies to graphics, models, sounds, music, and similar work. Suggesting random ideas and demanding payment isnt how this works.
ideas and demanding payment isnt how this works. Provide a portfolio and clear price Provide a portfolio and clear price sheets. We offer fair indie rates based on quality. If your work is lower quality, understand that others
sheets. We offer fair indie rates based on quality. If your work is lower quality, may carry more of the creative load.
understand that others may carry more of the creative load.
</p> </p>
</li> </li>
<hr /> <hr>
<li> <li>
<strong <strong>"If I'm hired to work on sound effects, that means I'm the sound director, right?"</strong>
>"If I'm hired to work on sound effects, that means I'm the sound director,
right?"</strong
>
<p> <p>
No, it doesnt. Roles like "director" require experience, leadership skills, and a No, it doesnt. Roles like "director" require experience, leadership skills, and a proven track record. Freelance roles are just thatfreelance.
proven track record. Freelance roles are just thatfreelance. Dont expect a salary or Dont expect a salary or profit share from such a position. Additionally, if we hire you to create assets, <b>we</b> decide whats needed,
profit share from such a position. Additionally, if we hire you to create assets, unless creative freedom is explicitly granted. Thank you for understanding.
<b>we</b> decide whats needed, unless creative freedom is explicitly granted. Thank
you for understanding.
</p> </p>
</li> </li>
<hr /> <hr>
<li> <li>
<strong>"Well, how much WOULD you pay me?"</strong> <strong>"Well, how much WOULD you pay me?"</strong>
<p> <p>
Provide your portfolio and rates. If you dont have one, well evaluate your work Provide your portfolio and rates. If you dont have one, well evaluate your work against indie standards to make a fair offer. We're not presently hiring.
against indie standards to make a fair offer. We're not presently hiring.
</p> </p>
</li> </li>
<hr /> <hr>
<li> <li>
<strong>On suggesting ideas and claiming ownership</strong> <strong>On suggesting ideas and claiming ownership</strong>
<p> <p>
Ideas, concepts, and words alone are not copyrightableonly tangible creations such as Ideas, concepts, and words alone are not copyrightableonly tangible creations such as art, sound effects, or completed works can be protected by copyright.
art, sound effects, or completed works can be protected by copyright. Claiming Claiming ownership of an idea without contributing to its execution is not recognized in the professional world. That said, we value constructive suggestions and
ownership of an idea without contributing to its execution is not recognized in the may acknowledge meaningful contributions with in-game benefits or recognition. However, we maintain a zero-tolerance policy for unfounded claims or disruptive behavior,
professional world. That said, we value constructive suggestions and may acknowledge to ensure a fair and respectful environment for everyone.
meaningful contributions with in-game benefits or recognition. However, we maintain a
zero-tolerance policy for unfounded claims or disruptive behavior, to ensure a fair
and respectful environment for everyone.
</p> </p>
</li> </li>
<hr /> <hr>
<li> <li>
<strong>"You guys make games, so I want you to make this concept of mine!"</strong> <strong>"You guys make games, so I want you to make this concept of mine!"</strong>
<p> <p>
Not for free. We have our own projects to focus on, and Not for free. We have our own projects to focus on, and <u>we are not for hire at this time</u>. Additionally, being an "idea person" is not a paid position in the game
<u>we are not for hire at this time</u>. Additionally, being an "idea person" is not a development industry. Successful game creation requires collaboration, execution, and tangible contributions.
paid position in the game development industry. Successful game creation requires
collaboration, execution, and tangible contributions.
</p> </p>
</li> </li>
</ul> </ul>
<hr /> <hr>
<button @click="closeFaqModal" class="close-button">Close</button> <button @click="closeFaqModal" class="close-button">Close</button>
</div> </div>
</div> </div>
@ -133,80 +107,71 @@ export default {
data() { data() {
return { return {
showFaqModal: false, showFaqModal: false,
selectedTag: 'All', selectedTag: "All",
tags: [ tags: ["All", "Website", "PC", "Singleplayer", "Multiplayer", "ALPHA", "Horror", "Relaxing", "Sim", "RPG"],
'All',
'Website',
'PC',
'Singleplayer',
'Multiplayer',
'ALPHA',
'Horror',
'Relaxing',
'Sim',
'RPG',
],
projects: [ projects: [
{ {
name: 'Wildspace', name: "Wildspace",
description: 'A browser pet game.', description: "A browser pet game.",
tags: ['Website', 'Multiplayer', 'ALPHA'], tags: ["Website", "Multiplayer", "ALPHA"],
links: { links: {
public: 'https://thewild.space', public: "https://thewild.space",
local: null, local: null,
testing: null, testing: null,
wiki: 'wiki.smgames.club/wildspace', wiki: "wiki.smgames.club/wildspace",
}, },
}, },
{ {
name: 'Ghostbound', name: "Ghostbound",
description: 'A ghost hunting game.', description: "A ghost hunting game.",
tags: ['PC', 'Singleplayer', 'Multiplayer', 'ALPHA', 'Horror', 'Team'], tags: ["PC", "Singleplayer", "Multiplayer", "ALPHA", "Horror", "Team"],
links: { links: {
public: null, public: null,
local: null, local: null,
testing: null, testing: null,
wiki: 'wiki.smgames.club/ghostbound', wiki: "wiki.smgames.club/ghostbound",
}, },
}, },
{ {
name: '"Island"', name: '"Island"',
description: 'A cozy play-at-your-pace game.', description: "A cozy play-at-your-pace game.",
tags: ['PC', 'Singleplayer', 'Multiplayer', 'ALPHA', 'Relaxing', 'Sim'], tags: ["PC", "Singleplayer", "Multiplayer", "ALPHA", "Relaxing", "Sim"],
links: { links: {
public: null, public: null,
local: null, local: null,
testing: null, testing: null,
wiki: 'wiki.smgames.club/island', wiki: "wiki.smgames.club/island",
}, },
}, },
{ {
name: '"Random RPG"', name: '"Random RPG"',
description: 'An RPG game with actions and consequences.', description: "An RPG game with actions and consequences.",
tags: ['PC', 'Singleplayer', 'ALPHA', 'RPG'], tags: ["PC", "Singleplayer", "ALPHA", "RPG"],
links: { links: {
public: null, public: null,
local: null, local: null,
testing: null, testing: null,
wiki: 'wiki.smgames.club/randomrpg', wiki: "wiki.smgames.club/randomrpg",
}, },
}, },
], ],
} };
}, },
computed: { computed: {
filteredProjects() { filteredProjects() {
if (this.selectedTag === 'All') return this.projects if (this.selectedTag === "All") return this.projects;
return this.projects.filter((project) => project.tags.includes(this.selectedTag)) return this.projects.filter((project) =>
project.tags.includes(this.selectedTag)
);
}, },
}, },
methods: { methods: {
filterByTag(tag: string) { filterByTag(tag: string) {
this.selectedTag = tag this.selectedTag = tag;
}, },
closeFaqModal() { closeFaqModal() {
this.showFaqModal = false this.showFaqModal = false;
}, },
}, },
} };
</script> </script>

View file

@ -13,9 +13,7 @@
@click="openModal(name)" @click="openModal(name)"
> >
<h3>{{ name }}</h3> <h3>{{ name }}</h3>
<p> <p>Status: <span :class="server.status">{{ server.status }}</span></p>
Status: <span :class="server.status">{{ server.status }}</span>
</p>
</div> </div>
</div> </div>
</section> </section>
@ -31,9 +29,7 @@
@click="openModal(name)" @click="openModal(name)"
> >
<h3>{{ name }}</h3> <h3>{{ name }}</h3>
<p> <p>Status: <span :class="server.status">{{ server.status }}</span></p>
Status: <span :class="server.status">{{ server.status }}</span>
</p>
</div> </div>
</div> </div>
</section> </section>
@ -43,14 +39,10 @@
<div class="modal-content" @click.stop> <div class="modal-content" @click.stop>
<img v-if="modalData.banner" :src="modalData.banner" alt="Server Banner" class="banner" /> <img v-if="modalData.banner" :src="modalData.banner" alt="Server Banner" class="banner" />
<h2>{{ modalData.name || 'No Server Selected' }}</h2> <h2>{{ modalData.name || 'No Server Selected' }}</h2>
<p> <p>Status:
Status:
<span :class="modalData.status"> <span :class="modalData.status">
<i v-if="modalData.status === 'online'" class="fas fa-check-circle online-icon"></i> <i v-if="modalData.status === 'online'" class="fas fa-check-circle online-icon"></i>
<i <i v-else-if="modalData.status === 'offline'" class="fas fa-times-circle offline-icon"></i>
v-else-if="modalData.status === 'offline'"
class="fas fa-times-circle offline-icon"
></i>
<i v-else class="fas fa-question-circle unknown-icon"></i> <i v-else class="fas fa-question-circle unknown-icon"></i>
{{ modalData.status || 'unknown' }} {{ modalData.status || 'unknown' }}
</span> </span>
@ -90,24 +82,24 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue' import { defineComponent } from "vue";
interface ServerInstructions { interface ServerInstructions {
public: string public: string;
local: string local: string;
} }
interface Server { interface Server {
name?: string name?: string;
banner: string banner: string;
status: string status: string;
playersOnline?: number playersOnline?: number;
maxPlayers?: number maxPlayers?: number;
about: string about: string;
instructions: ServerInstructions instructions: ServerInstructions;
installInstructions?: string installInstructions?: string;
queryIP: string queryIP: string;
link: string | null link: string | null;
} }
export default defineComponent({ export default defineComponent({
@ -117,202 +109,207 @@ export default defineComponent({
modalData: {} as Server, modalData: {} as Server,
alwaysOnServers: { alwaysOnServers: {
'Minecraft Java Modern': { "Minecraft Java Modern": {
banner: 'minecraft-modern-banner.jpg', banner: "minecraft-modern-banner.jpg",
status: 'unknown', status: "unknown",
playersOnline: 0, playersOnline: 0,
maxPlayers: 20, maxPlayers: 20,
about: 'This is the modern Minecraft Java server.', about: "This is the modern Minecraft Java server.",
instructions: { instructions: {
public: '', public: "",
local: '192.168.1.201', local: "192.168.1.201",
}, },
installInstructions: "Download Minecraft Java Edition from Mojang's website.", installInstructions: "Download Minecraft Java Edition from Mojang's website.",
queryIP: '', queryIP: "",
link: '', link: "",
}, },
'Minecraft Java 1.12.2': { "Minecraft Java 1.12.2": {
banner: 'minecraft-1-12-banner.jpg', banner: "minecraft-1-12-banner.jpg",
status: 'unknown', status: "unknown",
playersOnline: 0, playersOnline: 0,
maxPlayers: 10, maxPlayers: 10,
about: 'This is the Minecraft Java 1.12.2 server with mods.', about: "This is the Minecraft Java 1.12.2 server with mods.",
instructions: { instructions: {
public: '', public: "",
local: '', local: "",
}, },
installInstructions: 'Install Forge 1.12.2 and download the modpack provided.', installInstructions: "Install Forge 1.12.2 and download the modpack provided.",
queryIP: '', queryIP: "",
link: '', link: "",
}, },
Terraria: { Terraria: {
banner: 'terraria-banner.jpg', banner: "terraria-banner.jpg",
status: 'unknown', status: "unknown",
playersOnline: 0, playersOnline: 0,
maxPlayers: 8, maxPlayers: 8,
about: 'Our modded Terraria instance for friends and family.', about: "Our modded Terraria instance for friends and family.",
instructions: { instructions: {
public: '', public: "",
local: '192.168.1.121', local: "192.168.1.121",
}, },
installInstructions: installInstructions:
'Use TModLoader from Steam (not base Terraria) and download our pack.', "Use TModLoader from Steam (not base Terraria) and download our pack.",
queryIP: '', queryIP: "",
link: 'https://steamcommunity.com/sharedfiles/filedetails/?id=2943030068', link: "https://steamcommunity.com/sharedfiles/filedetails/?id=2943030068",
}, },
'Team Fortress 2': { "Team Fortress 2": {
banner: 'tf2-banner.jpg', banner: "tf2-banner.jpg",
status: 'unknown', status: "unknown",
playersOnline: 0, playersOnline: 0,
maxPlayers: 16, maxPlayers: 16,
about: 'Classic Team Fortress 2 fun with friends.', about: "Classic Team Fortress 2 fun with friends.",
instructions: { instructions: {
public: '', public: "",
local: '192.168.1.203', local: "192.168.1.203",
}, },
installInstructions: 'Download Team Fortress 2 for free on Steam.', installInstructions: "Download Team Fortress 2 for free on Steam.",
queryIP: '', queryIP: "",
link: '', link: "",
}, },
} as Record<string, Server>, // Add type Record<string, Server> for alwaysOnServers } as Record<string, Server>, // Add type Record<string, Server> for alwaysOnServers
toggledServers: { toggledServers: {
'Core Keeper': { "Core Keeper": {
banner: 'core-keeper-banner.jpg', banner: "core-keeper-banner.jpg",
status: 'unknown', status: "unknown",
playersOnline: 0, playersOnline: 0,
maxPlayers: 16, maxPlayers: 16,
about: 'Explore, mine, and survive in this pixelated adventure.', about: "Explore, mine, and survive in this pixelated adventure.",
instructions: { instructions: {
public: '', public: "",
local: '192.168.1.204', local: "192.168.1.204",
}, },
installInstructions: 'Available on Steam. Ensure your game is up to date.', installInstructions: "Available on Steam. Ensure your game is up to date.",
queryIP: '', queryIP: "",
link: null, link: null,
}, },
ECO: { ECO: {
banner: 'eco-banner.jpg', banner: "eco-banner.jpg",
status: 'unknown', status: "unknown",
playersOnline: 0, playersOnline: 0,
maxPlayers: 50, maxPlayers: 50,
about: 'A global survival game where players build an ecosystem.', about: "A global survival game where players build an ecosystem.",
instructions: { instructions: {
public: 'eco.example.com', public: "eco.example.com",
local: '', local: "",
}, },
installInstructions: installInstructions:
'Download ECO from the official website or Steam. Use the provided IP.', "Download ECO from the official website or Steam. Use the provided IP.",
queryIP: 'http://eco.example.com/status', queryIP: "http://eco.example.com/status",
link: null, link: null,
}, },
Enshrouded: { Enshrouded: {
banner: 'enshrouded-banner.jpg', banner: "enshrouded-banner.jpg",
status: 'unknown', status: "unknown",
playersOnline: 0, playersOnline: 0,
maxPlayers: 16, maxPlayers: 16,
about: 'A survival crafting game in a mysterious fantasy setting.', about: "A survival crafting game in a mysterious fantasy setting.",
instructions: { instructions: {
public: '', public: "",
local: '', local: "",
}, },
installInstructions: undefined, installInstructions: undefined,
queryIP: 'http://enshrouded.example.com/status', queryIP: "http://enshrouded.example.com/status",
link: null, link: null,
}, },
Empyrion: { Empyrion: {
banner: 'empyrion-banner.jpg', banner: "empyrion-banner.jpg",
status: 'unknown', status: "unknown",
playersOnline: 0, playersOnline: 0,
maxPlayers: 16, maxPlayers: 16,
about: 'Explore space and build your intergalactic empire.', about: "Explore space and build your intergalactic empire.",
instructions: { instructions: {
public: '', public: "",
local: '', local: "",
}, },
installInstructions: installInstructions:
'Download Empyrion from Steam and ensure mods match server settings.', "Download Empyrion from Steam and ensure mods match server settings.",
queryIP: 'http://empyrion.example.com/status', queryIP: "http://empyrion.example.com/status",
link: null, link: null,
}, },
Palworld: { Palworld: {
banner: 'palworld-banner.jpg', banner: "palworld-banner.jpg",
status: 'unknown', status: "unknown",
playersOnline: 0, playersOnline: 0,
maxPlayers: 32, maxPlayers: 32,
about: 'A multiplayer game where you befriend and fight alongside creatures.', about: "A multiplayer game where you befriend and fight alongside creatures.",
instructions: { instructions: {
public: '', public: "",
local: '', local: "",
}, },
installInstructions: 'Available on Steam. Join the server via multiplayer menu.', installInstructions: "Available on Steam. Join the server via multiplayer menu.",
queryIP: 'http://palworld.example.com/status', queryIP: "http://palworld.example.com/status",
link: null, link: null,
}, },
'Survive The Nights': { "Survive The Nights": {
banner: 'survive-the-nights-banner.jpg', banner: "survive-the-nights-banner.jpg",
status: 'unknown', status: "unknown",
playersOnline: 0, playersOnline: 0,
maxPlayers: 10, maxPlayers: 10,
about: 'A survival horror game set in a post-apocalyptic world.', about: "A survival horror game set in a post-apocalyptic world.",
instructions: { instructions: {
public: '', public: "",
local: '', local: "",
}, },
installInstructions: 'Purchase on Steam and ensure to update your client.', installInstructions: "Purchase on Steam and ensure to update your client.",
queryIP: 'http://survivethenights.example.com/status', queryIP: "http://survivethenights.example.com/status",
link: null, link: null,
}, },
Valheim: { Valheim: {
banner: 'valheim-banner.jpg', banner: "valheim-banner.jpg",
status: 'unknown', status: "unknown",
playersOnline: 0, playersOnline: 0,
maxPlayers: 10, maxPlayers: 10,
about: 'A Viking-themed survival game set in a procedurally generated world.', about: "A Viking-themed survival game set in a procedurally generated world.",
instructions: { instructions: {
public: '', public: "",
local: '', local: "",
}, },
installInstructions: 'Install via Steam and ensure mods match server settings.', installInstructions: "Install via Steam and ensure mods match server settings.",
queryIP: 'http://valheim.example.com/status', queryIP: "http://valheim.example.com/status",
link: null, link: null,
}, },
'V Rising': { "V Rising": {
banner: 'v-rising-banner.jpg', banner: "v-rising-banner.jpg",
status: 'unknown', status: "unknown",
playersOnline: 0, playersOnline: 0,
maxPlayers: 20, maxPlayers: 20,
about: 'Rise as a vampire lord in this survival RPG.', about: "Rise as a vampire lord in this survival RPG.",
instructions: { instructions: {
public: '', public: "",
local: '', local: "",
}, },
installInstructions: 'Available on Steam. Ensure your game and server mods are synced.', installInstructions:
queryIP: 'http://vrising.example.com/status', "Available on Steam. Ensure your game and server mods are synced.",
queryIP: "http://vrising.example.com/status",
link: null, link: null,
}, },
} as Record<string, Server>, // Add type Record<string, Server> for toggledServers } as Record<string, Server>, // Add type Record<string, Server> for toggledServers
} };
}, },
computed: { computed: {
showPlayerCount(): boolean { showPlayerCount(): boolean {
return this.modalData.playersOnline !== undefined && this.modalData.maxPlayers !== undefined return (
this.modalData.playersOnline !== undefined &&
this.modalData.maxPlayers !== undefined
);
}, },
displayPlayerCount(): string { displayPlayerCount(): string {
if (this.modalData.maxPlayers === undefined) return '? / ?' if (this.modalData.maxPlayers === undefined) return "? / ?";
if (this.modalData.playersOnline === undefined) return '? / ?' if (this.modalData.playersOnline === undefined) return "? / ?";
return `${this.modalData.playersOnline} / ${this.modalData.maxPlayers}` return `${this.modalData.playersOnline} / ${this.modalData.maxPlayers}`;
}, },
}, },
methods: { methods: {
openModal(serverName: string) { openModal(serverName: string) {
this.modalData = this.alwaysOnServers[serverName] || this.toggledServers[serverName] || {} this.modalData =
this.modalData.name = serverName this.alwaysOnServers[serverName] || this.toggledServers[serverName] || {};
this.showModal = true this.modalData.name = serverName;
this.showModal = true;
}, },
closeModal() { closeModal() {
this.showModal = false this.showModal = false;
}, },
}, },
}) });
</script> </script>