Compare commits

..

No commits in common. "main" and "main" have entirely different histories.
main ... main

121 changed files with 2831 additions and 9013 deletions

View file

@ -1,15 +1,9 @@
DISCORD_CLIENT= DISCORD_CLIENT=
DISCORD_SECRET= DISCORD_SECRET=
DISCORD_REDIRECT="http://localhost:3000/api/v1/auth" DISCORD_REDIRECT=
DISCORD_OAUTH=
DISCORD_API="https://discord.com/api/v10" MYSQL_HOST=
MYSQL_DB=
DB_NAME= MYSQL_USER=
DB_USER= MYSQL_PASS=
DB_PASS=
DB_HOST=
DB_PORT=
AUTH_SECRET=
MC_API=

View file

@ -1,17 +0,0 @@
{
"extends": "next/core-web-vitals",
"rules": {
"indent": [
"error",
2
],
"quotes": [
"error",
"single"
],
"semi": [
"error",
"always"
]
}
}

View file

@ -1,23 +0,0 @@
name: Deploy Website
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy Over SSH
uses: https://github.com/nekiro/ssh-job@main
with:
host: ${{ secrets.HOST }}
user: crss
password: ${{ secrets.DEPLOY_PASSWORD }}
command: |
cd ~/crss
git pull
~/.local/share/pnpm/pnpm install
~/.local/share/pnpm/pnpm build
sudo systemctl restart crss.service

View file

@ -1,24 +0,0 @@
name: Lint Codebase
on:
push:
pull_request:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout The Repository
uses: actions/checkout@v3
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 22
- name: Install PNPM
uses: https://github.com/pnpm/action-setup@v3.0.0
with:
version: 9
- name: Lint
run: |
pnpm install
pnpm lint

View file

@ -0,0 +1,31 @@
name: Upload Website
on: [ push ]
jobs:
sftp:
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@v2
- name: Upload files via SFTP
uses: https://github.com/wangyucode/sftp-upload-action@v2.0.2
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USER }}
password: ${{ secrets.PASSWORD }}
forceUpload: false
localDir: '.'
remoteDir: '/var/www/crss'
exclude: '.*,.git*,.gitea*,LICENSE,README.md'
- name: Install dependencies over SSH
uses: https://github.com/nekiro/ssh-job@main
with:
host: ${{ secrets.HOST }}
user: ${{ secrets.USER }}
password: ${{ secrets.PASSWORD }}
command: |
cd /var/www/crss
composer install --no-dev --optimize-autoloader

121
.gitignore vendored
View file

@ -1,36 +1,103 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # Created by https://www.toptal.com/developers/gitignore/api/phpstorm+all,composer,dotenv
# Edit at https://www.toptal.com/developers/gitignore?templates=phpstorm+all,composer,dotenv
# dependencies ### Composer ###
/node_modules composer.phar
/.pnp /vendor/
.pnp.js
.yarn/install-state.gz
# testing # Commit your application's lock file https://getcomposer.org/doc/01-basic-usage.md#commit-your-composer-lock-file-to-version-control
/coverage # You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file
# composer.lock
# next.js ### dotenv ###
/.next/ .env
/out/
# production ### PhpStorm+all ###
/build # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# misc # User-specific stuff
.DS_Store .idea/**/workspace.xml
*.pem .idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# debug # AWS User-specific
npm-debug.log* .idea/**/aws.xml
yarn-debug.log*
yarn-error.log*
# local env files # Generated files
.env*.local .idea/**/contentModel.xml
# vercel # Sensitive or high-churn files
.vercel .idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# typescript # Gradle
*.tsbuildinfo .idea/**/gradle.xml
next-env.d.ts .idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### PhpStorm+all Patch ###
# Ignore everything but code style settings and run configurations
# that are supposed to be shared within teams.
.idea/*
!.idea/codeStyles
!.idea/runConfigurations
# End of https://www.toptal.com/developers/gitignore/api/phpstorm+all,composer,dotenv

View file

@ -1,5 +0,0 @@
{
"tabWidth": 2,
"singleQuote": true,
"semi": true
}

15
.vscode/launch.json vendored
View file

@ -1,15 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost:8080",
"webRoot": "${workspaceFolder}"
}
]
}

View file

@ -1,6 +0,0 @@
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}

View file

@ -1,17 +1,5 @@
# CRSS Server's Website # Website
This is the official website for the CRSS Server. > Newer version of the CRSS site written with PHP!
## Documentation # why did I choose php help me
### Contributing
tba
## License
```
This work is licensed under CC BY 4.0.
```
See the [LICENSE](LICENSE) file for more information.

32
_config.php Normal file
View file

@ -0,0 +1,32 @@
<?php
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
require __DIR__ . '/vendor/autoload.php';
require __DIR__ . '/util/Database.php';
require __DIR__ . '/util/Discord.php';
require __DIR__ . '/util/Admin.php';
use Twig\Loader\FilesystemLoader;
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();
$loader = new FilesystemLoader('template');
$twig = new Twig\Environment($loader);
$twig->addGlobal('discord_auth', $_ENV['DISCORD_OAUTH']);
$discord = new Discord(
$_ENV['DISCORD_CLIENT'],
$_ENV['DISCORD_SECRET'],
$_ENV['DISCORD_REDIRECT']
);
$mysql = new Database(
$_ENV['MYSQL_HOST'],
$_ENV['MYSQL_USER'],
$_ENV['MYSQL_PASS'],
$_ENV['MYSQL_DB']
);

49
auth.php Normal file
View file

@ -0,0 +1,49 @@
<?php
global $discord, $mysql;
require_once "_config.php";
session_start();
if(isset($_GET['code'])) {
$res = $discord->validateCode($_GET['code']);
if(!$res['error']) {
$_SESSION['access_token'] = $res['access_token'];
$_SESSION['refresh_token'] = $res['refresh_token'];
$_SESSION['expires_in'] = $res['expires_in'];
$guilds = $discord->getGuilds($res['access_token']);
$guildIds = array();
foreach ($guilds as $guild) {
$guildIds[] = $guild['id'];
}
if(!in_array('1127731341283307520', $guildIds) || !in_array('1195393418151596032', $guildIds)) {
echo json_encode(array(
'error' => true,
'error_description' => 'You are not in any of CRSS\'s guilds.'
));
} else {
$_SESSION['user'] = $discord->getUser($res['access_token']);
$mysql->createUserRecord($_SESSION['user']);
if (isset($_GET['state'])) {
header('Location: ' . $_GET['state']);
} else {
header('Location: /');
}
}
} else {
echo json_encode($res);
}
} else {
echo json_encode(array(
'error' => true,
'error_description' => 'No code provided.'
));
}

View file

@ -1,29 +0,0 @@
'use client';
import styles from '@/styles/AdBanner.module.scss';
import { Info } from 'lucide-react';
export default function AdBanner() {
return (
<div className={styles.container} style={{ display: 'flex', justifyContent: 'center', marginBottom: '1.5rem' }}>
<iframe id='a4962f15' name='a4962f15' src='https://ads.theclashfruit.me/www/delivery/afr.php?zoneid=3&amp;cb=75234' frameBorder='0' scrolling='no' width='468' height='60' allow='autoplay'></iframe>
</div>
);
/*
return (
<div className={styles.adBanner}>
<div className={styles.adContent}>
</div>
<div className={styles.adRevive}>
<iframe id='a4962f15' name='a4962f15' src='https://ads.theclashfruit.me/www/delivery/afr.php?zoneid=3&amp;cb=75234' frameBorder='0' scrolling='no' width='468' height='60' allow='autoplay'></iframe>
</div>
<button className={styles.adInfo}>
<Info />
</button>
</div>
);
*/
}

View file

@ -1,10 +0,0 @@
import styles from '@/styles/Card.module.scss';
export default function Card({ children, className }: { children: React.ReactNode, className?: string }) {
return (
<div className={`${styles.card} ${className}`}>
{ children }
</div>
);
}

View file

@ -1,51 +0,0 @@
import styles from '@/styles/Dropdown.module.scss';
import Link from 'next/link';
import { MouseEventHandler, useRef } from 'react';
export default function Dropdown({ children, items, className }: { children: React.ReactNode, items: { divider?: boolean, icon?: any, label?: string, href?: string, onClick?: MouseEventHandler<HTMLAnchorElement> }[], className: string }) {
const dropDownRef = useRef<HTMLDivElement>(null);
const handleClick = () => {
dropDownRef.current?.classList.toggle(styles.open);
};
return (
<div className={`${styles.dropDown} ${className}`} ref={dropDownRef}>
<label onClick={handleClick}>
{children}
</label>
<div className={styles.dropDownMenu}>
<ul>
{items.map((item, i) => (
(item.divider &&
<li key={i}>
<div className={styles.divider}></div>
</li>
) || (
<li key={i}>
{item.href ? (
<Link href={item.href!}>
{item.icon && <item.icon />}
{item.label}
</Link>
) : (
<a onClick={item.onClick}>
{item.icon && <item.icon />}
{item.label}
</a>
)}
</li>
)
))}
</ul>
</div>
<div className={styles.mobileOverlay} onClick={handleClick} />
</div>
);
}

View file

@ -1,70 +0,0 @@
import {
SiModrinth,
SiForgejo,
SiYoutube,
SiDiscord
} from '@icons-pack/react-simple-icons';
import Link from 'next/link';
import styles from '@/styles/Footer.module.scss';
import AdBanner from './AdBanner';
import getConfig from 'next/config';
export default function Footer() {
const { publicRuntimeConfig } = getConfig();
const git = publicRuntimeConfig.git;
return (
<>
<AdBanner />
<footer className={styles.pageFooter}>
<div className={styles.container}>
<div>
<p>
Copyright &copy; {new Date().getFullYear()} CRSS
</p>
<p>
Website originally designed by Myadeleines, heavily modified and rewritten by TheClashFruit.
</p>
</div>
<div>
<ul>
<li>
<Link href="https://modrinth.com/organization/crss" target="_blank">
<SiModrinth />
</Link>
</li>
<li>
<Link href="https://git.theclashfruit.me/crss" target="_blank">
<SiForgejo />
</Link>
</li>
<li>
<Link href="https://youtube.com/@CRSS666" target="_blank">
<SiYoutube />
</Link>
</li>
<li>
<Link href="https://discord.gg/rGjCKawPkS" target="_blank">
<SiDiscord />
</Link>
</li>
</ul>
<p>
CRSS/Website
<br />
{git.branch}@<a href={`https://git.theclashfruit.me/CRSS/Website/commit/${git.commit.sha}`}>{git.commit.sha.slice(0, 7)}</a>
</p>
</div>
</div>
</footer>
</>
);
}

View file

@ -1,61 +0,0 @@
import Head from 'next/head';
import { useRouter } from 'next/router';
export default function Meta({ page }: { page: { title: string, user?: any } }) {
const router = useRouter();
let ldJson: any = {
'@context': 'https://schema.org',
'@type': 'WebSite',
name: 'Clyde\'s Real Survival SMP',
alternateName: [ 'CRSS' ],
url: 'https://crss.cc'
};
if (page.user) {
ldJson = {
'@context': 'https://schema.org',
'@type': 'ProfilePage',
mainEntity: {
'@type': 'Person',
name: page.user.global_name,
alternateName: page.user.username,
identifier: page.user.did,
image: `https://cdn.discordapp.com/avatars/${page.user.did}/${page.user.avatar}.png`,
}
};
}
return (
<Head>
<title>{page.title} &bull; Clyde&apos;s Real Survival SMP</title>
<meta name="name" content={`${page.title} &bull; Clyde's Real Survival SMP`} />
<meta name="description" content="A very cool minecraft SMP that updates to every version starting from b1.0." />
<meta name="keywords" content="crss, minecraft, beta, alpha, release, new, 1.0, version" />
<meta name="theme-color" content="#537F53" />
<meta property="og:site_name" content="Clyde's Real Survival SMP" />
<meta property="og:title" content={page.title} />
<meta property="og:url" content={`https://crss.cc${router.asPath}`} />
<meta property="og:type" content="website" />
<meta property="og:locale" content="en_GB" />
<meta property="og:image" content="https://crss.fra1.cdn.digitaloceanspaces.com/img/social_image.png" />
<meta property="og:description" content="A very cool minecraft SMP that updates to every version starting from b1.0." />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={`${page.title} &bull; Clyde's Real Survival SMP`} />
<meta name="twitter:description" content="A very cool minecraft SMP that updates to every version starting from b1.0." />
<meta name="twitter:image" content="https://crss.fra1.cdn.digitaloceanspaces.com/img/social_image.png" />
<meta property="twitter:domain" content="crss.cc" />
<meta property="twitter:url" content={`https://crss.cc${router.asPath}`} />
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(ldJson) }} />
<link rel="canonical" href={`https://crss.cc${router.asPath}`} />
<link rel="icon" href="/favicon.ico" />
</Head>
);
}

View file

@ -1,273 +0,0 @@
import {
Menu,
Home,
Scale,
AtSign,
Images,
Map,
Gamepad,
User,
Settings,
LayoutDashboard,
LogOut,
LogIn,
Earth,
X,
PlusIcon,
Plus,
} from 'lucide-react';
import Link from 'next/link';
import styles from '@/styles/NavBar.module.scss';
import { getCookie } from '@/utils/cookies';
import Logo from '@/public/logo.svg';
import { useRouter } from 'next/router';
import getConfig from 'next/config';
import { useEffect, useRef, useState } from 'react';
import Dropdown from './Dropdown';
import { useUser } from '@/context/UserContext';
import { Permission, hasPermission } from '@/utils/permissions';
export default function NavBar({ currentPage }: { currentPage: string }) {
const { user, isLoggedIn } = useUser();
const [navOpen, setNavOpen] = useState(false);
const { publicRuntimeConfig } = getConfig();
const router = useRouter();
const server = {
version: '1.16.5',
};
const buildDiscordUrl = (): string => {
const url = new URL('https://discord.com/api/oauth2/authorize');
url.searchParams.append('client_id', publicRuntimeConfig.discord.clientId);
url.searchParams.append('response_type', 'code');
url.searchParams.append(
'redirect_uri',
publicRuntimeConfig.discord.redirectUri
);
url.searchParams.append(
'scope',
publicRuntimeConfig.discord.scopes.join(' ')
);
url.searchParams.append('state', router.asPath);
return url.toString();
};
return (
<>
<header className={styles.pageHero}>
<div className={styles.heroOverlay}>
<div className={styles.container}>
<div>
<Logo />
<h1>Clyde&apos;s Real Survival SMP</h1>
</div>
<div>
<label htmlFor="ip">Server Address:</label>
<input
type="text"
value="play.crss.cc"
id="ip"
readOnly
size={8}
/>
<label htmlFor="ip">Version: {server.version}</label>
</div>
</div>
</div>
</header>
<nav className={`${styles.navBar} ${navOpen ? styles.navOpen : ''}`}>
<div className={styles.container}>
<div className={styles.navMobileContainer}>
<button
className={styles.navToggle}
onClick={() => {
setNavOpen(!navOpen);
}}
>
{!navOpen ? <Menu /> : <X />}
</button>
</div>
<div className={styles.navCollapse}>
<ul>
<li>
<Link
href={currentPage == 'home' ? '#' : '/'}
className={currentPage == 'home' ? styles.active : ''}
>
<Home />
Home
</Link>
</li>
<li>
<Link
href={currentPage == 'rules' ? '#' : '/rules'}
className={currentPage == 'rules' ? styles.active : ''}
>
<Scale />
Rules
</Link>
</li>
<li>
<Link
href={currentPage == 'about' ? '#' : '/about'}
className={currentPage == 'about' ? styles.active : ''}
>
<AtSign />
About
</Link>
</li>
<li>
<Link
href={currentPage == 'gallery' ? '#' : '/gallery'}
className={currentPage == 'gallery' ? styles.active : ''}
>
<Images />
Gallery
</Link>
</li>
<li>
<Link
href={currentPage == 'map' ? '#' : '/map'}
className={currentPage == 'map' ? styles.active : ''}
>
<Map />
Map
</Link>
</li>
<li>
<Link
href={currentPage == 'nations' ? '#' : '/nations'}
className={currentPage == 'nations' ? styles.active : ''}
>
<Earth />
Nations
</Link>
</li>
</ul>
<ul>
{isLoggedIn && user && (
<li>
{(hasPermission(user.permissions, Permission.Admin) && (
<Dropdown
items={[
{
icon: User,
label: 'Profile',
href: `/u/${
user ? user.names.username : 'Loading...'
}`,
},
{
icon: Settings,
label: 'Settings',
href: '/settings',
},
{
icon: Plus,
label: 'Premium+',
href: '/pp',
},
{
divider: true,
},
{
icon: LayoutDashboard,
label: 'Admin',
href: '/admin',
},
{
icon: LogOut,
label: 'Logout',
onClick: async (e) => {
e.preventDefault();
await fetch('/api/v1/session', {
method: 'DELETE',
});
router.reload();
},
},
]}
className={styles.dropDown}
>
<User />
{user ? user.names.global_name : 'Loading...'}
</Dropdown>
)) || (
<Dropdown
items={[
{
icon: User,
label: 'Profile',
href: `/u/${
user ? user.names.username : 'Loading...'
}`,
},
{
icon: Settings,
label: 'Settings',
href: '/settings',
},
{
divider: true,
},
{
icon: LogOut,
label: 'Logout',
onClick: async (e) => {
e.preventDefault();
await fetch('/api/v1/session', {
method: 'DELETE',
});
router.reload();
},
},
]}
className={styles.dropDown}
>
<User />
{user ? user.names.global_name : 'Loading...'}
</Dropdown>
)}
</li>
)}
{!isLoggedIn && !user && (
<li>
<Link href={buildDiscordUrl()}>
<LogIn />
Login
</Link>
</li>
)}
</ul>
</div>
</div>
</nav>
</>
);
}

View file

@ -1,21 +0,0 @@
import { motion } from 'framer-motion';
import styles from '@/styles/global.module.scss';
export default function PageContent({ children }: { children: React.ReactNode }) {
return (
<motion.main
className={styles.pageContent}
initial={{ scale: 0.96, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.96, opacity: 0 }}
transition={{ duration: 0.24, ease: 'easeInOut', bounce: 0.45, type: 'spring', stiffness: 100, opacity: { bounce: 0 } }}
>
<div className={styles.container}>
{children}
</div>
</motion.main>
);
}

8
composer.json Normal file
View file

@ -0,0 +1,8 @@
{
"require": {
"twig/twig": "^3.0",
"anlutro/curl": "^1.5",
"vlucas/phpdotenv": "^5.5",
"bramus/router": "1.6"
}
}

654
composer.lock generated Normal file
View file

@ -0,0 +1,654 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "37fb0f4a910e4ba4add92cd9abbdf84a",
"packages": [
{
"name": "anlutro/curl",
"version": "1.5.2",
"source": {
"type": "git",
"url": "https://github.com/anlutro/php-curl.git",
"reference": "1c569768a106dd40f5160ebebb703f09c6b58c8b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/anlutro/php-curl/zipball/1c569768a106dd40f5160ebebb703f09c6b58c8b",
"reference": "1c569768a106dd40f5160ebebb703f09c6b58c8b",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"require-dev": {
"mockery/mockery": "0.9.*",
"phpunit/phpunit": "4.*"
},
"type": "library",
"autoload": {
"psr-4": {
"anlutro\\cURL\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Andreas Lutro",
"email": "anlutro@gmail.com"
}
],
"description": "Simple OOP cURL wrapper.",
"support": {
"issues": "https://github.com/anlutro/php-curl/issues",
"source": "https://github.com/anlutro/php-curl/tree/1.5.2"
},
"time": "2023-06-27T07:39:41+00:00"
},
{
"name": "bramus/router",
"version": "1.6",
"source": {
"type": "git",
"url": "https://github.com/bramus/router.git",
"reference": "d2cf97d5c471e272ac5a2a88b652bc75089c8ae3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/bramus/router/zipball/d2cf97d5c471e272ac5a2a88b652bc75089c8ae3",
"reference": "d2cf97d5c471e272ac5a2a88b652bc75089c8ae3",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "~2.14",
"phpunit/php-code-coverage": "~2.0",
"phpunit/phpunit": "~4.8"
},
"type": "library",
"autoload": {
"psr-0": {
"Bramus": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Bram(us) Van Damme",
"email": "bramus@bram.us",
"homepage": "http://www.bram.us"
}
],
"description": "A lightweight and simple object oriented PHP Router",
"homepage": "https://github.com/bramus/router",
"keywords": [
"router",
"routing"
],
"support": {
"issues": "https://github.com/bramus/router/issues",
"source": "https://github.com/bramus/router/tree/1.6"
},
"time": "2021-07-23T09:48:14+00:00"
},
{
"name": "graham-campbell/result-type",
"version": "v1.1.1",
"source": {
"type": "git",
"url": "https://github.com/GrahamCampbell/Result-Type.git",
"reference": "672eff8cf1d6fe1ef09ca0f89c4b287d6a3eb831"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/672eff8cf1d6fe1ef09ca0f89c4b287d6a3eb831",
"reference": "672eff8cf1d6fe1ef09ca0f89c4b287d6a3eb831",
"shasum": ""
},
"require": {
"php": "^7.2.5 || ^8.0",
"phpoption/phpoption": "^1.9.1"
},
"require-dev": {
"phpunit/phpunit": "^8.5.32 || ^9.6.3 || ^10.0.12"
},
"type": "library",
"autoload": {
"psr-4": {
"GrahamCampbell\\ResultType\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
}
],
"description": "An Implementation Of The Result Type",
"keywords": [
"Graham Campbell",
"GrahamCampbell",
"Result Type",
"Result-Type",
"result"
],
"support": {
"issues": "https://github.com/GrahamCampbell/Result-Type/issues",
"source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.1"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type",
"type": "tidelift"
}
],
"time": "2023-02-25T20:23:15+00:00"
},
{
"name": "phpoption/phpoption",
"version": "1.9.1",
"source": {
"type": "git",
"url": "https://github.com/schmittjoh/php-option.git",
"reference": "dd3a383e599f49777d8b628dadbb90cae435b87e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/schmittjoh/php-option/zipball/dd3a383e599f49777d8b628dadbb90cae435b87e",
"reference": "dd3a383e599f49777d8b628dadbb90cae435b87e",
"shasum": ""
},
"require": {
"php": "^7.2.5 || ^8.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"phpunit/phpunit": "^8.5.32 || ^9.6.3 || ^10.0.12"
},
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": true
},
"branch-alias": {
"dev-master": "1.9-dev"
}
},
"autoload": {
"psr-4": {
"PhpOption\\": "src/PhpOption/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "Johannes M. Schmitt",
"email": "schmittjoh@gmail.com",
"homepage": "https://github.com/schmittjoh"
},
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
}
],
"description": "Option Type for PHP",
"keywords": [
"language",
"option",
"php",
"type"
],
"support": {
"issues": "https://github.com/schmittjoh/php-option/issues",
"source": "https://github.com/schmittjoh/php-option/tree/1.9.1"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption",
"type": "tidelift"
}
],
"time": "2023-02-25T19:38:58+00:00"
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.28.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb",
"reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"provide": {
"ext-ctype": "*"
},
"suggest": {
"ext-ctype": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.28-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Ctype\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Gert de Pagter",
"email": "BackEndTea@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for ctype functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"ctype",
"polyfill",
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.28.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2023-01-26T09:26:14+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.28.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "42292d99c55abe617799667f454222c54c60e229"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229",
"reference": "42292d99c55abe617799667f454222c54c60e229",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"provide": {
"ext-mbstring": "*"
},
"suggest": {
"ext-mbstring": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.28-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Mbstring\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for the Mbstring extension",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"mbstring",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2023-07-28T09:04:16+00:00"
},
{
"name": "symfony/polyfill-php80",
"version": "v1.28.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
"reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/6caa57379c4aec19c0a12a38b59b26487dcfe4b5",
"reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.28-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Php80\\": ""
},
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ion Bazan",
"email": "ion.bazan@gmail.com"
},
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php80/tree/v1.28.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2023-01-26T09:26:14+00:00"
},
{
"name": "twig/twig",
"version": "v3.7.1",
"source": {
"type": "git",
"url": "https://github.com/twigphp/Twig.git",
"reference": "a0ce373a0ca3bf6c64b9e3e2124aca502ba39554"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/a0ce373a0ca3bf6c64b9e3e2124aca502ba39554",
"reference": "a0ce373a0ca3bf6c64b9e3e2124aca502ba39554",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"symfony/polyfill-ctype": "^1.8",
"symfony/polyfill-mbstring": "^1.3"
},
"require-dev": {
"psr/container": "^1.0|^2.0",
"symfony/phpunit-bridge": "^5.4.9|^6.3"
},
"type": "library",
"autoload": {
"psr-4": {
"Twig\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com",
"homepage": "http://fabien.potencier.org",
"role": "Lead Developer"
},
{
"name": "Twig Team",
"role": "Contributors"
},
{
"name": "Armin Ronacher",
"email": "armin.ronacher@active-4.com",
"role": "Project Founder"
}
],
"description": "Twig, the flexible, fast, and secure template language for PHP",
"homepage": "https://twig.symfony.com",
"keywords": [
"templating"
],
"support": {
"issues": "https://github.com/twigphp/Twig/issues",
"source": "https://github.com/twigphp/Twig/tree/v3.7.1"
},
"funding": [
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/twig/twig",
"type": "tidelift"
}
],
"time": "2023-08-28T11:09:02+00:00"
},
{
"name": "vlucas/phpdotenv",
"version": "v5.5.0",
"source": {
"type": "git",
"url": "https://github.com/vlucas/phpdotenv.git",
"reference": "1a7ea2afc49c3ee6d87061f5a233e3a035d0eae7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/1a7ea2afc49c3ee6d87061f5a233e3a035d0eae7",
"reference": "1a7ea2afc49c3ee6d87061f5a233e3a035d0eae7",
"shasum": ""
},
"require": {
"ext-pcre": "*",
"graham-campbell/result-type": "^1.0.2",
"php": "^7.1.3 || ^8.0",
"phpoption/phpoption": "^1.8",
"symfony/polyfill-ctype": "^1.23",
"symfony/polyfill-mbstring": "^1.23.1",
"symfony/polyfill-php80": "^1.23.1"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.4.1",
"ext-filter": "*",
"phpunit/phpunit": "^7.5.20 || ^8.5.30 || ^9.5.25"
},
"suggest": {
"ext-filter": "Required to use the boolean validator."
},
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": true
},
"branch-alias": {
"dev-master": "5.5-dev"
}
},
"autoload": {
"psr-4": {
"Dotenv\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
},
{
"name": "Vance Lucas",
"email": "vance@vancelucas.com",
"homepage": "https://github.com/vlucas"
}
],
"description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.",
"keywords": [
"dotenv",
"env",
"environment"
],
"support": {
"issues": "https://github.com/vlucas/phpdotenv/issues",
"source": "https://github.com/vlucas/phpdotenv/tree/v5.5.0"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv",
"type": "tidelift"
}
],
"time": "2022-10-16T01:01:54+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": [],
"platform-dev": [],
"plugin-api-version": "2.6.0"
}

View file

@ -1,50 +0,0 @@
import { getCookie } from '@/utils/cookies';
import getConfig from 'next/config';
import { createContext, useContext, useEffect, useState } from 'react';
interface UserContextType {
user: any;
isLoggedIn: boolean;
}
const UserContext = createContext<UserContextType>({ user: null, isLoggedIn: false });
export const UserProvider = ({ children }: { children: React.ReactNode }) => {
const [ user, setUser ] = useState(null);
const [ isLoggedIn, setIsLoggedIn ] = useState(false);
const { publicRuntimeConfig } = getConfig();
useEffect(() => {
const fetchUser = async () => {
const sessionCookie = getCookie('session');
if (sessionCookie) {
try {
const res = await fetch('/api/v1/user/@me');
if (res.ok) {
const userData = await res.json();
setIsLoggedIn(true);
setUser(userData);
} else {
document.cookie = 'session=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT';
}
} catch (error) {
console.error('Error fetching user data:', error);
}
}
};
fetchUser();
}, []);
return (
<UserContext.Provider value={{ user, isLoggedIn }}>
{children}
</UserContext.Provider>
);
};
export const useUser = () => useContext(UserContext);

1
css/map.min.css vendored Normal file
View file

@ -0,0 +1 @@
*{padding:0;margin:0;box-sizing:border-box}#map{height:100vh}#homeLink{position:absolute;bottom:10px;left:10px;padding:5px 10px;z-index:400;background-color:#fff;background-clip:padding-box;border:2px solid rgba(0,0,0,.2);border-radius:5px;text-decoration:none;color:#000}img.leaflet-tile,img.leaflet-marker-icon{image-rendering:pixelated}.leaflet-control{color:#333}.leaflet-control-mouseposition{background:rgba(255,255,255,.8);margin:0 !important;padding:6px;width:100%;text-align:end;border-top-left-radius:5px}.leaflet-marker-icon{transition:300ms}/*# sourceMappingURL=map.min.css.map */

1
css/map.min.css.map Normal file
View file

@ -0,0 +1 @@
{"version":3,"sourceRoot":"","sources":["src/map.scss"],"names":[],"mappings":"AAAA,EACE,UACA,SAEA,sBAKF,kBAEA,UACE,kBACA,YACA,UACA,iBACA,YAEA,sBACA,4BAEA,gCACA,kBAEA,qBACA,WAGF,yCACE,0BAGF,iBACE,WAGF,+BACE,gCAEA,oBACA,YAEA,WAEA,eAEA,2BAGF,qBACE","file":"map.min.css"}

18
css/src/_fonts.scss Normal file
View file

@ -0,0 +1,18 @@
// @import url('https://fonts.googleapis.com/css2?family=Noto+Color+Emoji&family=Noto+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&family=Outfit:wght@100;200;300;400;500;600;700;800;900&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Comic+Neue:wght@700&family=Noto+Sans:ital,wght@0,400;0,700;1,400;1,700&family=Outfit:wght@400;500;700&display=swap');
body, input {
font-family: "Noto Sans", "Noto Color Emoji", sans-serif;
}
h1, h2, h3, h4, h5, h6 {
font-family: "Outfit", "Noto Color Emoji", sans-serif;
font-weight: 500;
//margin-bottom: 6px;
}
.pageHero {
font-family: "Comic Neue", "Comic Sans MS", "Noto Color Emoji", sans-serif;
//margin-bottom: 6px;
}

28
css/src/colors/_dark.scss Normal file
View file

@ -0,0 +1,28 @@
// Dark Theme
$pageBG: #202120;
$pageFG: white;
$accent: #527D52;
$linkColor: #d0dfd0;
$btnNormalBG: $accent;
$btnNormalFG: white;
$btnActiveBG: $accent;
$btnActiveFG: white;
$navBG: #272f27;
$navBorder: #575f57;
$cardNormalBG: #272727;
$cardNormalFG: $pageFG;
$cardNormalBorder: #575757;
$cardActiveBG: $navBG;
$cardActiveFG: $linkColor;
$cardActiveBorder: $navBorder;
$navLinkOutlineColor: #707f70;
$navLinkNormalColor: #7E9E7E;
$navLinkActiveColor: #DEFEDE;
$headerOverlay: #101610;

View file

@ -0,0 +1,32 @@
// Light Theme
$pageBG: white;
$pageFG: black;
$accent: #527D52;
$linkColor: $accent;
$btnNormalBG: $accent;
$btnNormalFG: white;
$btnActiveBG: $accent;
$btnActiveFG: white;
$navBG: #f9fff9;
$navBorder: #d0dfd0;
$cardNormalBG: #f9f9f9;
$cardNormalFG: $pageFG;
$cardNormalBorder: #d0d0d0;
$cardActiveBG: $navBG;
$cardActiveFG: $linkColor;
$cardActiveBorder: $navBorder;
/*$cardBG: $navBG;
$cardFG: $pageFG;
$cardBorder: $navBorder;*/
$navLinkOutlineColor: #D0DFD0;
$navLinkNormalColor: #7E9E7E;
$navLinkActiveColor: $accent;
$headerOverlay: white;

52
css/src/map.scss Normal file
View file

@ -0,0 +1,52 @@
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}
/// old
#map { height: 100vh; }
#homeLink {
position: absolute;
bottom: 10px;
left: 10px;
padding: 5px 10px;
z-index: 400;
background-color: #fff;
background-clip: padding-box;
border: 2px solid rgba(0,0,0,0.2);
border-radius: 5px;
text-decoration: none;
color: #000;
}
img.leaflet-tile, img.leaflet-marker-icon {
image-rendering: pixelated;
}
.leaflet-control {
color: #333;
}
.leaflet-control-mouseposition {
background: rgba(255, 255, 255, 0.8);
margin: 0 !important;
padding: 6px;
width: 100%;
text-align: end;
border-top-left-radius: 5px;
}
.leaflet-marker-icon {
transition: 300ms;
}

448
css/src/style.scss Normal file
View file

@ -0,0 +1,448 @@
@use "fonts";
@use "colors/light" as light;
@use "colors/dark" as dark;
* {
-webkit-tap-highlight-color: transparent; // fuck you (L)
}
html, body {
overflow-x: hidden;
}
body {
margin: 0;
background: light.$pageBG;
color: light.$pageFG;
@media (prefers-color-scheme: dark) {
background: dark.$pageBG;
color: dark.$pageFG;
}
}
main {
transition: 0.24s;
&.buffering {
transform: scale(1.01);
transition: 0.16s;
opacity: 0.4;
pointer-events: none;
user-select: none;
}
&.transition {
transform: scale(0.96);
transition: 0.16s;
opacity: 0;
}
}
.container {
max-width: 960px;
width: 100%;
margin: 0 auto;
padding: 24px;
box-sizing: border-box;
}
ul, ol {
list-style-position: inside;
}
p {
line-height: 1.6;
opacity: 0.88;
}
img {
max-width: 100%;
}
.cards {
display: flex;
flex-wrap: wrap;
gap: 8px;
width: 100%;
@media (max-width: 600px) {
gap: 12px;
}
}
.card {
position: relative;
display: flex;
flex-direction: column;
box-sizing: border-box;
max-width: 100%;
height: 150px;
text-decoration: none;
border-radius: 4px;
padding: 8px 12px;
cursor: pointer;
overflow: hidden;
outline: 1px solid light.$cardNormalBorder;
background: light.$cardNormalBG;
color: light.$cardNormalFG;
@media (prefers-color-scheme: dark) {
outline-color: dark.$cardNormalBorder;
background: dark.$cardNormalBG;
color: dark.$cardNormalFG;
}
transition: 0.24s;
&:hover {
outline-width: 2px;
outline-color: light.$cardActiveBorder;
background: light.$cardActiveBG;
color: light.$cardActiveFG;
@media (prefers-color-scheme: dark) {
outline-color: dark.$cardActiveBorder;
background: dark.$cardActiveBG;
color: dark.$cardActiveFG;
}
transition: 0.08s;
.icon {
transition: 0.48s;
transform: scale(1.2);
}
}
width: calc(100% / 3 - 6px);
@media (max-width: 800px) {
width: calc(50% - 4px);
}
@media (max-width: 600px) {
border-radius: 8px;
width: 100%;
}
h1 {
margin: 0;
font-size: 1.2em;
font-weight: 400;
}
.icon {
position: absolute;
top: 0;
right: 0;
height: 100%;
aspect-ratio: 1/1;
object-fit: cover;
mask-image: linear-gradient(to right, transparent, rgba(red, 0.3));
@media (prefers-color-scheme: dark) {
mask-image: linear-gradient(to right, transparent, rgba(red, 0.1));
}
transition: 0.64s;
}
p {
margin: 8px 0;
}
}
a {
color: light.$linkColor;
@media (prefers-color-scheme: dark) {
color: dark.$linkColor;
}
&:visited {
opacity: 0.9;
}
}
.pageHero {
background-image: linear-gradient(rgba(light.$headerOverlay, 0.9), rgba(light.$headerOverlay, 0.9)), // white overlay. yeah that's a bit of an ugly hack?
url('/img/Panorama-Lens-Blur.png');
@media (prefers-color-scheme: dark) {
background-image: linear-gradient(rgba(dark.$headerOverlay, 0.9), rgba(dark.$headerOverlay, 0.9)),
url('/img/Panorama-Lens-Blur.png');
}
background-position: center;
background-size: cover;
height: 220px;
> .container {
height: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
@media (max-width: 600px) {
justify-content: center;
}
> div {
display: flex;
flex-direction: column;
gap: 12px;
&.Branding {
align-items: flex-start;
@media (max-width: 600px) {
align-items: center;
}
@media (prefers-color-scheme: dark) {
img {
filter: invert(1)
}
}
span {
font-size: 24px;
opacity: 0.75;
}
}
&.Server-Information {
@media (max-width: 600px) {
display: none;
}
align-items: center;
gap: 4px;
input {
text-align: center;
font-size: 24px;
&:not(:hover, :focus) {
color: inherit;
border-color: transparent;
background: transparent;
}
}
}
}
}
}
.navToggle {
@media (min-width: 601px) {
display: none;
}
z-index: 10;
position: fixed;
top: 8px;
right: 8px;
font-size: 18px;
background: light.$btnNormalBG;
color: light.$btnNormalFG;
@media (prefers-color-scheme: dark) {
background: dark.$btnNormalBG;
color: dark.$btnNormalFG;
}
padding: 8px 24px;
border-radius: 32px;
font-weight: bold;
user-select: none;
cursor: pointer;
}
.pageNav {
width: 100%;
border: solid light.$navBorder;
border-width: 0 0 1px 0;
background: light.$navBG;
@media (prefers-color-scheme: dark) {
border-color: dark.$navBorder;
background: dark.$navBG;
}
> .container {
padding: 16px 24px;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
@media (max-width: 600px) {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 9;
backdrop-filter: blur(15px);
background: rgba(light.$navBG, 0.86);
@media (prefers-color-scheme: dark) {
background: rgba(dark.$navBG, 0.9);
}
justify-content: center;
flex-direction: column;
gap: 64px;
&, > * {
transition: 0.16s
}
&:not(.opened) {
opacity: 0;
pointer-events: none;
//&, > * { transition: 0.12s }
> * {
transform: translateX(32px);
}
}
}
> div {
margin: 0;
display: flex;
flex-wrap: wrap;
align-items: flex-end;
gap: 6px;
@media (max-width: 600px) {
width: 100%;
flex-direction: column;
}
> a {
font-family: Outfit;
user-select: none;
font-size: 16px;
box-sizing: border-box;
color: light.$navLinkNormalColor;
@media (prefers-color-scheme: dark) {
color: dark.$navLinkNormalColor;
}
@media (max-width: 600px) {
font-size: 28px;
outline: none;
padding: 8px 32px;
text-align: end;
&.active::before {
content: ""; // i know..
}
}
text-decoration: none;
padding: 6px 20px;
border-radius: 32px;
outline: solid transparent 2px;
transition: 0.24s;
&.active {
pointer-events: none;
}
&:hover, &.active {
//background: rgba(15, 23, 42, 0.1);
color: light.$navLinkActiveColor;
outline-color: light.$navLinkOutlineColor;
@media (prefers-color-scheme: dark) {
color: dark.$navLinkActiveColor;
outline-color: dark.$navLinkOutlineColor;
}
transition: 0.08s;
}
@media (min-width: 601px) {
&.buttonPrimary {
background: light.$btnNormalBG;
color: light.$btnNormalFG;
@media (prefers-color-scheme: dark) {
background: dark.$btnNormalBG;
color: dark.$btnNormalFG;
}
font-weight: 700 !important;
}
}
}
}
}
}
.pageContent {
//
}
.pageFooter {
opacity: 0.8;
> .SNS-Links {
display: flex;
flex-wrap: wrap;
gap: 16px;
> a {
display: flex;
gap: 8px;
font-size: 18px;
color: inherit;
img {
width: 1em;
@media (prefers-color-scheme: dark) {
filter: invert(1);
}
}
&:not(:hover, :focus) {
text-decoration: none;
}
}
}
}

1
css/style.min.css vendored Normal file

File diff suppressed because one or more lines are too long

1
css/style.min.css.map Normal file
View file

@ -0,0 +1 @@
{"version":3,"sourceRoot":"","sources":["src/_fonts.scss","src/style.scss","src/colors/_light.scss","src/colors/_dark.scss"],"names":[],"mappings":"CAEQ,qKAER,WACE,sDAGF,kBACE,mDACA,gBAIF,UACE,uECXF,EACE,0CAGF,UACE,kBAGF,KACE,SAEA,WCbO,KDcP,MCbO,KDeP,mCANF,KAOI,WEjBK,QFkBL,MEjBK,MFqBT,KACE,gBAEA,eACE,sBACA,gBACA,WACA,oBACA,iBAGF,gBACE,sBACA,gBACA,UAIJ,WACE,gBAEA,WAEA,cAEA,aACA,sBAGF,MACE,2BAGF,EACE,gBACA,YAGF,IACE,eAGF,OACE,aACA,eACA,QACA,WAEA,yBANF,OAOI,UAIJ,MACE,kBACA,aACA,sBACA,sBACA,eACA,aACA,qBACA,kBACA,iBACA,eACA,gBAEA,0BACA,WC3Ea,QD4Eb,MCzFO,KDiGP,gBAsBA,iCA5BA,mCAjBF,MAkBI,cE7Ee,QF8Ef,WEhFW,QFiFX,ME9FK,MFmGP,YACE,kBACA,cC1FQ,QD2FR,WC5FI,QD6FJ,MCrGK,QD6GL,gBANA,mCANF,YAOI,cE/FM,QFgGN,WEjGE,QFkGF,MEzGM,SF8GR,kBACE,gBACA,qBAMJ,yBA/CF,MAgDI,uBAGF,yBAnDF,MAoDI,kBACA,YAGF,SACE,SACA,gBACA,gBAGF,YACE,kBACA,MACA,QACA,YACA,iBACA,iBAEA,wEAKA,gBAJA,mCATF,YAUI,yEAMJ,QACE,aAIJ,EACE,MC5JO,QD8JP,mCAHF,EAII,ME9JQ,SFiKV,UACE,WAIJ,UACE,wHAQA,2BACA,sBAEA,aARA,mCAJF,UAKI,mHASF,qBACE,YAEA,aACA,mBAEA,mBACA,8BAEA,yBATF,qBAUI,wBAGF,yBACE,aACA,sBAEA,SAEA,kCACE,uBAEA,yBAHF,kCAII,oBAGF,mCACE,sCACE,kBAIJ,uCACE,eACA,YAIJ,4CAKE,mBACA,QALA,yBADF,4CAEI,cAMF,kDACE,kBACA,eAEA,qEACE,cACA,2BACA,yBAQZ,WAIE,WAEA,eACA,QACA,UACA,eAEA,WC7PO,QD8PP,MC1PY,KDiQZ,iBAEA,mBAEA,iBACA,iBAEA,eAzBA,yBADF,WAEI,cAYF,mCAdF,WAeI,WEjQK,QFkQL,ME9PU,MF2Qd,SACE,WAEA,qBACA,uBAEA,WC7QM,QD+QN,mCARF,SASI,aE/QQ,QFgRR,WEjRI,SFoRN,oBACE,kBAEA,aACA,eAEA,mBACA,8BAEA,yBATF,oBAUI,eACA,MACA,OACA,WACA,YACA,UAEA,2BACA,iCAKA,uBACA,sBACA,UANA,yDAnBJ,oBAoBM,8BAXJ,yBAkBE,0CACE,gBAGF,iCACE,UACA,oBAGA,mCACE,4BAKN,wBACE,SAEA,aACA,eACA,qBAEA,QAEA,yBATF,wBAUI,WACA,uBAGF,0BACE,mBACA,iBAEA,eACA,sBACA,MCnUa,QDoVb,qBAEA,iBAEA,mBAEA,gCAEA,gBAxBA,mCAPF,0BAQI,MEzUW,SF4Ub,yBAXF,0BAYI,eACA,aACA,iBACA,eAEA,yCACE,aAeJ,iCACE,oBAGF,iEAEE,MC3XD,QD4XC,cCtWY,QD8WZ,gBANA,mCALF,iEAMI,ME3WS,QF4WT,cE9WU,SFqXd,yBACE,wCACE,WCzYH,QD0YG,MCtYE,KD6YF,4BALA,yDAJF,wCAKI,WE7YL,QF8YK,ME1YA,MFyZd,YACE,WAEA,uBACE,aACA,eACA,SAEA,yBACE,aACA,QAEA,eAEA,cAEA,6BACE,UAEA,mCAHF,6BAII,kBAIJ,4CACE","file":"style.min.css"}

View file

@ -1,28 +0,0 @@
# The CRSS Mod <-> CRSS Website Communication API
A layer bellow the website api to retrieve and send data to the CRSS server. For example a user requests `https://crss.cc/api/v1/server/players`, the website api will call the crss mod api to get the data from the server.
It will use not use http for reasons.
## Packets
### Get Server Info
#### Client -> Server
| Identifier | Lenght |
|------------|-----------------------|
| `0x00` | `0x00 0x00 0x00 0x00` |
* Identifier (1 Byte) - `0x00`
* Length (4 Bytes) - `UInt32`
#### Server -> Client
| Identifier | Lenght | Data |
|------------|-----------------------|-----------|
| `0x00` | `0x00 0x00 0x00 0x00` | `{ ... }` |
* Identifier (1 Byte) - `0x00`
* Length (4 Bytes) - `UInt32`
* Data (*Lenght) - `String` - JSON Object

View file

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 101 KiB

3
fiveserver.config.js Normal file
View file

@ -0,0 +1,3 @@
module.exports = {
php: "/usr/bin/php"
}

20
img/CRSS-Logo.svg Normal file
View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="146px" height="96px" viewBox="0 0 146 96" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg">
<defs>
<path d="M146 0L146 0L146 96L0 96L0 0L146 0Z" id="path_1" />
<clipPath id="clip_1">
<use xlink:href="#path_1" clip-rule="evenodd" fill-rule="evenodd" />
</clipPath>
</defs>
<g id="Frame" clip-path="url(#clip_1)">
<path d="M146 0L146 0L146 96L0 96L0 0L146 0Z" id="Frame" fill="none" stroke="none" />
<path d="M83.7827 12.6087C83.7827 10.6157 85.3984 9 87.3914 9C89.3844 9 91.0001 10.6157 91.0001 12.6087C91.0001 14.6017 89.3844 16.2174 87.3914 16.2174C85.3984 16.2174 83.7827 14.6017 83.7827 12.6087Z" id="Oval-5" fill="#000000" fill-rule="evenodd" stroke="none" />
<path d="M32.6594 16.4528L41.9008 32.5134L50.8527 17.4646L54.8404 29.75L55.9553 28.8478L66.5686 10.2029" id="Vector-3" fill="none" fill-rule="evenodd" stroke="#000000" stroke-width="5" stroke-linecap="round" stroke-linejoin="round" />
<path d="M8 19.8261C8 17.8331 9.61567 16.2174 11.6087 16.2174C13.6017 16.2174 15.2174 17.8331 15.2174 19.8261C15.2174 21.8191 13.6017 23.4348 11.6087 23.4348C9.61567 23.4348 8 21.8191 8 19.8261Z" id="Oval-6" fill="#000000" fill-rule="evenodd" stroke="none" />
<g id="CRSS" transform="translate(0 32)">
<g id="CRSS">
<path d="M21.25 57.9906C27.4531 57.9906 33.0938 55.3656 36.6094 50.3656C37.0625 49.725 37.1875 49.0844 37.1875 48.4438C37.1875 46.3969 35.6406 45.2406 33.9844 45.2406C33.0312 45.2406 32.25 45.5063 31.4844 46.5219C29.4375 49.2094 26.2969 51.8344 21.25 51.8344C14.5938 51.8344 8.95312 47.0375 8.95312 38.725C8.95312 29.4438 15.4844 20.5375 22.0781 20.5375C25.5312 20.5375 27.5156 21.6313 28.8594 23.0375L28.8594 23.4906C28.8594 24.8344 30.1406 26.0531 31.875 26.0531C33.6562 26.0531 35.0781 24.5688 35.0781 22.85L35.0781 18.7563C35.0781 17.1469 33.8594 15.8031 32.125 15.8031C31.2344 15.8031 30.5938 16.1938 30.2031 16.5688C28.2812 15.4281 25.4062 14.3344 22.5312 14.3344C12.7344 14.3344 2.5625 25.9125 2.5625 38.725C2.5625 50.8813 11.0781 57.9906 21.25 57.9906ZM46.4015 57.9906C48.0578 57.9906 49.5421 56.6469 49.5421 54.85L49.5421 43.2563C51.0734 43.0063 52.6046 42.8813 53.8234 42.8813C56.0578 42.8813 60.9953 43.6469 68.9328 56.3188C69.5734 57.35 70.5265 57.9906 71.7453 57.9906C73.6671 57.9906 74.8859 56.4438 74.8859 54.85C74.8859 54.3969 74.8234 53.8813 74.5578 53.3813C71.3546 46.975 66.9484 42.7563 63.9328 40.3813C68.9953 38.1469 73.4015 34.4281 73.4015 27.975C73.4015 19.3344 65.339 14.3344 51.0109 14.3344L48.3859 14.3344C43.964 14.3344 43.2609 14.9125 43.2609 18.1781L43.2609 54.85C43.2609 56.6469 44.6046 57.9906 46.4015 57.9906ZM49.5421 37.3813L49.5421 20.475L50.8234 20.475C61.0578 20.475 67.0734 23.0375 67.0734 27.975C67.0734 34.1156 57.339 36.8656 49.5421 37.3813ZM93.0573 57.9906C101.37 57.9906 108.479 52.475 108.479 45.1156C108.479 38.85 103.807 34.8188 95.3541 32.3813C88.4479 30.3969 85.5104 28.5375 85.5104 25.5375C85.5104 22.3344 88.4479 20.225 94.276 20.225C99.0104 20.225 101.885 22.0844 102.214 24.3813C102.401 25.85 103.62 27.0688 105.151 27.0688C106.948 27.0688 108.292 25.6625 108.292 23.7406C108.292 18.1156 103.104 14.3344 94.276 14.3344C84.9948 14.3344 79.2916 19.1313 79.2916 25.9906C79.2916 33.2875 85.8854 36.4125 93.1823 38.4594C100.229 40.4438 102.339 42.8188 102.339 45.4438C102.339 49.0844 98.5573 51.975 93.1823 51.975C89.3385 51.975 84.9323 50.1781 83.526 46.2719C83.0104 44.8656 81.9166 43.8969 80.3229 43.8969C78.4635 43.8969 77.0573 45.3813 77.0573 47.1C77.0573 47.8031 77.3073 48.6469 77.7604 49.6C79.8073 53.9594 85.2448 57.9906 93.0573 57.9906ZM128.001 57.9906C136.314 57.9906 143.423 52.475 143.423 45.1156C143.423 38.85 138.751 34.8188 130.298 32.3813C123.392 30.3969 120.454 28.5375 120.454 25.5375C120.454 22.3344 123.392 20.225 129.22 20.225C133.954 20.225 136.829 22.0844 137.158 24.3813C137.345 25.85 138.564 27.0688 140.095 27.0688C141.892 27.0688 143.236 25.6625 143.236 23.7406C143.236 18.1156 138.048 14.3344 129.22 14.3344C119.939 14.3344 114.236 19.1313 114.236 25.9906C114.236 33.2875 120.829 36.4125 128.126 38.4594C135.173 40.4438 137.283 42.8188 137.283 45.4438C137.283 49.0844 133.501 51.975 128.126 51.975C124.283 51.975 119.876 50.1781 118.47 46.2719C117.954 44.8656 116.861 43.8969 115.267 43.8969C113.408 43.8969 112.001 45.3813 112.001 47.1C112.001 47.8031 112.251 48.6469 112.704 49.6C114.751 53.9594 120.189 57.9906 128.001 57.9906Z" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

1
img/Discord.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 127.14 96.36"><path d="M107.7,8.07A105.15,105.15,0,0,0,81.47,0a72.06,72.06,0,0,0-3.36,6.83A97.68,97.68,0,0,0,49,6.83,72.37,72.37,0,0,0,45.64,0,105.89,105.89,0,0,0,19.39,8.09C2.79,32.65-1.71,56.6.54,80.21h0A105.73,105.73,0,0,0,32.71,96.36,77.7,77.7,0,0,0,39.6,85.25a68.42,68.42,0,0,1-10.85-5.18c.91-.66,1.8-1.34,2.66-2a75.57,75.57,0,0,0,64.32,0c.87.71,1.76,1.39,2.66,2a68.68,68.68,0,0,1-10.87,5.19,77,77,0,0,0,6.89,11.1A105.25,105.25,0,0,0,126.6,80.22h0C129.24,52.84,122.09,29.11,107.7,8.07ZM42.45,65.69C36.18,65.69,31,60,31,53s5-12.74,11.43-12.74S54,46,53.89,53,48.84,65.69,42.45,65.69Zm42.24,0C78.41,65.69,73.25,60,73.25,53s5-12.74,11.44-12.74S96.23,46,96.12,53,91.08,65.69,84.69,65.69Z"/></svg>

After

Width:  |  Height:  |  Size: 749 B

3
img/Mastodon.svg Normal file
View file

@ -0,0 +1,3 @@
<svg width="74" height="79" viewBox="0 0 74 79" fill="black" xmlns="http://www.w3.org/2000/svg">
<path d="M73.7014 17.4323C72.5616 9.05152 65.1774 2.4469 56.424 1.1671C54.9472 0.950843 49.3518 0.163818 36.3901 0.163818H36.2933C23.3281 0.163818 20.5465 0.950843 19.0697 1.1671C10.56 2.41145 2.78877 8.34604 0.903306 16.826C-0.00357854 21.0022 -0.100361 25.6322 0.068112 29.8793C0.308275 35.9699 0.354874 42.0498 0.91406 48.1156C1.30064 52.1448 1.97502 56.1419 2.93215 60.0769C4.72441 67.3445 11.9795 73.3925 19.0876 75.86C26.6979 78.4332 34.8821 78.8603 42.724 77.0937C43.5866 76.8952 44.4398 76.6647 45.2833 76.4024C47.1867 75.8033 49.4199 75.1332 51.0616 73.9562C51.0841 73.9397 51.1026 73.9184 51.1156 73.8938C51.1286 73.8693 51.1359 73.8421 51.1368 73.8144V67.9366C51.1364 67.9107 51.1302 67.8852 51.1186 67.862C51.1069 67.8388 51.0902 67.8184 51.0695 67.8025C51.0489 67.7865 51.0249 67.7753 50.9994 67.7696C50.9738 67.764 50.9473 67.7641 50.9218 67.7699C45.8976 68.9569 40.7491 69.5519 35.5836 69.5425C26.694 69.5425 24.3031 65.3699 23.6184 63.6327C23.0681 62.1314 22.7186 60.5654 22.5789 58.9744C22.5775 58.9477 22.5825 58.921 22.5934 58.8965C22.6043 58.8721 22.621 58.8505 22.6419 58.8336C22.6629 58.8167 22.6876 58.8049 22.714 58.7992C22.7404 58.7934 22.7678 58.794 22.794 58.8007C27.7345 59.9796 32.799 60.5746 37.8813 60.5733C39.1036 60.5733 40.3223 60.5733 41.5447 60.5414C46.6562 60.3996 52.0437 60.1408 57.0728 59.1694C57.1983 59.1446 57.3237 59.1233 57.4313 59.0914C65.3638 57.5847 72.9128 52.8555 73.6799 40.8799C73.7086 40.4084 73.7803 35.9415 73.7803 35.4523C73.7839 33.7896 74.3216 23.6576 73.7014 17.4323ZM61.4925 47.3144H53.1514V27.107C53.1514 22.8528 51.3591 20.6832 47.7136 20.6832C43.7061 20.6832 41.6988 23.2499 41.6988 28.3194V39.3803H33.4078V28.3194C33.4078 23.2499 31.3969 20.6832 27.3894 20.6832C23.7654 20.6832 21.9552 22.8528 21.9516 27.107V47.3144H13.6176V26.4937C13.6176 22.2395 14.7157 18.8598 16.9118 16.3545C19.1772 13.8552 22.1488 12.5719 25.8373 12.5719C30.1064 12.5719 33.3325 14.1955 35.4832 17.4394L37.5587 20.8853L39.6377 17.4394C41.7884 14.1955 45.0145 12.5719 49.2765 12.5719C52.9614 12.5719 55.9329 13.8552 58.2055 16.3545C60.4017 18.8574 61.4997 22.2371 61.4997 26.4937L61.4925 47.3144Z" fill="inherit"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
img/Panorama-Lens-Blur.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 819 KiB

3
img/Youtube.svg Normal file
View file

@ -0,0 +1,3 @@
<svg width="72" height="52" viewBox="0 0 72 52" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M28.8125 36.7812L47.464 26L28.8125 15.2188V36.7812ZM70.3562 8.64225C70.8235 10.3313 71.147 12.5952 71.3625 15.4702C71.614 18.3452 71.722 20.825 71.722 22.9813L71.9375 26C71.9375 33.8703 71.3625 39.6562 70.3562 43.3577C69.4577 46.5923 67.3735 48.6765 64.139 49.575C62.45 50.0422 59.3595 50.3658 54.6158 50.5813C49.9438 50.8328 45.6672 50.9408 41.714 50.9408L36 51.1562C20.9422 51.1562 11.5625 50.5812 7.861 49.575C4.6265 48.6765 2.54225 46.5923 1.64375 43.3577C1.1765 41.6688 0.853251 39.4047 0.637501 36.5297C0.386001 33.6547 0.278 31.175 0.278 29.0188L0.0625 26C0.0625 18.1297 0.637499 12.3438 1.64375 8.64225C2.54225 5.40775 4.6265 3.3235 7.861 2.425C9.55 1.95775 12.6407 1.63425 17.3845 1.41875C22.0562 1.16725 26.3327 1.05925 30.286 1.05925L36 0.84375C51.0578 0.84375 60.4375 1.41875 64.139 2.425C67.3735 3.3235 69.4577 5.40775 70.3562 8.64225Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 976 B

BIN
img/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
img/social_image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 461 KiB

BIN
img/spawn.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

223
index.php Normal file
View file

@ -0,0 +1,223 @@
<?php
global $twig, $mysql;
require_once '_config.php';
use Bramus\Router\Router;
$curl = new anlutro\cURL\cURL;
$router = new Router();
session_start();
$nations = array(
'psf' => array(
'name' => 'Panorama Socialist Federation',
'flag' => 'https://raw.theclashfruit.me/CRSS/CRSS/main/Nations/Republic%20of%20Panorama/Flag.svg',
'short' => 'psf',
'description' => 'The first nation on CRSS, previously known as ROP.',
),
'drr' => array(
'name' => 'Democratic Republic of Rayland',
'flag' => 'https://raw.theclashfruit.me/CRSS/CRSS/main/Nations/Democratic%20Republic%20of%20Rayland/bannre.png',
'short' => 'drr',
'description' => 'Short description of D.R.R.',
),
'cnk' => array(
'name' => 'Chunkia',
'flag' => 'https://raw.theclashfruit.me/CRSS/CRSS/main/Nations/Chunkia/chunkia512.png',
'short' => 'cnk',
'description' => 'In the chaos of Minecraft, chunk errors are inevitable. Chunkia is based in one.',
)
);
if(isset($_SESSION['user'])) {
$dbUser = $mysql->getUserRecordFromId($_SESSION['user']['id']);
$user = $_SESSION['user'];
$user['is_admin'] = $dbUser['is_admin'];
$twig->addGlobal('user', $user);
}
$res = $curl->get('https://crss.blurryface.xyz/api/v1/players');
$json = json_decode($res->body, true);
if($json != null)
$twig->addGlobal('playerCount', count($json));
else
$twig->addGlobal('playerCount', $json);
$twig->addGlobal('nations', $nations);
$twig->addGlobal('dc_uri', 'https://discord.com/api/oauth2/authorize?client_id=1144248396467683338&redirect_uri=' . urlencode($_ENV['DISCORD_REDIRECT']) . '&response_type=code&scope=identify%20guilds&state=' . urlencode($_SERVER['REQUEST_URI']));
$twig->addGlobal('reduced', isset($_GET['reduced']));
$router->get('/', function() {
global $twig;
$twig->addGlobal('pageUri', '/');
echo $twig->render('index.twig');
});
$router->get('/nations', function() {
global $twig;
$twig->addGlobal('pageUri', '/nations');
echo $twig->render('nations.twig');
});
$router->get('/gallery', function() {
global $twig;
$twig->addGlobal('pageUri', '/gallery');
echo $twig->render('gallery.twig');
});
$router->get('/map', function() {
global $twig, $mysql;
$twig->addGlobal('pageUri', '/map');
$twig->addGlobal('markers', json_encode($mysql->getMarkers()));
if(isset($_GET['center']))
$twig->addGlobal('center', $_GET['center']);
else
$twig->addGlobal('center', '0;0');
echo $twig->render('map.twig');
});
$router->get('/profile', function() {
global $twig, $mysql;
$twig->addGlobal('pageUri', '/profile');
if (isset($_SESSION['user'])) {
$user = $mysql->getUserRecordFromId($_SESSION['user']['id']);
if ($user == null && $user['admin'] == 0) {
http_response_code(404);
echo $twig->render('404.twig');
} else {
echo $twig->render('profile.twig', array('db_data' => $user));
}
} else {
http_response_code(404);
echo $twig->render('404.twig');
}
});
$router->get('/nation/([a-z]+)', function ($nation) {
global $twig, $mysql, $nations;
$twig->addGlobal('pageUri', '/nation/' . $nation);
if(!$nations[$nation]) {
http_response_code(404);
echo $twig->render('404.twig');
} else {
echo $twig->render('nation.twig', array('nation' => $nations[$nation]));
}
});
$router->get('/u/([a-z0-9_\.]+)', function($name) {
global $twig, $mysql, $discord;
$twig->addGlobal('pageUri', '/u/' . $name);
$user = $mysql->getUserRecordFromUsername($name);
if($user == null) {
http_response_code(404);
echo $twig->render('404.twig');
} else {
echo $twig->render('user.twig', array('db_user' => $user));
}
});
// ---------------- Admin ---------------- //
$router->get('/admin', function() {
global $twig, $mysql;
$twig->addGlobal('pageUri', '/admin');
if (isset($_SESSION['user'])) {
$user = $mysql->getUserRecordFromId($_SESSION['user']['id']);
$users = $mysql->getUsers();
$markers = $mysql->getMarkers();
if ($user == null && $user['admin'] == 0) {
http_response_code(401);
echo '<style>body { overflow: hidden; height: 100svh; background: black; display: flex; justify-content: center; align-items: center; }</style><img src="https://http.cat/401" alt="401 Unauthorized" />';
} else {
echo $twig->render('admin/index.twig', array('users' => $users, 'markers' => $markers));
}
} else {
http_response_code(401);
echo '<style>body { overflow: hidden; height: 100svh; background: black; display: flex; justify-content: center; align-items: center; }</style><img src="https://http.cat/401" alt="401 Unauthorized" />';
}
});
$router->get('/admin/__data/page/([a-z]+)', function($page) {
global $twig, $mysql;
if (isset($_SESSION['user'])) {
$user = $mysql->getUserRecordFromId($_SESSION['user']['id']);
$users = $mysql->getUsers();
$markers = $mysql->getMarkers();
if ($user == null && $user['admin'] == 0) {
http_response_code(401);
echo '<style>body { overflow: hidden; height: 100svh; background: black; display: flex; justify-content: center; align-items: center; }</style><img src="https://http.cat/401" alt="401 Unauthorized" />';
} else {
try {
echo $twig->render('admin/pages/' . urlencode($page) . '.twig', array('users' => $users, 'markers' => $markers));
} catch (Exception $e) {
http_response_code(404);
echo $twig->render('admin/pages/404.twig');
}
}
} else {
http_response_code(401);
echo '<style>body { overflow: hidden; height: 100svh; background: black; display: flex; justify-content: center; align-items: center; }</style><img src="https://http.cat/401" alt="401 Unauthorized" />';
}
});
// ---------------- Admin API ---------------- //
$adminApi = new Admin($router);
$adminApi->registerApiRoutes();
// ----------------- 404 ----------------- //
$router->set404(function() {
global $twig;
$twig->addGlobal('pageUri', '404');
http_response_code(404);
echo $twig->render('404.twig');
});
$router->run();

50
js/admin/nav.js Normal file
View file

@ -0,0 +1,50 @@
const pageContainer = document.querySelector('.pageContainer');
window.history.pushState({}, '', '#/');
window.addEventListener('hashchange', () => {
let uri = window.location.href.split('#')[1];
if (!window.location.href.includes('#'))
uri = '/admin#/'
const allActiveLinks = document.querySelectorAll('.nav a.active');
const allLinksWithThisUrl = document.querySelectorAll(`.nav a[href="#${uri}"]`);
allActiveLinks.forEach(activeLink => {
activeLink.classList.remove('active');
activeLink.classList.add('link-body-emphasis');
});
allLinksWithThisUrl.forEach(link => {
link.classList.add('active');
link.classList.remove('link-body-emphasis');
});
changePage(window.location.href.split('#')[1].replace('/', ''));
});
const changePage = (url) => {
if (!url)
url = 'dashboard';
pageContainer.innerHTML = `<i class="loader" data-lucide="loader-circle"></i>`;
pageContainer.classList.add('d-flex');
pageContainer.classList.add('align-items-center');
pageContainer.classList.add('justify-content-center');
lucide.createIcons();
fetch(`/admin/__data/page/${url}`)
.then(res => res.text())
.then(html => {
pageContainer.innerHTML = html;
pageContainer.classList.remove('d-flex');
pageContainer.classList.remove('align-items-center');
pageContainer.classList.remove('justify-content-center');
lucide.createIcons();
});
};

13
js/main.js Normal file
View file

@ -0,0 +1,13 @@
import { enableTransition } from "/js/trans.js";
function enableLinks() {
document.querySelectorAll(".transitionEnabled").forEach( hyperlinkElement => {
enableTransition(hyperlinkElement);
hyperlinkElement.classList.remove("transitionEnabled");
});
}
enableLinks();
window.addEventListener("transitionEnd", enableLinks);

176
js/map.js Normal file
View file

@ -0,0 +1,176 @@
L.TileLayer.CRSSLayer = L.TileLayer.extend({
getTileUrl: function(coordinate) {
const tileX = coordinate.x;
const tileY = coordinate.y;
const tileZ = coordinate.z;
const url = ('https://cdn-new.theclashfruit.me/crss/tiles/zoom.{z}/{xd}/{yd}/tile.{x}.{y}.png')
.replace('{yd}', Math.floor(tileY / 10))
.replace('{xd}', Math.floor(tileX / 10))
.replace('{y}', tileY)
.replace('{x}', tileX)
.replace('{z}', tileZ);
return url;
}
});
L.tileLayer.crssLayer = function(templateUrl, options) {
return new L.TileLayer.CRSSLayer(templateUrl, options);
}
let mapLayer = L.tileLayer.crssLayer('https://cdn-new.theclashfruit.me/crss/tiles/zoom.{z}/{xd}/{yd}/tile.{x}.{y}.png', {
attribution: '&copy; <a href="https://crss.blurryface.xyz/">CRSS</a> Players | Tiles With <a href="https://unmined.net/">uNmINeD</a>.',
tileSize: 256,
noWrap: true,
maxNativeZoom: 0,
minNativeZoom: -4,
minZoom: -4,
maxZoom: -4 + 8,
zoomOffset: -8
});
let ropMarkers = L.layerGroup([]);
let drrMarkers = L.layerGroup([]);
let miscMarkers = L.layerGroup([]);
markers.forEach(marker => {
const coords = marker.data.split(';');
switch (marker.category) {
case 'rop':
ropMarkers
.addLayer(
L.marker([
parseInt(coords[0]), parseInt(coords[1])
]).bindPopup(marker.name)
);
break;
case 'drr':
drrMarkers.addLayer(
L.marker([
parseInt(coords[0]), parseInt(coords[1])
]).bindPopup(marker.name)
);
break;
default:
miscMarkers.addLayer(
L.marker([
parseInt(coords[0]), parseInt(coords[1])
]).bindPopup(marker.name)
);
break;
}
});
let playerMarkers = L.layerGroup([
]);
console.log(L.CRS.Simple.infinite)
let map = L.map('map', {
layers: [
mapLayer,
miscMarkers,
ropMarkers,
drrMarkers,
playerMarkers
],
preferCanvas: true,
crs: L.Util.extend(L.CRS.Simple, {
transformation: new L.Transformation(1, 0, 1, 0),
projection: L.Projection.LonLat
}),
}).setView([
parseInt(center.split(';')[1]),
parseInt(center.split(';')[0])
], 2);
let baseMaps = {
"Overworld": mapLayer
};
let overlayMaps = {
"Players": playerMarkers,
"Miscellaneous Markers": miscMarkers,
"Markers in RoP": ropMarkers,
"Markers in DRR": drrMarkers,
};
let layerControl = L.control.layers(baseMaps, overlayMaps).addTo(map);
L.control.mousePosition({
position: 'bottomright',
separator: '; ',
lngFormatter: (x) => {
return Math.floor(x)
},
latFormatter: (y) => {
return Math.floor(y)
},
wrapLng: false,
lngFirst: true
}).addTo(map);
const mappedPlayers = {}
const updatePlayerPos = (players) =>{
for (const player of players) {
const playerMarker = mappedPlayers[player.uniqueId];
if(playerMarker) {
playerMarker.setLatLng([player.location.z, player.location.x]);
playerMarker.setPopupContent(`${player.displayName} (${Math.floor(player.location.x)}; ${Math.floor(player.location.y)}; ${Math.floor(player.location.z)})`);
mappedPlayers[player.uniqueId] = playerMarker;
} else {
const playerIcon = L.icon({
iconUrl: `https://mc-heads.net/avatar/${player.displayName}/16`,
iconSize: [28, 28],
iconAnchor: [14, 14],
popupAnchor: [0, -14]
});
const marker = L.marker([player.location.z, player.location.x], { icon: playerIcon, alt: player.displayName })
.bindPopup(`${player.displayName} (${Math.floor(player.location.x)}; ${Math.floor(player.location.y)}; ${Math.floor(player.location.z)})`);
playerMarkers.addLayer(marker);
mappedPlayers[player.uniqueId] = marker;
}
}
for (const [uniqueId, playerMarker] of Object.entries(mappedPlayers)) {
if(!players.find(p => p.uniqueId === uniqueId)) {
playerMarkers.removeLayer(playerMarker);
delete mappedPlayers[uniqueId];
}
}
}
fetch('https://crss.blurryface.xyz/api/v1/players')
.then(r => r.json())
.then(p => {
updatePlayerPos(p);
});
setInterval(() => {
fetch('https://crss.blurryface.xyz/api/v1/players')
.then(r => r.json())
.then(p => {
updatePlayerPos(p);
});
}, 1000);

18
js/nav.js Normal file
View file

@ -0,0 +1,18 @@
const $ = selector => document.querySelector(selector);
const navToggle = $(".navToggle");
const menu = $(".pageNav > .container");
navToggle.onclick = () => {
const menuToggled = menu.classList.contains("opened");
if (menuToggled) {
menu.classList.remove("opened");
} else {
menu.classList.add("opened");
}
navToggle.innerHTML = menuToggled ? "Menu" : "Close";
}
window.addEventListener("transitionBuffering", () => menu.classList.remove("opened"));

66
js/trans.js Normal file
View file

@ -0,0 +1,66 @@
/*
* Myadeleines' Simple Page Transition Script. "Trans" for short. :trol:
* Making CSS-powered animated transitions between pages possible.
*
* Licensed under the Apache License. Please refer to the LICENSE file.
*/
const transitionBufferingEvent = new Event("transitionBuffering");
const transitionStartEvent = new Event("transitionStart");
const transitionEndEvent = new Event("transitionEnd");
const $ = selector => document.querySelector(selector);
const parser = new DOMParser();
const mainElement = $("main");
export function enableTransition( hyperlinkElement ) {
hyperlinkElement.addEventListener("click", event => {
event.preventDefault(); // Browser won't load the page when the hyperlink is pressed.
loadURL(hyperlinkElement.href, true, hyperlinkElement);
});
}
function loadURL( targetedURL, updateURL, hyperlinkElement ) {
const activeHyperlink = $(".pageNav .active");
mainElement.classList.add("buffering");
window.dispatchEvent(transitionBufferingEvent);
fetch(targetedURL + "?reduced")
.catch(error => {
console.log(error);
alert(error);
mainElement.classList.remove("buffering");
})
.then(response => response.text().then( fetchedPage => {
fetchedPage = parser.parseFromString(fetchedPage, "text/html");
fetchedPage = {
content: fetchedPage.querySelector("main").innerHTML,
title: fetchedPage.querySelector("title").innerHTML,
}
if (activeHyperlink) activeHyperlink.classList.remove("active");
if (hyperlinkElement) hyperlinkElement.classList.add("active");
if (updateURL) history.pushState({}, fetchedPage.title, targetedURL);
$("title").innerHTML = fetchedPage.title;
mainElement.classList.remove("buffering");
mainElement.classList.add("transition");
let transitionDuration = getComputedStyle(mainElement).transitionDuration;
transitionDuration = parseFloat(transitionDuration) * 1000;
window.dispatchEvent(transitionStartEvent);
setTimeout(() => {
mainElement.innerHTML = fetchedPage.content;
mainElement.classList.remove("transition");
window.dispatchEvent(transitionEndEvent);
}, transitionDuration);
}));
}
window.addEventListener("popstate", Event => {
loadURL(window.location.href, false)
});

View file

@ -1,224 +0,0 @@
import { Role } from '@/utils/permissions';
import mysql, { Pool, QueryResult } from 'mysql2/promise';
import crypto from 'node:crypto';
interface DiscordTokenData {
token_type: string;
access_token: string;
expires_in: number;
refresh_token: string;
scope: string;
id_token: string;
}
interface UserTable {
id: number;
did: string;
username: string;
global_name: string;
email: string;
avatar?: string;
banner?: string;
accent_color?: number;
permissions: number;
}
interface UserTableWithoutEmail {
id: number;
did: string;
username: string;
global_name: string;
avatar?: string;
banner?: string;
accent_color?: number;
permissions: number;
}
interface TeamMember {
id: number;
uid: number;
did: string;
username: string;
global_name: string;
email: string;
avatar: string;
banner: string;
accent_color: number;
permissions: number;
role: Role;
}
class Database {
static instance: (Database | null) = null;
mysqlPool: (Pool | null) = null;
constructor() {
if(Database.instance)
return Database.instance;
this.mysqlPool = mysql.createPool({
database: process.env.DB_NAME,
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASS,
waitForConnections: true,
supportBigNumbers: true,
connectionLimit: 10,
queueLimit: 0
});
Database.instance = this;
}
// Auth Stuff
async createUser(user: any): Promise<number> {
const [ result ] = await this.mysqlPool!.execute('SELECT * FROM users WHERE did = ?', [ user.id ]);
if ((result as any).length > 0) {
return (result as any)[0].id;
}
const [ res ] = await this.mysqlPool!.execute('INSERT INTO users (did, username, global_name, email, avatar, banner, accent_color) VALUES (?, ?, ?, ?, ?, ?, ?)', [
user.id,
user.username,
(user.global_name || user.username),
user.email,
user.avatar,
user.banner,
user.accent_color
]);
return (res as any).insertId;
}
/**
* Creates a session for the user.
*
* Should only be called by backend!
*
* @param userData The data returned from the `/oauth2/token` endpoint.
* @param userAgent The user agent of the user.
*
* @returns `string` The session token.
*/
async createSession(userData: DiscordTokenData, userAgent?: string): Promise<string | null> {
const res = await fetch(`${process.env.DISCORD_API}/users/@me`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${userData.access_token}`,
},
});
if (res.ok) {
const user = await res.json();
const uid = await this.createUser(user);
const sum = crypto.createHmac('sha256', process.env.AUTH_SECRET!);
const base = Buffer.from(user.id).toString('base64').replaceAll('=', '');
const date = Buffer.from((Date.now() - 1688940000000).toString()).toString('base64').replaceAll('=', '');
sum.update(userData.access_token);
const sid = base + '.' + date + '.' + sum.digest('hex');
const [ result ] = await this.mysqlPool!.execute('INSERT INTO sessions (sid, uid, access_token, refresh_token, id_token, user_agent, expires) VALUES (?, ?, ?, ?, ?, ?, ?)', [
sid,
uid,
userData.access_token,
userData.refresh_token,
userData.id_token,
userAgent,
new Date(Date.now() + (userData.expires_in * 1000)),
]);
return sid;
} else {
throw new Error('Error Fetching Discord User Data');
}
}
async deleteSession(sid: string): Promise<void> {
await this.mysqlPool!.execute('DELETE FROM sessions WHERE sid = ?', [ sid ]);
}
async getSession(sid: string): Promise<any> {
const [ rows ] = await this.mysqlPool!.execute('SELECT * FROM sessions WHERE sid = ?', [ sid ]);
return (rows as any)[0];
}
// End of Auth Stuff
async getUser(id: number): Promise<UserTable> {
const [ rows ] = await this.mysqlPool!.execute('SELECT * FROM users WHERE id = ?', [ id ]);
return (rows as UserTable[])[0];
}
async getUsers(): Promise<UserTableWithoutEmail[]> {
const [ rows ] = await this.mysqlPool!.execute('SELECT id, did, username, global_name, avatar, banner, accent_color, permissions FROM users');
return rows as UserTableWithoutEmail[];
}
async getUserUsername(username: string): Promise<UserTable> {
const [ rows ] = await this.mysqlPool!.execute('SELECT * FROM users WHERE username = ?', [ username ]);
return (rows as UserTable[])[0];
}
async getUserSessions(uid: number): Promise<any> {
const [ rows ] = await this.mysqlPool!.execute('SELECT * FROM sessions WHERE uid = ?', [ uid ]);
return rows;
}
async getTeam(): Promise<TeamMember[]> {
const [ rows ] = await this.mysqlPool!.execute('SELECT team_members.id AS tid, team_members.uid AS uid, users.did, users.username, users.global_name, users.email, users.avatar, users.banner, users.accent_color, users.permissions, team_members.role FROM team_members JOIN users ON team_members.uid = users.id;');
return (rows as any).map((row: any) => {
return {
id: row.tid,
uid: row.uid,
did: row.did,
username: row.username,
global_name: row.global_name,
email: row.email,
avatar: row.avatar,
banner: row.banner,
accent_color: row.accent_color,
permissions: row.permissions,
role: (row.role as Role),
};
});
}
async getNations(): Promise<any> {
const [ rows ] = await this.mysqlPool!.execute('SELECT * FROM nations');
return rows;
}
async getNationCode(code: string): Promise<any> {
const [ rows ] = await this.mysqlPool!.execute('SELECT * FROM nations WHERE code = ?', [ code ]);
return (rows as any)[0];
}
async getCompaies(nid: number): Promise<any> {
const [ rows ] = await this.mysqlPool!.execute('SELECT * FROM companies WHERE nid = ?', [ nid ]);
return rows;
}
async updateUserPermissions(id: number, permissions: any) {
await this.mysqlPool!.execute('UPDATE users SET permissions = ? WHERE id = ?', [ permissions, id ]);
return;
}
}
export default Database;

View file

@ -1,79 +0,0 @@
const path = require('path');
const childProcess = require('child_process');
const gitBranch = childProcess.execSync('git rev-parse --abbrev-ref HEAD').toString().trim();
const gitCommit = childProcess.execSync('git rev-parse HEAD').toString().trim();
const gitCommitTime = childProcess.execSync('git show -s --format=%cI').toString().trim();
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
sassOptions: {
includePaths: [ path.join(__dirname, 'styles') ]
},
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'cdn.discordapp.com',
port: '',
pathname: '/**',
},
{
protocol: 'https',
hostname: 'crss.fra1.cdn.digitaloceanspaces.com',
port: '',
pathname: '/**',
}
]
},
publicRuntimeConfig: {
modifiedDate: new Date().getTime(),
discord: {
clientId: process.env.DISCORD_CLIENT,
redirectUri: process.env.DISCORD_REDIRECT,
api: process.env.DISCORD_API,
scopes: [
'identify',
'email',
'openid'
]
},
git: {
branch: gitBranch,
commit: {
sha: gitCommit,
created: new Date(gitCommitTime)
}
}
},
generateBuildId: async () => {
return childProcess.execSync('git rev-parse HEAD').toString().trim();
},
headers: async () => {
return [
{
source: '/api/:path*',
headers: [
{
key: 'Access-Control-Allow-Origin',
value: '*'
}
]
}
];
},
webpack(config) {
config.module.rules.push({
test: /\.svg$/,
use: [ '@svgr/webpack' ]
});
return config;
},
experimental: {
optimizePackageImports: [ '@icons-pack/react-simple-icons' ]
}
};
module.exports = nextConfig;

View file

@ -1,37 +0,0 @@
{
"name": "crss-website",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@icons-pack/react-simple-icons": "^10.0.0",
"@svgr/webpack": "^8.1.0",
"bcrypt": "^5.1.1",
"cookie": "^0.6.0",
"framer-motion": "^11.3.31",
"jose": "^5.8.0",
"lucide-react": "^0.429.0",
"mysql2": "^3.11.0",
"next": "14.2.6",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"showdown": "^2.1.0",
"ua-parser-js": "^1.0.38"
},
"devDependencies": {
"@types/cookie": "^0.6.0",
"@types/node": "^20.16.2",
"@types/react": "^18.3.4",
"@types/react-dom": "^18.3.0",
"@types/ua-parser-js": "^0.7.39",
"eslint": "^8.57.0",
"eslint-config-next": "14.2.6",
"sass": "^1.77.8",
"typescript": "^5.5.4"
}
}

View file

@ -1,25 +0,0 @@
import Footer from '@/components/Footer';
import Meta from '@/components/Meta';
import NavBar from '@/components/NavBar';
import PageContent from '@/components/PageContent';
export default function NotFound() {
return (
<>
<Meta page={{ title: '404' }} />
<NavBar currentPage="404" />
<PageContent>
<div className="container">
<h1 id="404-not-found">404 Not Found</h1>
<p>
We couldn&apos;t find this page :(
</p>
</div>
</PageContent>
<Footer />
</>
);
}

View file

@ -1,16 +0,0 @@
import '@/styles/globals.scss';
import { UserProvider } from '@/context/UserContext';
import { AnimatePresence } from 'framer-motion';
import type { AppProps } from 'next/app';
export default function App({ Component, pageProps, router }: AppProps) {
return (
<UserProvider>
<AnimatePresence mode="wait" initial={false} onExitComplete={() => window.scrollTo(0, 0)}>
<Component {...pageProps} key={router.asPath} />
</AnimatePresence>
</UserProvider>
);
}

View file

@ -1,13 +0,0 @@
import { Html, Head, Main, NextScript } from 'next/document';
export default function Document() {
return (
<Html lang="en">
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
);
}

View file

@ -1,78 +0,0 @@
import Card from '@/components/Card';
import Footer from '@/components/Footer';
import NavBar from '@/components/NavBar';
import PageContent from '@/components/PageContent';
import Database from '@/lib/Database';
import Image from 'next/image';
import Link from 'next/link';
import { Role } from '@/utils/permissions';
import styles from '@/styles/About.module.scss';
import { Globe } from 'lucide-react';
import Meta from '@/components/Meta';
export default function About({ teamMembers }: { teamMembers: any[] }) {
return (
<>
<Meta page={{ title: 'About' }} />
<NavBar currentPage="about" />
<PageContent>
<h1>About</h1>
<p>
We are a small team running this server.
</p>
<h2>Team</h2>
<div className={styles.teamList}>
{teamMembers.map((member, i) => (
<Card key={i} className={styles.teamCard}>
<div className={styles.memberBanner}>
<Image src={`https://cdn.discordapp.com/avatars/${member.did}/a_${member.avatar}.png`} alt={member.global_name} width={128} height={128} />
</div>
<div className={styles.memberContent}>
<ul className={styles.memberLinks}>
<li>
<Link href="#" title="Website">
<Globe />
</Link>
</li>
</ul>
<h3>{member.global_name}</h3>
{member.role === Role.Owner && <label>Owner</label>}
{member.role === Role.Admin && <label>Admin</label>}
</div>
</Card>
))}
</div>
</PageContent>
<Footer />
</>
);
}
export async function getServerSideProps(context: any) {
const db = new Database();
const teamMembers = await db.getTeam();
if (!teamMembers)
return {
notFound: true
};
return {
props: {
teamMembers
}
};
}

View file

@ -1,134 +0,0 @@
import PageContent from '@/components/PageContent';
import { useUser } from '@/context/UserContext';
import { getCookieFromContext } from '@/utils/cookies';
import { use, useEffect, useState } from 'react';
import Image from 'next/image';
export default function Admin() {
const { user, isLoggedIn } = useUser();
const [ users, setUsers ] = useState<any[]>([]);
useEffect(() => {
fetch('/api/v1/users')
.then((response) => response.json())
.then((data) => {
setUsers(data);
});
}, [ ]);
const addTeamMember = (userId: number) => {
alert(userId);
};
const editPermissions = (user: any) => {
const perms = prompt('Enter new permissions for user (1 == Admin; There is nothing more.):', user.permissions);
if (perms === null) {
return;
}
fetch(`/api/v1/user/${user.username}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
permissions: perms,
})
})
.then((response) => {
if (response.status === 204) {
alert('Permissions updated.');
} else {
alert('Failed to update permissions.');
}
});
};
if (!isLoggedIn) {
return null;
}
return (
<>
<PageContent>
<h1>Admin</h1>
<p>Welcome, {user.names.global_name}!</p>
<p>What are we doin&apos; today?</p>
<h2>Users</h2>
<table>
<thead>
<tr>
<th>Avatar</th>
<th>Username</th>
<th>Global Name</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{users && (
users.map((u) => (
<tr key={u.id}>
<td>
<Image src={`https://cdn.discordapp.com/avatars/${u.did}/${u.avatar}.png`} alt={user.global_name} width={32} height={32} />
</td>
<td>{u.username}</td>
<td>{u.global_name}</td>
<td>
<button onClick={() => { addTeamMember(u.id); }}>
Add As Team Member (On the /about page.)
</button>
<button onClick={() => { editPermissions(u); }}>
Edit Permissions
</button>
<button onClick={() => { alert('GDPR? Yeah no, will implement later.'); }}>
Delete
</button>
</td>
</tr>
))
)}
</tbody>
</table>
<h2>Your User in JSON Format</h2>
<p>In case you need it for some reason.</p>
<pre>
{JSON.stringify(user, null, 2)}
</pre>
</PageContent>
</>
);
}
export async function getServerSideProps(context: any) {
const { req } = context;
const cookie = req.headers.cookie;
let isLoggedIn = false;
if (cookie) {
const sessionCookie = getCookieFromContext('session', cookie);
if (sessionCookie) {
isLoggedIn = true;
}
}
if (!isLoggedIn) {
return {
notFound: true,
};
}
return {
props: {},
};
}

View file

@ -1,13 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next';
type Data = {
name: string;
};
export default function handler(
req: NextApiRequest,
res: NextApiResponse<Data>,
) {
res.status(200).json({ name: 'John Doe' });
}

View file

@ -1,71 +0,0 @@
import Database from '@/lib/Database';
import { serialize } from 'cookie';
import type { NextApiRequest, NextApiResponse } from 'next';
type Data = {
sid: string | null;
};
type Error = {
error: string;
};
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<Data | Error>,
) {
const db = new Database();
const { code, state } = req.query;
console.log(code, state);
const discordApi = process.env.DISCORD_API!;
try {
if (typeof code === 'string') {
const data = await fetch(`${discordApi}/oauth2/token`, {
method: 'POST',
body: new URLSearchParams({
client_id: process.env.DISCORD_CLIENT!,
client_secret: process.env.DISCORD_SECRET!,
grant_type: 'authorization_code',
code,
redirect_uri: process.env.DISCORD_REDIRECT!,
}),
});
const json = await data.json();
const sid = await db.createSession(json, req.headers['user-agent']);
if (sid) {
const cookie = serialize('session', sid, {
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
path: '/',
expires: new Date(Date.now() + json.expires_in * 1000),
});
res.setHeader('Set-Cookie', cookie);
if ((state as string).startsWith('/'))
res.status(302).redirect(state as string);
else
res.status(400).json({ error: 'Invalid redirect uri in state!' });
}
return;
}
} catch (error) {
console.error(error);
res.status(500).json(
{ error: 'Internal Server Error' }
);
}
res.status(400).json(
{ error: 'Invalid code' }
);
}

View file

@ -1,61 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
type Error = {
message: string;
};
interface ServerInfo {
version: string;
online: number;
worlds: string[];
}
import * as net from 'node:net';
export default function handler(
req: NextApiRequest,
res: NextApiResponse<ServerInfo | Error>,
) {
try {
const mc_api = process.env.MC_API!.split(':');
const socket = net.createConnection({
host: mc_api[0],
port: parseInt(mc_api[1]),
}, async () => {
const reqData = Buffer.alloc(1 + 4);
reqData.writeInt8(0x00, 0);
reqData.writeUint32BE(0, 1);
socket.write(reqData);
socket.on('data', (data) => {
const packetId = data[0];
const length = data.readUInt32BE(1); // unused but in case someone wants to verify the lenght :3
if (packetId !== 0x00) {
socket.end();
return res.status(500).json({ message: 'There was an error with the server.' });
}
const jsonData = data.toString('utf-8', 5);
socket.end();
res.status(200).json(JSON.parse(jsonData));
});
socket.on('error', (err) => {
console.error(err);
socket.end();
res.status(500).json({ message: 'There was an error with the server.' });
});
});
} catch (e) {
res.status(500).json({ message: 'There was an error with the server.' });
}
}

View file

@ -1,48 +0,0 @@
import Database from '@/lib/Database';
import type { NextApiRequest, NextApiResponse } from 'next';
import { serialize } from 'cookie';
type Data = {
success: string;
};
interface Error {
error: string;
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<Data | Error>,
) {
const db = new Database();
const sid = req.cookies.session;
if (!sid)
return res.status(401).json({ error: 'Unauthorized' });
const session = await db.getSession(sid!);
if (!session)
return res.status(401).json({ error: 'Unauthorized' });
const user = await db.getUser(session.uid);
if (!user)
return res.status(404).json({ error: 'Not Found' });
if (req.method === 'DELETE') {
db.deleteSession(sid!);
res.setHeader('Set-Cookie', serialize('session', '', {
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
path: '/',
expires: new Date(0),
}));
return res.status(200).json({ success: 'Delete Session for ' + user.username });
}
res.status(405).json({ error: 'Method Not Allowed' });
}

View file

@ -1,52 +0,0 @@
import Database from '@/lib/Database';
import type { NextApiRequest, NextApiResponse } from 'next';
type Data = {
id: number;
discord_id: string;
names: {
username: string;
global_name: string;
};
email: string;
avatar?: string;
banner?: string;
accent_color?: number;
permissions: number;
};
interface Error {
error: string;
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<Data | Error>,
) {
const db = new Database();
const sid = req.cookies.session;
if (!sid)
return res.status(401).json({ error: 'Unauthorized' });
const session = await db.getSession(sid!);
if (!session)
return res.status(401).json({ error: 'Unauthorized' });
const user = await db.getUser(session.uid);
res.status(200).json({
id: user.id,
discord_id: user.did,
names: {
username: user.username,
global_name: user.global_name
},
email: user.email,
avatar: user.avatar,
banner: user.banner,
accent_color: user.accent_color,
permissions: user.permissions
});
}

View file

@ -1,69 +0,0 @@
import Database from '@/lib/Database';
import { isUserAdmin } from '@/utils/auth_util';
import type { NextApiRequest, NextApiResponse } from 'next';
type Data = {
id: number;
discord_id: string;
names: {
username: string;
global_name: string;
};
avatar?: string;
banner?: string;
accent_color?: number;
permissions: number;
};
interface Error {
error: string;
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<Data | Error>,
) {
const db = new Database();
const user = await db.getUserUsername(req.query.username as string);
if (!user) {
return res.status(404).json({ error: 'Not Found' });
}
// hehe only admins update users :trolley:
// also validation yeah uh... didn't have budget for that
// tech debt for the win
if (req.method === 'PATCH') {
const sid = req.cookies.session;
const isAdmin = isUserAdmin(sid);
if (!isAdmin) {
return res.status(401).json({ error: 'Unauthorized' });
}
const { permissions } = req.body;
if (permissions) {
await db.updateUserPermissions(user.id, permissions);
}
res.status(204).end();
return;
}
res.status(200).json({
id: user.id,
discord_id: user.did,
names: {
username: user.username,
global_name: user.global_name
},
avatar: user.avatar,
banner: user.banner,
accent_color: user.accent_color,
permissions: user.permissions
});
}

View file

@ -1,24 +0,0 @@
import Database from '@/lib/Database';
import type { NextApiRequest, NextApiResponse } from 'next';
interface Response {
id: number;
did: string;
username: string;
global_name: string;
avatar?: string;
banner?: string;
accent_color?: number;
permissions: number;
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<Response[]>,
) {
const db = new Database();
const users = await db.getUsers();
res.status(200).json(users);
}

View file

@ -1,25 +0,0 @@
import Footer from '@/components/Footer';
import Meta from '@/components/Meta';
import NavBar from '@/components/NavBar';
import PageContent from '@/components/PageContent';
export default function Gallery() {
return (
<>
<Meta page={{ title: 'Gallery' }} />
<NavBar currentPage="gallery" />
<PageContent>
<div className="container">
<h1>Gallery</h1>
<p>
Under Construction
</p>
</div>
</PageContent>
<Footer />
</>
);
}

View file

@ -1,41 +0,0 @@
import AdBanner from '@/components/AdBanner';
import Footer from '@/components/Footer';
import Meta from '@/components/Meta';
import NavBar from '@/components/NavBar';
import PageContent from '@/components/PageContent';
export default function Home() {
const nations = [ ];
return (
<>
<Meta page={{ title: 'Home' }} />
<NavBar currentPage="home" />
<PageContent>
<h1>Home</h1>
<p>
Welcome to Clyde&apos;s Real Survival SMP, CRSS for short. We are a small SMP server that updates to every version starting from b1.0 on the 1st of every month. We have a small community of players that are very friendly and welcoming to new players. We have a few rules that you should follow to make the server a better place for everyone, you can find them at the bottom of the page.
</p>
<p>
The server is built on the idea of nations, featuring { nations.length } nations so far, with the oldest being Panorama Socialist Federation, which was originally known as Republic of Panorama. These nations are scattered around the map, with some being more active than others. You can be sure to find a nation that fits your playstyle, if not you can just start your own!
</p>
<h2 id="modpacl">Modpack</h2>
<p>
We have a client-side modpack to make your experince on CRSS better, the mods included are intended to be small, simple, and follow the rules of CRSS.
</p>
<p>
You can download it from Modrinth: <a href="https://modrinth.com/modpack/crsspack">https://modrinth.com/modpack/crsspack</a>.
</p>
</PageContent>
<Footer />
</>
);
}

View file

@ -1,137 +0,0 @@
import Footer from '@/components/Footer';
import Meta from '@/components/Meta';
import NavBar from '@/components/NavBar';
import PageContent from '@/components/PageContent';
import Link from 'next/link';
export default function PrivacyPolicy() {
return (
<>
<Meta page={{ title: 'Privacy Policy' }} />
<NavBar currentPage="privacy" />
<PageContent>
<h1>Privacy Policy</h1>
<p>
<strong>Effective Date</strong>: {new Date('2024-08-29T02:00:00.000Z').toLocaleDateString('en-GB')}
</p>
<h2 id="introduction">1. Introduction</h2>
<p>
Welcome to Clyde&apos;s Real Survival SMP (&quot;CRSS&quot;). We are
committed to protecting your privacy and ensuring that your personal
information is handled in a safe and responsible manner. This Privacy
Policy outlines the types of information we collect from our users,
how we use that information, and the measures we take to protect it.
</p>
<h2 id="information-we-collect">2. Information We Collect</h2>
<p>CRSS collects the following personal information from users:</p>
<ul>
<li>
<strong>Discord User IDs</strong>: A unique identifier provided by Discord for each user.
</li>
<li>
<strong>Discord Username</strong>: The username you use on Discord.
</li>
<li>
<strong>Discord Global Display Names</strong>: The display name that is visible to other users on Discord.
</li>
<li>
<strong>Discord Emails</strong>: The email address associated with your Discord account.
</li>
<li>
<strong>User Agents</strong>: Information regarding the device, browser, and operating system you use to access CRSS.
</li>
</ul>
<h2 id="how-we-use-your-information">3. How We Use Your Information</h2>
<p>The information we collect is used for the following purposes:</p>
<ul>
<li>
<strong>Account Management</strong>: To verify your identity and manage your account on CRSS.
</li>
<li>
<strong>Communication</strong>: To send notifications, updates, and other relevant communications related to CRSS.
</li>
<li>
<strong>Security and Moderation</strong>: To ensure the safety and security of our community, including the detection and prevention of fraudulent or unauthorized activities.
</li>
<li>
<strong>Improvement of Services</strong>: To enhance and improve the user experience on CRSS.
</li>
</ul>
<h2 id="sharing-and-disclosure-of-information">4. Sharing and Disclosure of Information</h2>
<p>CRSS does not sell, trade, or otherwise transfer your personal information to outside parties except under the following circumstances:</p>
<ul>
<li>
<strong>Legal Compliance</strong>: We may disclose your information if required to do so by law or in response to a valid request from a law enforcement authority.
</li>
<li>
<strong>Protection of Rights</strong>: We may share information when we believe it is necessary to protect the rights, property, or safety of CRSS, our users, or others.
</li>
<li>
<strong>Service Providers</strong>: We may engage third-party service providers to perform functions on our behalf, such as server hosting and maintenance. These providers have access to the necessary information only to perform their functions and are obligated to maintain confidentiality.
</li>
</ul>
<h2 id="data-security">5. Data Security</h2>
<p>We implement reasonable and appropriate security measures to protect your personal information from unauthorized access, alteration, disclosure, or destruction. However, no method of transmission over the internet or electronic storage is 100% secure, and we cannot guarantee absolute security.</p>
<h2 id="data-retention">6. Data Retention</h2>
<p>We will retain your personal information only for as long as necessary to fulfill the purposes for which it was collected or as required by applicable laws. Once your information is no longer needed, we will delete or anonymize it in a secure manner.</p>
<h2 id="your-rights">7. Your Rights</h2>
<p>You have the right to:</p>
<ul>
<li>
Access the personal information we hold about you.
</li>
<li>
Request corrections to any inaccuracies in your personal information.
</li>
<li>
Request the deletion of your personal information.
</li>
<li>
Withdraw your consent to the processing of your information.
</li>
</ul>
<p>To exercise these rights, please contact us using the information provided below.</p>
<h2 id="changes-to-this-privacy-policy">8. Changes to This Privacy Policy</h2>
<p>CRSS reserves the right to update or modify this Privacy Policy at any time. Any changes will be effective immediately upon posting the revised policy. We will notify you of any significant changes through the Discord server or by other means. Your continued use of CRSS after any modifications to this Privacy Policy constitutes your acceptance of the updated terms.</p>
<h2 id="contact-us">9. Contact Us</h2>
<p>If you have any questions or concerns about this Privacy Policy or our data practices, please contact us at:</p>
<p>
<strong>Email:</strong> <Link href="mailto:admin@theclashfruit.me">admin@theclashfruit.me</Link> <br />
<strong>Discord Server:</strong> <Link href="https://discord.gg/rGjCKawPkS">https://discord.gg/rGjCKawPkS</Link>
</p>
<hr />
<p>By using Clyde&apos;s Real Survival SMP, you acknowledge that you have read and understood this Privacy Policy and agree to the collection, use, and sharing of your information as described herein.</p>
</PageContent>
<Footer />
</>
);
}

View file

@ -1,108 +0,0 @@
import Footer from '@/components/Footer';
import Meta from '@/components/Meta';
import NavBar from '@/components/NavBar';
import PageContent from '@/components/PageContent';
import Link from 'next/link';
export default function TermsOfService() {
return (
<>
<Meta page={{ title: 'Terms of Service' }} />
<NavBar currentPage="terms" />
<PageContent>
<h1>Terms of Service</h1>
<p>
<strong>Effective Date</strong>: {new Date('2024-08-29T02:00:00.000Z').toLocaleDateString('en-GB')}
</p>
<h2 id="acceptance-of-terms">1. Acceptance of Terms</h2>
<p>
By accessing or using Clyde&apos;s Real Survival SMP (&quot;CRSS&quot;), you agree to comply with and be bound by these Terms of Service. If you do not agree with any part of these terms, you must not use CRSS.
</p>
<h2 id="eligibility">2. Eligibility</h2>
<p>
To participate in CRSS, you must have a valid Discord account and be at least 13 years old. By using CRSS, you represent and warrant that you meet these eligibility requirements.
</p>
<h2 id="user-conduct">3. User Conduct</h2>
<p>While using CRSS, you agree to:</p>
<ul>
<li>
<strong>Respect Other Users</strong>: Do not engage in harassment, hate speech, discrimination, or any behavior that harms or threatens other users.
</li>
<li>
<strong>Follow the Rules</strong>: Adhere to all posted rules and guidelines specific to CRSS, including in-game rules and server rules.
</li>
<li>
<strong>No Cheating or Exploiting</strong>: Do not use cheats, hacks, or exploits to gain an unfair advantage in the game.
</li>
<li>
<strong>No Unauthorized Activities</strong>: Do not engage in activities that violate any laws or regulations or that could harm CRSS or its users.
</li>
</ul>
<h2 id="account-responsibility">4. Account Responsibility</h2>
<p>
You are responsible for maintaining the confidentiality of your Discord account credentials and for all activities that occur under your account. If you suspect any unauthorized use of your account, you must notify CRSS administrators immediately.
</p>
<h2 id="content-and-intellectual-property">5. Content and Intellectual Property</h2>
<p>
All content within CRSS, including but not limited to text, graphics, logos, and software, is the property of CRSS or its licensors. You may not use, reproduce, distribute, or create derivative works based on this content without explicit permission.
</p>
<h2 id="termination">6. Termination</h2>
<p>
CRSS reserves the right to terminate or suspend your access to the server at any time, without notice, for violating these Terms of Service or for any other reason deemed necessary by the administrators.
</p>
<h2 id="disclaimers">7. Disclaimers</h2>
<p>
CRSS is provided &quot;as is&quot; without warranties of any kind, either express or implied. We do not guarantee that the server will be available at all times or that it will be free of errors or interruptions.
</p>
<h2 id="limitation-of-liability">8. Limitation of Liability</h2>
<p>
In no event shall CRSS or its administrators be liable for any direct, indirect, incidental, special, or consequential damages arising from your use or inability to use the server.
</p>
<h2 id="changes-to-these-terms">9. Changes to These Terms</h2>
<p>
CRSS reserves the right to update or modify these Terms of Service at any time. Any changes will be effective immediately upon posting the revised terms. Your continued use of CRSS after any modifications constitutes your acceptance of the updated terms.
</p>
<h2 id="contact-information">10. Contact Information</h2>
<p>
If you have any questions or concerns about these Terms of Service, please contact us at:
</p>
<p>
<strong>Email:</strong> <Link href="mailto:admin@theclashfruit.me">admin@theclashfruit.me</Link> <br />
<strong>Discord Server:</strong> <Link href="https://discord.gg/rGjCKawPkS">https://discord.gg/rGjCKawPkS</Link>
</p>
<hr />
<p>
By using Clyde&apos;s Real Survival SMP, you acknowledge that you have read, understood, and agree to these Terms of Service.
</p>
</PageContent>
<Footer />
</>
);
}

View file

@ -1,36 +0,0 @@
import Footer from '@/components/Footer';
import Meta from '@/components/Meta';
import NavBar from '@/components/NavBar';
import PageContent from '@/components/PageContent';
import { ExternalLink } from 'lucide-react';
import Link from 'next/link';
export default function Map() {
return (
<>
<Meta page={{ title: 'Map' }} />
<NavBar currentPage="map" />
<PageContent>
<h1>Map</h1>
<p>
The orginal map&apos;s v2 version will be coming eventually. Till then enjoy the BlueMap:
</p>
<iframe src="https://map.crss.cc" width="100%" style={{ aspectRatio: '16/9' }} />
<p>
<Link href="https://map.crss.cc" target="_blank">
Open in New Tab
<ExternalLink />
</Link>
</p>
</PageContent>
<Footer />
</>
);
}

View file

@ -1,61 +0,0 @@
import Footer from '@/components/Footer';
import Meta from '@/components/Meta';
import NavBar from '@/components/NavBar';
import PageContent from '@/components/PageContent';
import Database from '@/lib/Database';
export default function Nation({ nation, companies }: { nation: any, companies: any[] }) {
return (
<>
<Meta page={{ title: nation.name }} />
<NavBar currentPage={nation.code} />
<PageContent>
<div className="container">
<h1>{nation.name}</h1>
<p>{nation.description}</p>
<h2>Companies</h2>
{companies.length > 0 ? (
<ul>
{companies.map((company) => {
return (
<li key={company.id}>
{company.name}
</li>
);
})}
</ul>
) : (
<p>This nation has no companies.</p>
)}
</div>
</PageContent>
<Footer />
</>
);
}
export async function getServerSideProps(context: any) {
const db = new Database();
const nation = await db.getNationCode(context.query.nation as string);
if (!nation) {
return {
notFound: true
};
}
const companies = await db.getCompaies(nation.id);
return {
props: {
nation,
companies
}
};
}

View file

@ -1,48 +0,0 @@
import Footer from '@/components/Footer';
import Meta from '@/components/Meta';
import NavBar from '@/components/NavBar';
import PageContent from '@/components/PageContent';
import Database from '@/lib/Database';
import Link from 'next/link';
import Image from 'next/image';
import styles from '@/styles/Nations.module.scss';
export default function Nations({ nations }: { nations: any[] }) {
return (
<>
<Meta page={{ title: 'Nations' }} />
<NavBar currentPage="nations" />
<PageContent>
<h1>Nations</h1>
<div className={styles.nationGrid}>
{nations.map((nation, i) => (
<Link key={i} href={`/nation/${nation.code}`} className={styles.nationCard}>
<h2>{nation.name}</h2>
<p>{nation.short_description}</p>
<Image src={`https://crss.fra1.cdn.digitaloceanspaces.com/nation/${nation.code}/flag.svg`} alt={nation.name} width={128} height={64} className={styles.icon} />
</Link>
))}
</div>
</PageContent>
<Footer />
</>
);
}
export async function getServerSideProps(context: any) {
const db = new Database();
const nations = await db.getNations();
return {
props: {
nations
}
};
}

View file

@ -1,138 +0,0 @@
import PageContent from '@/components/PageContent';
import NavBar from '@/components/NavBar';
import Meta from '@/components/Meta';
import styles from '../styles/PremiumPlus.module.scss';
import Link from 'next/link';
import Footer from '@/components/Footer';
export default function PremiumPlus() {
return (
<>
<Meta page={{ title: 'Premium Plus' }} />
<NavBar currentPage="pp" />
<PageContent>
<div className={styles.cardContainer}>
<div className={styles.card}>
<h1>CRSS Premium+ Subscription Plan</h1>
<span>
Unlock the Ultimate Minecraft Experience with CRSS Premium
</span>
<h2>$6.90/month</h2>
<span>or save 2% with annual billing!</span>
</div>
<span>
Join the elite ranks of CRSS Premium+ members and elevate your
Minecraft gameplay to new heights.
</span>
<span>
For just $6.90 per month, our subscription plan offers you exclusive
perks, priority access, and a superior gaming environment. With CRSS
Premium+, you&apos;ll enjoy premium features that enhance your
experience and set you apart from the rest.
</span>
<span>
<h3>Exclusive Benefits of CRSS Premium+:</h3>
<ul>
<li>
<strong>Priority Queue Access:</strong> Skip the lines and jump
straight into the game with priority server access, ensuring
you&apos;re always where the action is.
</li>
<li>
<strong>Custom Kits & Items:</strong> Receive unique in-game
kits and items available only to CRSS Premium+ members, giving
you an edge in every match.
</li>
<li>
<strong>Enhanced Gameplay Features:</strong> Access premium
plugins and gameplay enhancements, including advanced commands
and special game modes, designed to take your Minecraft
experience to the next level.
</li>
<li>
<strong>Monthly Loot Crates:</strong> Get monthly loot crates
filled with rare items, unique cosmetics, and special bonuses,
keeping your inventory well-stocked with top-tier gear.
</li>
<li>
<strong>Dedicated Support:</strong> Enjoy priority assistance
from our premium support team, ensuring any issues or questions
are resolved quickly so you can focus on the game.
</li>
<li>
<strong>VIP Events & Contests:</strong> Participate in exclusive
events and contests with bigger and better rewards, open only to
CRSS Premium+ members.
</li>
</ul>
<h3>Subscription Details:</h3>
<ul>
<li>
<strong>Subscription Fee:</strong> $6.90 per month, offering
exceptional value for a premium gaming experience.
</li>
<li>
<strong>Contract Term:</strong> Flexible monthly subscription
with the freedom to cancel anytime.
</li>
<li>
<strong>Instant Activation:</strong> Your CRSS Premium+ benefits
activate immediately upon subscription, so you can start
enjoying all the perks right away.
</li>
</ul>
<h3>Join CRSS Premium+ Today</h3>
<span>
For just $6.90 per month, you can access the ultimate Minecraft
experience with CRSS Premium+. Don&apos;t miss out on the
exclusive benefits that come with being a premium member of our
community. For more information or to subscribe, visit our website
at <Link href="/">crss.cc</Link> or contact our support team
at&nbsp;
<a href="mailto:admin@theclashfruit.me">admin@theclashfruit.me</a>
.
</span>
</span>
<center>
{' '}
<h2>Compare our plans</h2>
</center>
<div className={styles.subLayout}>
<div className={styles.card}>
<h1>CRSS Free</h1>
<ul>
<li>9 inventory slots</li>
<li>Full access to inventory</li>
<li>Unrestricted use of chat</li>
</ul>
<h3>Current Plan</h3>
</div>
<div className={styles.card}>
<h1>CRSS Premium+</h1>
<ul>
<li>Ad-free browsing</li>
<li>Access to special kits and gamemodes</li>
<li>Browse the web ingame</li>
<li>Exclusive capes</li>
</ul>
<h3>$6.90</h3>
<strong>Best Value</strong>
</div>
<div className={styles.card}>
<h1>CRSS EnterpriseX</h1>
<ul>
<li>Free flight</li>
<li>Creative mode</li>
<li>Add your own custom cape</li>
<li>Create your own items</li>
<li>Enterprise-grade management solutions</li>
</ul>
<h3>Contact Us</h3>
</div>
</div>
</div>
</PageContent>
<Footer />
</>
);
}

View file

@ -1,84 +0,0 @@
import AdBanner from '@/components/AdBanner';
import Footer from '@/components/Footer';
import Meta from '@/components/Meta';
import NavBar from '@/components/NavBar';
import PageContent from '@/components/PageContent';
export default function Rules() {
return (
<>
<Meta page={{ title: 'Rules' }} />
<NavBar currentPage="rules" />
<PageContent>
<h1 id="rules">Rules</h1>
<ol>
<li>
The use of modified clients that give an unfair advantage to
players, such as hacked clients, is not permitted.
<ul>
<li>
You are not allowed to use them even for their legitimate
features, such as a &quot;fullbright&quot; option.
</li>
<li>
If admins suspect you are hacking you will be immediately
banned.
</li>
</ul>
</li>
<li>
Do not modify or destroy (grief) other player&apos;s constructions
without their consent, or steal any of their items.
<ul>
<li>
You are allowed to visit any build, as long as you don&apos;t
take anything, and if you do you pay them back.
</li>
<li>
You should ask permission in the discord or the in-game chat
before modifying builds.
</li>
</ul>
</li>
<li>
Follow the laws of the nations you are in to avoid issues with other
players and making the server not fun to play.
<ul>
<li>
If you feel the laws are too vague, feel free to ask the people
in charge of them what they mean with something, and feel free
to contribute to them. Complaining that they don&apos;t make
sense won&apos;t get you anywhere.
</li>
<li>
Breaking laws won&apos;t necessarily get you banned, the nation
you are in will take measures and punish you for your actions as
they see fit.
</li>
</ul>
</li>
<li>
Do not attempt to make nations where the territory is already owned
by another nation.
<ul>
<li>
You can make it near the borders of a nation but never inside
one, you can&apos;t just take existing territory as your own.
</li>
<li>
Other nations are free to claim more territory whenever they
feel like it, as long as it doesn&apos;t take other
nations&apos; territory with it.
</li>
</ul>
</li>
</ol>
</PageContent>
<Footer />
</>
);
}

View file

@ -1,167 +0,0 @@
import Footer from '@/components/Footer';
import NavBar from '@/components/NavBar';
import PageContent from '@/components/PageContent';
import Database from '@/lib/Database';
import { notFound } from 'next/navigation';
import { useEffect, useState } from 'react';
import cookie from 'cookie';
import Meta from '@/components/Meta';
import { UAParser } from 'ua-parser-js';
import styles from '@/styles/Settings.module.scss';
import { X } from 'lucide-react';
interface SettingsType {
animations: boolean;
ads: boolean;
}
export default function Settings({ sessions }: { sessions: any[] }) {
const [ settings, setSettings ] = useState<SettingsType>({
animations: true,
ads: true
});
useEffect(() => {
const settings = localStorage.getItem('crss_settings');
if (settings !== null) {
setSettings(JSON.parse(settings));
}
}, []);
useEffect(() => {
localStorage.setItem('crss_settings', JSON.stringify(settings));
}, [ settings ]);
const currentSession = sessions.find(s => s.current);
const otherSessions = sessions.filter(s => !s.current);
return (
<>
<Meta page={{ title: 'Settings' }} />
<NavBar currentPage="settings" />
<PageContent>
<h1>Settings</h1>
{ /* TODO: Move it to the css 😒 */ }
<div style={{ display: 'flex', flexDirection: 'column', gap: '.5rem', marginBottom: '1rem' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<label htmlFor="ani">Animations</label>
<input id="ani" type="checkbox" checked={settings.animations} onChange={e => setSettings({ ...settings, animations: e.target.checked })} />
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<label htmlFor="ads">Ads</label>
<input id="ads" type="checkbox" checked={settings.ads} onChange={e => setSettings({ ...settings, ads: e.target.checked })} />
</div>
</div>
<h2>Sessions</h2>
<p>
View and manage your active sessions.
</p>
<h3>Current Session</h3>
<div className={styles.sessions}>
<div className={styles.sessionCard}>
<div>
<p>{currentSession.user_agent.os.name} {currentSession.user_agent.os.version} &bull; {currentSession.user_agent.browser.name} {currentSession.user_agent.browser.version}</p>
<p>{new Date(currentSession.created).toLocaleString('en-GB')}</p>
</div>
</div>
</div>
<h3>Other Sessions</h3>
<div className={styles.sessions}>
{otherSessions.map(session => (
<div key={session.id} className={styles.sessionCard}>
<div>
<p>{session.user_agent.os.name} {session.user_agent.os.version} &bull; {session.user_agent.browser.name} {session.user_agent.browser.version}</p>
<p>{new Date(session.created).toLocaleString('en-GB')}</p>
</div>
<button onClick={() => { alert('Not yet implemented, yeah...'); }}>
<X />
</button>
</div>
))}
</div>
</PageContent>
<Footer />
</>
);
}
export async function getServerSideProps(context: any) {
const db = new Database();
const cookies = cookie.parse(context.req.headers.cookie);
if (!cookies.session)
return {
notFound: true
};
const session = await db.getSession(cookies.session);
if (!session)
return {
notFound: true
};
const sessions = await db.getUserSessions(session.uid);
if (!sessions)
return {
notFound: true
};
sessions.forEach((s: any) => {
s.current = session.id === s.id;
s.created = s.created.toISOString();
s.expires = s.expires.toISOString();
const ua = new UAParser(s.user_agent).getResult();
s.user_agent = {
ua: ua.ua,
browser: {
name: ua.browser.name || null,
version: ua.browser.version || null,
major: ua.browser.major || null
},
engine: {
name: ua.engine.name || null,
version: ua.engine.version || null
},
os: {
name: ua.os.name || null,
version: ua.os.version || null
},
device: {
vendor: ua.device.vendor || null,
model: ua.device.model || null,
type: ua.device.type || null
},
cpu: {
architecture: ua.cpu.architecture || null
}
};
});
return {
props: {
sessions
}
};
}

View file

@ -1,64 +0,0 @@
import Footer from '@/components/Footer';
import Meta from '@/components/Meta';
import NavBar from '@/components/NavBar';
import PageContent from '@/components/PageContent';
import Database from '@/lib/Database';
import {
Permission,
hasPermission
} from '@/utils/permissions';
import Image from 'next/image';
export default function User({ user }: { user: any }) {
return (
<>
<Meta page={{ title: user.global_name, user }} />
<NavBar currentPage="user" />
<PageContent>
<div className="container">
<h1>{user.global_name}</h1>
<Image src={`https://cdn.discordapp.com/avatars/${user.did}/a_${user.avatar}.png`} alt={user.global_name} width={128} height={128} />
<ul>
<li>Admin: {hasPermission(user.permissions, Permission.Admin) ? 'Yes' : 'No'}</li>
</ul>
<p>Lorem, ipsum dolor sit amet consectetur adipisicing elit. Deserunt perferendis exercitationem aliquid? Corrupti, veniam nam quis, quas, reprehenderit similique perspiciatis veritatis consectetur quidem omnis iste placeat quod! Dolore, labore est.</p>
</div>
</PageContent>
<Footer />
</>
);
}
export async function getServerSideProps(context: any) {
const db = new Database();
const user = await db.getUserUsername(context.query.username as string);
if (!user) {
return {
notFound: true
};
}
return {
props: {
user: {
id: user.id,
did: user.did,
username: user.username,
global_name: user.global_name,
avatar: user.avatar,
banner: user.banner,
accent_color: user.accent_color,
permissions: user.permissions
}
}
};
}

File diff suppressed because it is too large Load diff

View file

@ -1 +0,0 @@
<svg width="146" height="96" viewBox="0 0 146 96" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M83.7827 12.6087C83.7827 10.6157 85.3984 9 87.3914 9C89.3844 9 91.0001 10.6157 91.0001 12.6087C91.0001 14.6017 89.3844 16.2174 87.3914 16.2174C85.3984 16.2174 83.7827 14.6017 83.7827 12.6087Z" fill="currentColor"/><path d="M32.6594 16.4528L41.9008 32.5134L50.8527 17.4646L54.8404 29.75L55.9553 28.8478L66.5686 10.2029" stroke="currentColor" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/><path fill-rule="evenodd" clip-rule="evenodd" d="M8 19.8261C8 17.8331 9.61567 16.2174 11.6087 16.2174C13.6017 16.2174 15.2174 17.8331 15.2174 19.8261C15.2174 21.8191 13.6017 23.4348 11.6087 23.4348C9.61567 23.4348 8 21.8191 8 19.8261Z" fill="currentColor"/><path d="M21.25 89.9906C27.4531 89.9906 33.0938 87.3656 36.6094 82.3656C37.0625 81.725 37.1875 81.0844 37.1875 80.4438C37.1875 78.3969 35.6406 77.2406 33.9844 77.2406C33.0312 77.2406 32.25 77.5063 31.4844 78.5219C29.4375 81.2094 26.2969 83.8344 21.25 83.8344C14.5938 83.8344 8.95312 79.0375 8.95312 70.725C8.95312 61.4438 15.4844 52.5375 22.0781 52.5375C25.5312 52.5375 27.5156 53.6313 28.8594 55.0375V55.4906C28.8594 56.8344 30.1406 58.0531 31.875 58.0531C33.6562 58.0531 35.0781 56.5688 35.0781 54.85V50.7563C35.0781 49.1469 33.8594 47.8031 32.125 47.8031C31.2344 47.8031 30.5938 48.1938 30.2031 48.5688C28.2812 47.4281 25.4062 46.3344 22.5312 46.3344C12.7344 46.3344 2.5625 57.9125 2.5625 70.725C2.5625 82.8813 11.0781 89.9906 21.25 89.9906ZM46.4015 89.9906C48.0578 89.9906 49.5421 88.6469 49.5421 86.85V75.2563C51.0734 75.0063 52.6046 74.8813 53.8234 74.8813C56.0578 74.8813 60.9953 75.6469 68.9328 88.3188C69.5734 89.35 70.5265 89.9906 71.7453 89.9906C73.6671 89.9906 74.8859 88.4438 74.8859 86.85C74.8859 86.3969 74.8234 85.8813 74.5578 85.3813C71.3546 78.975 66.9484 74.7563 63.9328 72.3813C68.9953 70.1469 73.4015 66.4281 73.4015 59.975C73.4015 51.3344 65.339 46.3344 51.0109 46.3344H48.3859C43.964 46.3344 43.2609 46.9125 43.2609 50.1781V86.85C43.2609 88.6469 44.6046 89.9906 46.4015 89.9906ZM49.5421 69.3813V52.475H50.8234C61.0578 52.475 67.0734 55.0375 67.0734 59.975C67.0734 66.1156 57.339 68.8656 49.5421 69.3813ZM93.0573 89.9906C101.37 89.9906 108.479 84.475 108.479 77.1156C108.479 70.85 103.807 66.8188 95.3541 64.3813C88.4479 62.3969 85.5104 60.5375 85.5104 57.5375C85.5104 54.3344 88.4479 52.225 94.276 52.225C99.0104 52.225 101.885 54.0844 102.214 56.3813C102.401 57.85 103.62 59.0688 105.151 59.0688C106.948 59.0688 108.292 57.6625 108.292 55.7406C108.292 50.1156 103.104 46.3344 94.276 46.3344C84.9948 46.3344 79.2916 51.1313 79.2916 57.9906C79.2916 65.2875 85.8854 68.4125 93.1823 70.4594C100.229 72.4438 102.339 74.8188 102.339 77.4438C102.339 81.0844 98.5573 83.975 93.1823 83.975C89.3385 83.975 84.9323 82.1781 83.526 78.2719C83.0104 76.8656 81.9166 75.8969 80.3229 75.8969C78.4635 75.8969 77.0573 77.3813 77.0573 79.1C77.0573 79.8031 77.3073 80.6469 77.7604 81.6C79.8073 85.9594 85.2448 89.9906 93.0573 89.9906ZM128.001 89.9906C136.314 89.9906 143.423 84.475 143.423 77.1156C143.423 70.85 138.751 66.8188 130.298 64.3813C123.392 62.3969 120.454 60.5375 120.454 57.5375C120.454 54.3344 123.392 52.225 129.22 52.225C133.954 52.225 136.829 54.0844 137.158 56.3813C137.345 57.85 138.564 59.0688 140.095 59.0688C141.892 59.0688 143.236 57.6625 143.236 55.7406C143.236 50.1156 138.048 46.3344 129.22 46.3344C119.939 46.3344 114.236 51.1313 114.236 57.9906C114.236 65.2875 120.829 68.4125 128.126 70.4594C135.173 72.4438 137.283 74.8188 137.283 77.4438C137.283 81.0844 133.501 83.975 128.126 83.975C124.283 83.975 119.876 82.1781 118.47 78.2719C117.954 76.8656 116.861 75.8969 115.267 75.8969C113.408 75.8969 112.001 77.3813 112.001 79.1C112.001 79.8031 112.251 80.6469 112.704 81.6C114.751 85.9594 120.189 89.9906 128.001 89.9906Z" fill="currentColor"/></svg>

Before

Width:  |  Height:  |  Size: 3.8 KiB

View file

@ -1,94 +0,0 @@
@import 'variables.module';
@import 'global.module';
.teamList {
display: grid;
gap: 1rem;
grid-template-columns: repeat(3, minmax(200px, 1fr));
> .teamCard {
padding: 0;
width: 100%;
> .memberBanner {
position: relative;
height: 150px;
background: #537f53;
border-top-left-radius: calc(1rem - 1px);
border-top-right-radius: calc(1rem - 1px);
> img {
position: absolute;
bottom: -52px;
left: 16px;
border: 4px solid $colorSurfaceLight2;
border-radius: 1rem;
@media (prefers-color-scheme: dark) {
border-color: $colorSurfaceDark2;
}
}
&[data-has-background] {
background: var(--background);
}
}
> .memberContent {
padding: 1rem;
> .memberLinks {
display: flex;
justify-content: end;
align-items: end;
gap: .5rem;
list-style: none;
margin: 0 0 .5rem;
> li {
> a {
display: flex;
align-items: center;
padding: 4px;
color: $colorBorderLight3;
&:hover {
color: $colorPrimary;
}
@media (prefers-color-scheme: dark) {
color: $colorBorderDark3;
}
}
}
}
> h3 {
margin: 0;
}
}
}
@media (max-width: 992px) {
grid-template-columns: repeat(2, minmax(200px, 1fr));
}
@media (max-width: 768px) {
grid-template-columns: 1fr;
}
}

View file

@ -1,111 +0,0 @@
@import 'variables.module';
@import 'global.module';
.adBanner {
width: 100%;
height: 106px;
padding: 8px;
background: $colorSurfaceLight3;
border: 1px solid $colorBorderLight1;
border-radius: 1rem;
position: relative;
> .adContent {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
> .adRevive {
position: absolute;
top: 8px;
bottom: 8px;
left: 8px;
right: 8px;
display: flex;
align-items: center;
justify-content: center;
scale: 1.3;
@media (max-width: 768px) {
scale: 1;
height: 60px;
}
}
> .adInfo {
position: absolute;
top: 8px;
right: 8px;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 8px;
background: transparent;
border: none;
border-radius: 100%;
color: $colorTextLight1;
transition: 150ms;
&:hover {
background: rgba($colorPrimary, 0.25);
}
@media (max-width: 768px) {
display: none;
}
@media (prefers-color-scheme: dark) {
color: $colorTextDark1;
}
}
@media (max-width: 768px) {
border-color: transparent !important;
background: transparent !important;
height: calc(60px);
padding: 0;
> .adContent {
display: none;
}
> .adRevive {
position: initial;
}
}
@media (prefers-color-scheme: dark) {
background: $colorSurfaceDark3;
border-color: $colorBorderDark1;
}
}

View file

@ -1,17 +0,0 @@
@import 'variables.module';
@import 'global.module';
.card {
background: $colorSurfaceLight2;
border: 1px solid $colorBorderLight1;
border-radius: 1rem;
padding: 1rem;
@media (prefers-color-scheme: dark) {
background: $colorSurfaceDark2;
border-color: $colorBorderDark1;
}
}

View file

@ -1,153 +0,0 @@
@import 'variables.module';
@import 'global.module';
.dropDown {
position: relative;
> label {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
padding: 8px 24px;
}
> .dropDownMenu {
position: absolute;
top: calc(100% + 4px);
right: 0;
background: $colorSurfaceLight4;
border: 1px solid $colorBorderLight1;
border-radius: .5rem;
overflow: hidden;
display: none;
min-width: max(100%, 200px);
> ul {
list-style: none;
display: flex;
flex-direction: column;
margin: 0;
> li {
> a {
padding: 11px 24px;
display: flex;
flex-direction: row;
align-items: center;
gap: 24px;
text-decoration: none;
transition: 150ms;
> .icon {
width: 20px;
height: 20px;
}
&:hover {
background: rgba($colorPrimary, 0.65);
color: $colorSurfaceLight1;
}
}
> .divider {
border-bottom: 1px solid $colorBorderLight1;
width: 100%;
@media (prefers-color-scheme: dark) {
border-color: $colorBorderDark1;
}
}
}
}
@media (prefers-color-scheme: dark) {
border-color: $colorBorderDark1;
background: $colorSurfaceDark4;
}
}
> .mobileOverlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba($colorSurfaceLight1, 0.65);
display: none;
@media (prefers-color-scheme: dark) {
background: rgba($colorSurfaceDark1, 0.65);
}
}
&.open {
> label {
outline: 2px solid rgba($colorPrimary, 0.65);
color: $colorPrimary;
}
> .dropDownMenu {
display: block;
z-index: 1000000;
}
@media (max-width: 768px) {
> .mobileOverlay {
display: block;
z-index: 100000;
}
> .dropDownMenu {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
min-width: fit-content;
min-height: fit-content;
height: fit-content;
width: 80vw;
overflow-y: auto;
border-radius: 1rem;
padding: 8px 0;
> ul {
align-items: initial;
}
}
}
}
}

View file

@ -1,102 +0,0 @@
@import 'variables.module';
@import 'global.module';
.pageFooter {
background: $colorSurfaceLight3;
border-top: 1px solid $colorBorderLight1;
> .container {
display: flex;
justify-content: space-between;
align-items: start;
padding: 1rem 0;
p {
margin-bottom: .5rem;
}
div:first-child {
width: 420px;
}
div:last-child {
text-align: right;
> ul {
display: flex;
gap: 1rem;
list-style: none;
> li {
> a {
display: flex;
align-items: center;
justify-content: center;
width: 31px;
height: 31px;
border-radius: 50%;
padding: 4px;
transition: 150ms;
color: $colorTextLight1;
&:hover {
color: $colorPrimary;
}
@media (prefers-color-scheme: dark) {
color: $colorTextDark1;
}
}
}
}
}
@media (max-width: 768px) {
flex-direction: column;
div:first-child {
width: 100%;
text-align: center;
}
div:last-child {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
margin-top: 1rem;
}
ul {
margin: 0;
margin-bottom: 1.5rem;
}
}
}
@media (prefers-color-scheme: dark) {
background: $colorSurfaceDark3;
border-color: $colorBorderDark1;
}
}

View file

@ -1,97 +0,0 @@
@import 'variables.module';
@import 'global.module';
.nationGrid {
display: grid;
grid-template-columns: repeat(3, minmax(200px, 1fr));
gap: 1rem;
> .nationCard {
position: relative;
display: flex;
flex-direction: column;
width: 100%;
height: 150px;
text-decoration: none;
padding: 1rem;
cursor: pointer;
overflow: hidden;
outline: 1px solid $colorBorderLight1;
border-radius: 1rem;
background: $colorSurfaceLight2;
color: $colorTextLight1;
transition: 0.24s;
> h2 {
margin: 0;
margin-bottom: .1rem;
font-size: 1.2em;
}
> p {
margin: 0;
}
> .icon {
position: absolute;
top: 0;
right: 0;
height: 100%;
object-fit: cover;
mask-image: linear-gradient(to right, transparent, rgba(red, 0.3));
aspect-ratio: 1/1;
transition: 0.64s;
@media (prefers-color-scheme: dark) {
mask-image: linear-gradient(to right, transparent, rgba(red, 0.1));
}
}
&:hover {
outline-width: 2px;
outline-color: $colorPrimary;
transition: 0.08s;
> .icon {
transition: 0.48s;
transform: scale(1.2);
}
}
@media (prefers-color-scheme: dark) {
outline-color: $colorBorderDark1;
background: $colorSurfaceDark2;
color: $colorTextDark1;
}
}
@media (max-width: 992px) {
grid-template-columns: repeat(2, minmax(200px, 1fr));
}
@media (max-width: 768px) {
grid-template-columns: 1fr;
}
}

View file

@ -1,333 +0,0 @@
@import 'variables.module';
@import 'global.module';
.pageHero {
height: 220px;
background-image: url("https://crss.fra1.cdn.digitaloceanspaces.com/img/2024-06-08_14.19.52.png");
background-size: cover;
background-repeat: no-repeat;
background-position: center;
font-family: 'Comic Neue', 'Comic Sans MS', 'Noto Color Emoji', 'Noto Emoji', sans-serif;
font-weight: 600;
> .heroOverlay {
background: rgba($colorSurfaceLight2, 0.9);
backdrop-filter: blur(8px);
height: 100%;
width: 100%;
> .container {
height: 100%;
display: flex;
align-items: center;
justify-content: space-between;
> div:first-child {
display: flex;
flex-direction: column;
gap: 0.5rem;
> h1 {
font-family: 'Comic Neue', 'Comic Sans MS', 'Noto Color Emoji', 'Noto Emoji', sans-serif;
font-weight: 600;
font-size: 1.5rem;
margin: 0;
}
@media (max-width: 768px) {
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
}
> div:last-child {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
> input {
background: rgba(0,0,0,0);
width: fit-content;
padding: 4px 16px;
font-size: 1.5rem;
text-align: center;
color: inherit;
border: 1px solid rgba(0, 0, 0, 0);
border-radius: 1rem;
transition: 150ms;
&:focus, &:hover {
outline: none;
background: rgba($colorSurfaceLight4, .65);
border: 1px solid $colorBorderLight3;
}
@media (prefers-color-scheme: dark) {
color: inherit;
&:focus, &:hover {
background: rgba($colorSurfaceDark4, .65);
border: 1px solid $colorBorderDark3;
}
}
}
@media (max-width: 768px) {
display: none;
}
}
}
@media (prefers-color-scheme: dark) {
background: rgba($colorSurfaceDark2, 0.9);
}
}
@media (max-width: 768px) {
border-bottom: 1px solid $colorBorderLight1;
@media (prefers-color-scheme: dark) {
border-color: $colorBorderDark1;
}
}
}
.navBar {
height: 64px;
position: sticky;
top: 0;
left: 0;
right: 0;
background: $colorSurfaceLight3;
border-bottom: 1px solid $colorBorderLight1;
z-index: 999999999;
> .container {
display: flex;
justify-content: space-between;
align-items: center;
height: 100%;
@media (max-width: 768px) {
justify-content: space-between;
}
> .navMobileContainer {
display: none;
width: 100%;
height: 100%;
justify-content: end;
align-items: center;
z-index: 10000;
> .navToggle {
padding: 0.5rem;
background: none;
border: none;
border-radius: 50%;
display: flex;
color: $colorTextLight1;
transition: 150ms;
&:hover, &:focus {
background: rgba($colorPrimary, 0.5);
}
@media (prefers-color-scheme: dark) {
color: $colorTextDark1;
}
}
@media (max-width: 768px) {
display: flex;
}
}
> .navCollapse {
display: flex;
justify-content: space-between;
align-items: center;
height: 100%;
width: 100%;
z-index: 5000;
> ul {
list-style: none;
display: flex;
align-items: center;
gap: .5rem;
margin: 0;
> li {
> a, .dropDown > label {
padding: 8px 16px;
display: flex;
flex-direction: row;
align-items: center;
gap: 1rem;
text-decoration: none;
transition: 150ms;
border-radius: 2rem;
color: $colorTextLight2;
outline: 2px solid rgba(0, 0, 0, 0);
> .icon {
width: 20px;
height: 20px;
}
&:hover, &:focus, &.active {
outline: 2px solid rgba($colorPrimary, 0.65);
color: $colorPrimary;
}
@media (prefers-color-scheme: dark) {
color: $colorTextDark2;
&:hover, &:focus, &.active {
outline-color: rgba($colorPrimary, 0.65);
}
}
}
}
}
@media (max-width: 768px) {
opacity: 1;
transition: 300ms;
position: fixed;
height: 100vh;
width: 100vw;
top: 0;
right: -110%;
display: flex;
flex: 1;
flex-direction: column;
align-items: end;
justify-content: center;
gap: 2rem;
padding: 1rem;
background: rgba($colorSurfaceLight3, 0.95);
backdrop-filter: blur(8px);
ul {
flex-direction: column;
align-items: end;
gap: 1rem;
li {
a {
padding: 8px 24px;
}
}
}
@media (prefers-color-scheme: dark) {
background: rgba($colorSurfaceDark3, 0.95);
}
}
}
}
@media (prefers-color-scheme: dark) {
background: $colorSurfaceDark3;
border-color: $colorBorderDark1;
}
&.navOpen {
> .container {
flex-direction: column;
> .navMobileContainer {
height: 64px;
}
> .navCollapse {
opacity: 1;
right: 0;
}
}
}
@media (max-width: 768px) {
background: none;
position: fixed;
top: 0;
border: none;
}
}

View file

@ -1,57 +0,0 @@
@import 'variables.module';
.card {
display: flex;
flex-direction: column;
align-items: center;
border-radius: 1.5rem;
padding: 1rem;
gap: 0.25rem;
h1 {
margin: 0px;
}
h2 {
margin: 0px;
}
* {
text-align: center;
}
}
.cardContainer {
display: flex;
flex-direction: column;
gap: 1rem;
}
div.card {
border: 1px solid $colorBorderLight1;
background: linear-gradient(
180deg,
$colorSurfaceLight1 0%,
$colorBorderLight1 200%
);
}
@media (prefers-color-scheme: dark) {
div.card {
border: 1px solid $colorBorderDark1;
background: linear-gradient(
180deg,
$colorSurfaceDark2 0%,
$colorBorderDark1 200%
);
}
}
.subLayout {
display: grid;
gap: 1rem;
}
@media (min-width: 1000px) {
div.subLayout {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}

View file

@ -1,55 +0,0 @@
@import 'variables.module';
@import 'global.module';
.sessions {
display: flex;
flex-direction: column;
gap: .5rem;
> .sessionCard {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
p {
margin: 0;
}
button {
display: flex;
justify-content: center;
align-items: center;
background: transparent;
color: $colorTextLight1;
border: none;
border-radius: 100%;
padding: 8px;
cursor: pointer;
transition: 150ms;
&:hover {
background: rgba($colorPrimary, 0.35);
color: $colorPrimary;
}
@media (prefers-color-scheme: dark) {
color: $colorTextDark1;
}
}
}
&:not(:last-child) {
margin-bottom: 1rem;
}
}

View file

@ -1,43 +0,0 @@
@import url('https://fonts.googleapis.com/css2?family=Comic+Neue:ital,wght@0,300;0,400;0,700;1,300;1,400;1,700&family=Noto+Color+Emoji&family=Noto+Emoji:wght@300..700&family=Noto+Sans+Mono:wght@100..900&family=Noto+Sans:ital,wght@0,100..900;1,100..900&family=Outfit:wght@100..900&display=swap');
body, input, textarea, select, button {
font-family: 'Noto Sans', 'Noto Color Emoji', 'Noto Emoji', sans-serif;
}
code, pre, kbd {
font-family: 'Noto Sans Mono', 'Noto Color Emoji', 'Noto Emoji', sans-serif;
}
$headers: (
h1: 1.95rem,
h2: 1.5rem,
h3: 1.25rem,
h4: 140%,
h5: 120%,
h6: 110%
);
h1, h2, h3, h4, h5, h6 {
font-family: 'Outfit', 'Noto Color Emoji', 'Noto Color Emoji', 'Noto Emoji', sans-serif;
font-weight: 500;
line-height: 1.7;
}
@each $tag, $size in $headers {
#{$tag} {
font-size: $size;
margin-bottom: 1rem;
}
}
p, ol, ul, label, a {
font-size: 1rem;
line-height: 1.7;
}
p:not(:last-child), ol:not(li > ol, li > ul, :last-child), ul:not(li > ol, li > ul, :last-child) {
margin-bottom: 1rem;
}

View file

@ -1,13 +0,0 @@
.container {
max-width: 1100px;
margin: 0 auto;
@media (max-width: 1100px) {
margin: 0 1rem;
}
}
.pageContent {
// trolley
}

View file

@ -1,237 +0,0 @@
@import 'variables.module';
@import 'fonts.module';
@import 'global.module';
* {
padding: 0;
margin: 0;
box-sizing: border-box;
scrollbar-color: $colorPrimary $colorSurfaceLight1;
@media (prefers-color-scheme: dark) {
scrollbar-color: $colorPrimary $colorSurfaceDark1;
}
}
*::selection {
background: rgba($colorPrimary, 0.75);
@media (prefers-color-scheme: dark) {
background: rgba($colorPrimary, 0.65);
}
}
body {
background: $colorSurfaceLight1;
color: $colorTextLight1;
overflow-x: hidden;
@media (prefers-color-scheme: dark) {
background: $colorSurfaceDark1;
color: $colorTextDark1;
}
}
#__next {
display: flex;
flex-direction: column;
min-height: 100svh;
}
main {
padding: 1.5rem 0;
flex: 1 1;
}
a {
color: $colorPrimary;
text-decoration: none;
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
button {
cursor: pointer;
}
ol, ul {
list-style-position: inside;
margin-left: 1rem;
}
pre {
overflow-x: scroll;
max-width: 100%;
display: block;
padding: 1rem;
background: $colorSurfaceLight2;
border: 1px solid $colorBorderLight1;
border-radius: .5rem;
@media (prefers-color-scheme: dark) {
background: $colorSurfaceDark2;
border-color: $colorBorderDark1;
}
}
iframe {
background: $colorSurfaceLight2;
border: 1px solid $colorBorderLight1;
border-radius: 1rem;
@media (prefers-color-scheme: dark) {
background: $colorSurfaceDark2;
border-color: $colorBorderDark1;
}
}
hr {
border: none;
height: 1px;
background: $colorBorderLight1;
margin: 1rem 0;
@media (prefers-color-scheme: dark) {
background: $colorBorderDark1;
}
}
// inputs
input {
&[type='checkbox'] {
appearance: none;
width: 52px;
height: 2rem;
background: $colorSurfaceLight2;
border: 1px solid $colorBorderLight1;
border-radius: 1rem;
position: relative;
cursor: pointer;
transition: 150ms;
&::before {
content: "";
position: absolute;
height: 1rem;
width: 1rem;
border-radius: 50%;
background: $colorPrimary;
top: 50%;
left: .5rem;
transform: translateY(-50%);
transition: 150ms;
}
&:checked {
background: $colorPrimary;
&::before {
background: $colorSurfaceLight2;
transform: translate(20px, -50%);
animation: thumbAnimation 150ms;
}
}
@media (prefers-color-scheme: dark) {
background: $colorSurfaceDark2;
border-color: $colorBorderDark1;
&::before {
background: $colorPrimary;
}
&:checked {
background: $colorPrimary;
&::before {
background: $colorSurfaceDark2;
transform: translate(20px, -50%);
animation: thumbAnimation 150ms;
}
}
}
}
}
select {
appearance: none;
padding: .5rem 1rem;
background: $colorSurfaceLight2;
color: $colorTextLight1;
border: 1px solid $colorBorderLight1;
border-radius: .5rem;
cursor: pointer;
transition: 150ms;
&:hover {
border-color: $colorPrimary;
}
&:focus {
outline: none;
border-color: $colorPrimary;
}
@media (prefers-color-scheme: dark) {
background: $colorSurfaceDark2;
color: $colorTextDark1;
border-color: $colorBorderDark1;
}
}
@keyframes thumbAnimation {
from {
transform: translateY(-50%);
}
to {
transform: translate(20px, -50%);
}
}

View file

@ -1,27 +0,0 @@
$colorPrimary: hsl(120, 21%, 41%);
$colorTextLight1: hsl(120, 40%, 8%);
$colorTextLight2: hsl(120, 5%, 45%);
$colorSurfaceLight1: hsl(120, 25%, 97%);
$colorSurfaceLight2: hsl(120, 20%, 95%);
$colorSurfaceLight3: hsl(120, 20%, 92%);
$colorSurfaceLight4: hsl(120, 20%, 85%);
$colorBorderLight1: hsl(120, 20%, 80%);
$colorBorderLight2: hsl(120, 20%, 70%);
$colorBorderLight3: hsl(120, 20%, 60%);
$colorBorderLight4: hsl(120, 20%, 50%);
$colorTextDark1: hsl(120, 15%, 85%);
$colorTextDark2: hsl(120, 5%, 65%);
$colorSurfaceDark1: hsl(120, 5%, 10%);
$colorSurfaceDark2: hsl(120, 10%, 15%);
$colorSurfaceDark3: hsl(120, 5%, 20%);
$colorSurfaceDark4: hsl(120, 5%, 25%);
$colorBorderDark1: hsl(120, 5%, 30%);
$colorBorderDark2: hsl(120, 5%, 40%);
$colorBorderDark3: hsl(120, 5%, 50%);
$colorBorderDark4: hsl(120, 5%, 60%);

16
template/404.twig Normal file
View file

@ -0,0 +1,16 @@
{% include 'includes/head.twig' with {'pageTitle': '404'} %}
{% include 'includes/hero.twig' %}
{% include 'includes/nav.twig' with {'page': '404',} %}
<main class="pageContent container">
<h1>Not found! :<</h1>
<p>
This page is nowhere to be found!<br>
Go on Discord and complain about it to Clash or Mya!!
</p>
</main>
{% include 'includes/footer.twig' %}
{% include 'includes/foot.twig' %}

View file

@ -0,0 +1,12 @@
<script src="/js/admin/nav.js"></script>
<script src="https://unpkg.com/lucide@latest"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script>
lucide.createIcons();
changePage();
</script>
</body>
</html>

View file

@ -0,0 +1,35 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<title>{{ pageTitle }} - Admin &bull; Clyde's Real Survival SMP</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" />
<style>
body {
min-height: 100svh;
}
.lucide {
vertical-align: -10%;
}
/* Loading Animation */
.loader {
animation: spin 1s linear infinite;
}
@keyframes spin {
100% {
-webkit-transform: rotate(360deg);
transform:rotate(360deg);
}
}
</style>
</head>
<body class="d-flex flex-column">

View file

@ -0,0 +1,52 @@
<div class="d-flex flex-column flex-shrink-0 p-3 bg-body-tertiary" style="width: 280px;">
<a href="/" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto link-body-emphasis text-decoration-none">
<i class="pe-none me-2" data-lucide="gauge" width="40" height="32"></i>
<span class="fs-4">Admin Panel</span>
</a>
<hr>
<ul class="nav nav-pills flex-column mb-auto">
<li class="nav-item">
<a href="#/" class="nav-link active" aria-current="page">
<i class="pe-none me-2" data-lucide="home" width="16" height="16"></i>
Home
</a>
</li>
<li class="nav-item">
<a href="#/pages" class="nav-link link-body-emphasis">
<i class="pe-none me-2" data-lucide="notebook-text" width="16" height="16"></i>
Pages
</a>
</li>
<li class="nav-item">
<a href="#/users" class="nav-link link-body-emphasis">
<i class="pe-none me-2" data-lucide="users" width="16" height="16"></i>
Users
</a>
</li>
<li class="nav-item">
<a href="#/markers" class="nav-link link-body-emphasis">
<i class="pe-none me-2" data-lucide="map-pin" width="16" height="16"></i>
Markers
</a>
</li>
</ul>
<hr>
<div class="dropdown">
<a href="#" class="d-flex align-items-center link-body-emphasis text-decoration-none dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<img src="https://cdn.discordapp.com/avatars/{{ user.id }}/{{ user.avatar }}.png" alt="" width="32" height="32" class="rounded-circle me-2">
<span>{{ user.global_name }}</span>
</a>
<ul class="dropdown-menu text-small shadow">
<li><a class="dropdown-item" href="#">New project...</a></li>
<li><a class="dropdown-item" href="#">Settings</a></li>
<li><a class="dropdown-item" href="#">Profile</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="/">Exit Admin</a></li>
</ul>
</div>
</div>

11
template/admin/index.twig Normal file
View file

@ -0,0 +1,11 @@
{% include 'admin/includes/head.twig' with {'pageTitle': 'Dashboard'} %}
<div class="d-flex flex-grow-1">
{% include 'admin/includes/sidebar.twig' %}
<div class="flex-grow-1 pageContainer">
</div>
</div>
{% include 'admin/includes/foot.twig' %}

View file

@ -0,0 +1,3 @@
<div class="d-flex align-items-center justify-content-center h-100">
<h1>Not Found :(</h1>
</div>

View file

@ -0,0 +1,3 @@
<div class="d-flex align-items-center justify-content-center h-100">
<h1>Welcome {{ user.global_name }} to the admin panel!</h1>
</div>

View file

@ -0,0 +1,24 @@
<table class="table">
<thead>
<tr>
<th scope="col">id</th>
<th scope="col">name</th>
<th scope="col">category</th>
<th scope="col">data</th>
<th scope="col">actions</th>
</tr>
</thead>
<tbody>
{% for marker in markers %}
<tr>
<th scope="row">{{ marker.id }}</th>
<td>{{ marker.name }}</td>
<td>{{ marker.category }}</td>
<td>{{ marker.data }}</td>
<td>
<button type="button" class="btn btn-success" data-bs-toggle="modal" data-bs-target="#editModal">Edit</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>

View file

@ -0,0 +1 @@
pages

Some files were not shown because too many files have changed in this diff Show more