[Add] Team section & teams post

This commit is contained in:
Alex Emmet 2026-04-22 13:52:22 +02:00
commit c2d03efc57
15 changed files with 1063 additions and 53 deletions

369
file_graph_generator.py Normal file
View file

@ -0,0 +1,369 @@
"""
File Graph Generator
Creates a tree-like visualization of files with their content,
respecting .gitignore patterns with proper tree rendering.
"""
import argparse
import fnmatch
import logging
import os
import sys
from dataclasses import dataclass, field
from pathlib import Path
from typing import Iterator, List, Optional
# ───────────────────────────── Logging Setup ──────────────────────────────
def setup_logging(level: int = logging.INFO) -> None:
"""Configure structured logging to stderr (stdout is reserved for output)."""
logging.basicConfig(
stream=sys.stderr,
level=level,
format="%(asctime)s | %(levelname)-8s | %(message)s",
datefmt="%H:%M:%S",
)
logger = logging.getLogger("file_graph")
# ─────────────────────────── Gitignore Engine ─────────────────────────────
@dataclass
class GitignoreRule:
"""A single parsed gitignore rule."""
pattern: str
is_negation: bool = False
is_dir_only: bool = False
is_anchored: bool = False
is_relative: bool = False # contains / (not at start)
class GitignoreParser:
"""Parse a .gitignore file into ordered rules."""
@staticmethod
def parse(path: Path) -> List[GitignoreRule]:
rules = []
if not path.exists():
return rules
try:
with path.open("r", encoding="utf-8", errors="ignore") as f:
for line in f:
line = line.rstrip("\n\r")
# Skip empty lines and comments
if not line or line.startswith("#"):
continue
is_negation = line.startswith("!")
if is_negation:
line = line[1:]
# Handle escaped ! and #
if line.startswith("\\!") or line.startswith("\\#"):
line = line[1:]
line = line.rstrip()
if not line:
continue
is_dir_only = line.endswith("/")
if is_dir_only:
line = line[:-1]
is_anchored = line.startswith("/")
if is_anchored:
line = line[1:]
is_relative = "/" in line
rules.append(
GitignoreRule(
pattern=line,
is_negation=is_negation,
is_dir_only=is_dir_only,
is_anchored=is_anchored,
is_relative=is_relative,
)
)
except OSError as e:
logger.warning(f"Could not read {path}: {e}")
return rules
class GitignoreChecker:
"""Check if paths should be ignored, respecting per-directory .gitignore files."""
def __init__(self, root: Path):
self.root = root.resolve()
self._cache: dict[Path, List[GitignoreRule]] = {}
self._skip_dirs = {
".git",
"__pycache__",
".pytest_cache",
".mypy_cache",
".tox",
".venv",
"venv",
}
self._skip_files = {
"file_graph_generator.py",
"file_graph_generator_v2.py",
"file_graph_generator_v3.py",
"file_tree.txt",
}
def _load(self, directory: Path) -> List[GitignoreRule]:
"""Load rules for a directory (cached)."""
directory = directory.resolve()
if directory not in self._cache:
gitignore_path = directory / ".gitignore"
if gitignore_path.exists():
logger.debug(f"Parsing gitignore: {gitignore_path}")
self._cache[directory] = GitignoreParser.parse(gitignore_path)
return self._cache[directory]
def _match_rule(
self, rule: GitignoreRule, name: str, rel_path: str, is_dir: bool
) -> bool:
"""Check if a single rule matches."""
if rule.is_dir_only and not is_dir:
return False
# Anchored pattern: match only at .gitignore level
if rule.is_anchored:
return fnmatch.fnmatch(rel_path, rule.pattern) or fnmatch.fnmatch(
rel_path, rule.pattern + "/"
)
# Relative path pattern (contains /): match against rel_path
if rule.is_relative:
return fnmatch.fnmatch(rel_path, rule.pattern) or fnmatch.fnmatch(
rel_path, rule.pattern + "/"
)
# Basename pattern: match name at any level
return fnmatch.fnmatch(name, rule.pattern)
def is_ignored(self, path: Path, is_dir: bool) -> bool:
"""Check if path should be ignored, walking up to root."""
name = path.name
# Fast-path: always skip common VCS/build dirs and script/output files
if is_dir and name in self._skip_dirs:
logger.debug(f"Skipping common dir: {name}")
return True
if not is_dir and name in self._skip_files:
logger.debug(f"Skipping script/output file: {name}")
return True
# Walk from current directory up to root
current = path.parent.resolve()
while True:
rules = self._load(current)
if rules:
rel_path = path.relative_to(current).as_posix()
ignored = False
for rule in rules:
if self._match_rule(rule, name, rel_path, is_dir):
if rule.is_negation:
ignored = False
else:
ignored = True
if ignored:
logger.debug(f"Ignored by gitignore: {path}")
return True
if current == self.root:
break
current = current.parent
return False
# ──────────────────────────── File Reading ────────────────────────────────
MAX_CONTENT_BYTES = 50 * 100 # ~50 lines avg
def is_binary(path: Path) -> bool:
"""Fast binary detection using first 8KB chunk."""
try:
with path.open("rb") as f:
chunk = f.read(8192)
if not chunk:
return False
if b"\x00" in chunk:
return True
if chunk[:2] in (b"\xff\xfe", b"\xfe\xff", b"\x1f\x8b"):
return True
return False
except OSError:
return True
def read_content(path: Path, max_lines: int = 50) -> Iterator[str]:
"""Yield content lines with minimal memory footprint."""
if is_binary(path):
yield "[binary file — content skipped]"
return
try:
with path.open("r", encoding="utf-8", errors="replace") as f:
for i, line in enumerate(f):
if i >= max_lines:
remaining = sum(1 for _ in f)
yield f"... ({remaining} more lines)"
return
yield line.rstrip("\n")
except OSError as e:
yield f"[Error reading file: {e}]"
# ─────────────────────────── Tree Generator ───────────────────────────────
def get_visible_children(path: Path, checker: GitignoreChecker) -> List[os.DirEntry]:
"""Get sorted, filtered children of a directory."""
try:
entries = list(os.scandir(path))
except PermissionError:
logger.warning(f"Permission denied: {path}")
return []
except OSError as e:
logger.error(f"Error scanning {path}: {e}")
return []
visible = []
for entry in entries:
# Skip .gitignore files entirely
if entry.name == ".gitignore":
continue
child_path = Path(entry.path)
is_dir = entry.is_dir()
if not checker.is_ignored(child_path, is_dir):
visible.append(entry)
# Sort: dirs first, then files, both alphabetically (case-insensitive)
visible.sort(key=lambda e: (not e.is_dir(), e.name.lower()))
return visible
def build_tree_lines(
path: Path,
checker: GitignoreChecker,
prefix: str = "",
max_lines: int = 50,
max_depth: Optional[int] = None,
current_depth: int = 0,
) -> Iterator[str]:
"""Recursively yield tree lines. Correctly handles is_last filtering."""
if path.is_dir():
if max_depth is not None and current_depth >= max_depth:
return
children = get_visible_children(path, checker)
count = len(children)
for i, entry in enumerate(children):
is_last = i == count - 1
connector = "└── " if is_last else "├── "
name = entry.name
if entry.is_dir():
yield prefix + connector + name + "/"
extension = " " if is_last else ""
yield from build_tree_lines(
Path(entry.path),
checker,
prefix + extension,
max_lines,
max_depth,
current_depth + 1,
)
else:
yield prefix + connector + name
content_prefix = prefix + (" " if is_last else "") + " "
for content_line in read_content(Path(entry.path), max_lines):
yield content_prefix + content_line
# ─────────────────────────────── Main ─────────────────────────────────────
def main() -> None:
parser = argparse.ArgumentParser(
description="File tree generator with gitignore support",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
%(prog)s # Scan current directory
%(prog)s ./src -o tree.txt # Scan ./src, write to tree.txt
%(prog)s --max-depth 3 # Limit depth to 3 levels
%(prog)s --max-lines 20 # Show only 20 lines per file
%(prog)s -v # Verbose logging
%(prog)s --no-gitignore # Ignore all .gitignore files
""",
)
parser.add_argument(
"directory", nargs="?", default=".", help="Root directory to scan"
)
parser.add_argument("-o", "--output", default="file_tree.txt", help="Output file")
parser.add_argument(
"--max-depth", type=int, default=None, help="Maximum directory depth"
)
parser.add_argument(
"--max-lines", type=int, default=50, help="Max lines to show per file"
)
parser.add_argument(
"--no-gitignore", action="store_true", help="Disable .gitignore processing"
)
parser.add_argument(
"-v", "--verbose", action="store_true", help="Enable debug logging"
)
args = parser.parse_args()
setup_logging(logging.DEBUG if args.verbose else logging.INFO)
root = Path(args.directory).resolve()
if not root.is_dir():
logger.error(f"Not a directory: {root}")
sys.exit(1)
checker = GitignoreChecker(root)
if args.no_gitignore:
# Disable by making all paths return empty rules
checker._cache = {d: [] for d in checker._cache}
# Also override _load to always return empty
checker._load = lambda d: []
logger.info("Gitignore processing disabled")
try:
with open(args.output, "w", encoding="utf-8") as f:
# Print root
f.write(root.name + "/\n")
# Print tree
for line in build_tree_lines(
root, checker, max_lines=args.max_lines, max_depth=args.max_depth
):
f.write(line + "\n")
logger.info(f"Output written to: {args.output}")
except OSError as e:
logger.error(f"Failed to write output: {e}")
sys.exit(1)
if __name__ == "__main__":
main()

173
file_tree.txt Normal file
View file

@ -0,0 +1,173 @@
Anarchyphase-Website/
├── server/
│ ├── src/
│ │ └── main.rs
│ │ use axum::{
│ │ body::Body,
│ │ extract::{Path, State},
│ │ http::{header, StatusCode, Uri},
│ │ response::{IntoResponse, Response},
│ │ ... (194 more lines)
│ ├── Cargo.lock
│ │ # This file is automatically @generated by Cargo.
│ │ # It is not intended for manual editing.
│ │ version = 4
│ │
│ │ [[package]]
│ │ ... (1114 more lines)
│ └── Cargo.toml
│ [package]
│ name = "server"
│ version = "0.1.0"
│ edition = "2021"
│ ... (11 more lines)
├── website/
│ ├── api/
│ │ ├── backdrops.json
│ │ │ [
│ │ │ {
│ │ │ "name": "horse_fog",
│ │ │ "destructive": false
│ │ │ },
│ │ │ ... (26 more lines)
│ │ ├── posts.json
│ │ │ [
│ │ │ {
│ │ │ "date": "2026-04-21, by AlexEmmet",
│ │ │
│ │ │ "head_img": "village_sunset.png",
│ │ │ ... (32 more lines)
│ │ ├── staff.json
│ │ │ [
│ │ │ {
│ │ │ "icon": "morice.png",
│ │ │ "name": "MoriceMC",
│ │ │ "priority": 0,
│ │ │ ... (48 more lines)
│ │ └── teams.json
│ │ [
│ │ {
│ │ "title": "Anarchyphase Public Infrastructure",
│ │ "acronym": "API",
│ │ "leader": "NiniVX",
│ │ ... (5 more lines)
│ ├── assets/
│ │ ├── ahnyokatzin.png
│ │ │ [binary file — content skipped]
│ │ ├── alexemmet.png
│ │ │ [binary file — content skipped]
│ │ ├── anarchyphase.png
│ │ │ [binary file — content skipped]
│ │ ├── api.png
│ │ │ [binary file — content skipped]
│ │ ├── banner.png
│ │ │ [binary file — content skipped]
│ │ ├── burrata.png
│ │ │ [binary file — content skipped]
│ │ ├── campsite.png
│ │ │ [binary file — content skipped]
│ │ ├── harrpy.png
│ │ │ [binary file — content skipped]
│ │ ├── harrpyrose.png
│ │ │ [binary file — content skipped]
│ │ ├── horse_fog.png
│ │ │ [binary file — content skipped]
│ │ ├── horse_rainbow.png
│ │ │ [binary file — content skipped]
│ │ ├── icon.png
│ │ │ [binary file — content skipped]
│ │ ├── morice.png
│ │ │ [binary file — content skipped]
│ │ ├── ninivx.png
│ │ │ [binary file — content skipped]
│ │ ├── outpost.png
│ │ │ [binary file — content skipped]
│ │ ├── pitrex.png
│ │ │ [binary file — content skipped]
│ │ ├── tower_large.png
│ │ │ [binary file — content skipped]
│ │ ├── tower_small.png
│ │ │ [binary file — content skipped]
│ │ ├── trailer.mp4
│ │ │ [binary file — content skipped]
│ │ └── village_sunset.png
│ │ [binary file — content skipped]
│ ├── directs/
│ │ └── api
│ │ https://dsc.gg/api
│ ├── posts/
│ │ ├── AlexsOpinion.md
│ │ │ # AnarchyPhase Balance Changes
│ │ │ An advice & opinion
│ │ │
│ │ │ ## Proposed by AlexEmmet
│ │ │ ### Edited by xLagging & reviewed mewjo_
│ │ │ ... (201 more lines)
│ │ ├── Home.md
│ │ │ # The /Home command
│ │ │
│ │ │ Hello Yall, Alex here,
│ │ │
│ │ │ I made this website so a little selfinsert won't hurt.
│ │ │ ... (6 more lines)
│ │ ├── JoinAnarchyphase.md
│ │ │ # Join Anarchyphase
│ │ │
│ │ │ Join the Anarchyphase community today!
│ │ └── ThisWebsite.md
│ │ # This website
│ │
│ │ Hello Yall, Alex here,
│ │
│ │ I made this website so a little selfinsert won't hurt.
│ │ ... (7 more lines)
│ ├── app.js
│ │ // Smooth scroll for nav links
│ │ document.querySelectorAll('a[href^="#"]').forEach((anchor) => {
│ │ anchor.addEventListener("click", function (e) {
│ │ e.preventDefault();
│ │ const target = document.querySelector(this.getAttribute("href"));
│ │ ... (304 more lines)
│ ├── index.html
│ │ <!doctype html>
│ │ <html lang="en">
│ │ <head>
│ │ <meta charset="UTF-8" />
│ │ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
│ │ ... (163 more lines)
│ ├── md-render.js
│ │ // Basic Markdown renderer
│ │ function renderMarkdown(md) {
│ │ let html = md
│ │ .replace(/&/g, "&amp;")
│ │ .replace(/</g, "&lt;")
│ │ ... (109 more lines)
│ ├── post.html
│ │ <!doctype html>
│ │ <html lang="en">
│ │ <head>
│ │ <meta charset="UTF-8" />
│ │ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
│ │ ... (39 more lines)
│ └── style.css
│ * {
│ margin: 0;
│ padding: 0;
│ box-sizing: border-box;
│ }
│ ... (987 more lines)
├── flake.lock
│ {
│ "nodes": {
│ "flake-utils": {
│ "inputs": {
│ "systems": "systems"
│ ... (55 more lines)
└── flake.nix
{
description = "Rust Development Flake";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
... (49 more lines)

View file

@ -18,6 +18,46 @@ struct AppState {
website_root: PathBuf, website_root: PathBuf,
} }
async fn git_pull(State(state): State<AppState>) -> impl IntoResponse {
let output = tokio::process::Command::new("git")
.arg("pull")
.current_dir(&state.website_root)
.output()
.await;
match output {
Ok(o) => {
let stdout = String::from_utf8_lossy(&o.stdout);
let stderr = String::from_utf8_lossy(&o.stderr);
let status = if o.status.success() { "ok" } else { "error" };
info!("git pull stdout: {}", stdout.trim());
if !stderr.is_empty() {
warn!("git pull stderr: {}", stderr.trim());
}
(
if o.status.success() {
StatusCode::OK
} else {
StatusCode::INTERNAL_SERVER_ERROR
},
format!(
"status: {}\n\nstdout:\n{}\nstderr:\n{}",
status, stdout, stderr
),
)
}
Err(e) => {
warn!("failed to spawn git pull: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("failed to run git pull: {}", e),
)
}
}
}
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
tracing_subscriber::fmt::init(); tracing_subscriber::fmt::init();
@ -31,9 +71,12 @@ async fn main() {
.route("/", get(serve_index)) .route("/", get(serve_index))
.route("/post/{file}", get(serve_post_page)) .route("/post/{file}", get(serve_post_page))
.route("/api/backdrops.json", get(serve_backdrops)) .route("/api/backdrops.json", get(serve_backdrops))
.route("/api/team.json", get(serve_team)) .route("/api/staff.json", get(serve_staff))
.route("/api/teams.json", get(serve_teams))
.route("/api/posts.json", get(serve_posts)) .route("/api/posts.json", get(serve_posts))
.route("/api/post/{file}", get(serve_post_md)) .route("/api/post/{file}", get(serve_post_md))
.route("/api/update", get(git_pull))
.route("/directs/{name}", get(serve_direct))
.nest_service("/assets", ServeDir::new(website_root.join("assets"))) .nest_service("/assets", ServeDir::new(website_root.join("assets")))
.nest_service("/posts", ServeDir::new(website_root.join("posts"))) .nest_service("/posts", ServeDir::new(website_root.join("posts")))
.fallback(get(serve_static)) .fallback(get(serve_static))
@ -90,8 +133,12 @@ async fn serve_backdrops(State(state): State<AppState>) -> impl IntoResponse {
serve_file(state.website_root.join("api/backdrops.json")).await serve_file(state.website_root.join("api/backdrops.json")).await
} }
async fn serve_team(State(state): State<AppState>) -> impl IntoResponse { async fn serve_staff(State(state): State<AppState>) -> impl IntoResponse {
serve_file(state.website_root.join("api/team.json")).await serve_file(state.website_root.join("api/staff.json")).await
}
async fn serve_teams(State(state): State<AppState>) -> impl IntoResponse {
serve_file(state.website_root.join("api/teams.json")).await
} }
async fn serve_posts(State(state): State<AppState>) -> impl IntoResponse { async fn serve_posts(State(state): State<AppState>) -> impl IntoResponse {
@ -152,3 +199,40 @@ async fn serve_file(path: PathBuf) -> Response {
.body(Body::from(content)) .body(Body::from(content))
.unwrap() .unwrap()
} }
use axum::http::HeaderValue;
async fn serve_direct(
State(state): State<AppState>,
Path(name): Path<String>,
) -> impl IntoResponse {
if name.contains('/') || name.contains('\\') || name.starts_with('.') {
return StatusCode::BAD_REQUEST.into_response();
}
let path = state.website_root.join("directs").join(&name);
if !path.exists() || !path.is_file() {
return StatusCode::NOT_FOUND.into_response();
}
let content = match fs::read_to_string(&path).await {
Ok(c) => c.trim().to_string(),
Err(_) => return StatusCode::INTERNAL_SERVER_ERROR.into_response(),
};
if content.is_empty() {
return StatusCode::NO_CONTENT.into_response();
}
if !content.starts_with("http://") && !content.starts_with("https://") {
warn!("invalid redirect target in {:?}: {}", path, content);
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
}
Response::builder()
.status(StatusCode::FOUND) // 302 redirect
.header(header::LOCATION, HeaderValue::from_str(&content).unwrap())
.body(Body::empty())
.unwrap()
}

View file

@ -1,6 +1,15 @@
[ [
{ {
"date": "2026-04-21", "date": "2026-04-22, by AlexEmmet",
"head_img": "outpost.png",
"file": "Teams.md",
"title": "Teams",
"description": "Get to know the teams!"
},
{
"date": "2026-04-21, by AlexEmmet",
"head_img": "village_sunset.png", "head_img": "village_sunset.png",
"file": "JoinAnarchyphase.md", "file": "JoinAnarchyphase.md",
@ -9,7 +18,7 @@
"description": "Join the Anarchyphase community today!" "description": "Join the Anarchyphase community today!"
}, },
{ {
"date": "2026-04-21", "date": "2026-04-21, by AlexEmmet",
"head_img": "horse_rainbow.png", "head_img": "horse_rainbow.png",
"file": "Home.md", "file": "Home.md",
@ -18,7 +27,7 @@
"description": "Learn about the /Home command." "description": "Learn about the /Home command."
}, },
{ {
"date": "2026-04-21", "date": "2026-04-21, by AlexEmmet",
"head_img": "horse_rainbow.png", "head_img": "horse_rainbow.png",
"file": "ThisWebsite.md", "file": "ThisWebsite.md",
@ -27,7 +36,7 @@
"description": "Learn about this website." "description": "Learn about this website."
}, },
{ {
"date": "2026-04-21", "date": "2026-04-21, by AlexEmmet",
"head_img": "horse_rainbow.png", "head_img": "horse_rainbow.png",
"file": "AlexsOpinion.md", "file": "AlexsOpinion.md",

View file

@ -9,7 +9,7 @@
"icon": "ninivx.png", "icon": "ninivx.png",
"name": "NiniVX", "name": "NiniVX",
"priority": 1, "priority": 1,
"role": "Administrator" "role": "Community Manager"
}, },
{ {
"icon": "burrata.png", "icon": "burrata.png",
@ -18,6 +18,7 @@
"role": "Administrator" "role": "Administrator"
}, },
{ {
"icon": "pitrex.png",
"name": "Pitrex", "name": "Pitrex",
"priority": 2, "priority": 2,
"role": "Moderator & Developer" "role": "Moderator & Developer"

44
website/api/teams.json Normal file
View file

@ -0,0 +1,44 @@
[
{
"title": "Anarchyphase Public Infrastructure",
"acronym": "API",
"leader": "NiniVX",
"vips": "mewjo_",
"description": "Building infrastructure across the server.",
"image": "api.png",
"discord": "directs/api"
},
{
"title": "Commune",
"acronym": "",
"leader": "StormGamer0512",
"vips": "",
"description": "Spreading Communism around the world",
"image": "campsite.png"
},
{
"title": "Orbit",
"acronym": "",
"leader": "Yippee",
"vips": "Skely, xLeggings, Luxi",
"description": "Me when I Orb it",
"image": "village_sunset.png"
},
{
"title": "Chunky Town",
"acronym": "CT",
"leader": "MoriceMC",
"vips": "Yeah he's the owner",
"description": "We are number one",
"image": "icon.png",
"discord": "directs/ct"
},
{
"title": "Ardubia",
"acronym": "",
"leader": "Liwitikiwi",
"vips": "",
"description": "Build borders to keep out foreign aliens",
"image": "outpost.png"
}
]

View file

@ -124,24 +124,24 @@ async function loadBackdrops() {
} }
} }
// ─── Load Team ─── // ─── Load Staff (in Socials section) ───
async function loadTeam() { async function loadStaff() {
try { try {
const response = await fetch("/api/team.json"); const response = await fetch("/api/staff.json");
const team = await response.json(); const staff = await response.json();
team.sort((a, b) => a.priority - b.priority); staff.sort((a, b) => a.priority - b.priority);
const container = document.getElementById("team-grid"); const container = document.getElementById("staff-grid");
if (!container) return; if (!container) return;
container.innerHTML = team container.innerHTML = staff
.map( .map(
(member) => ` (member) => `
<div class="team-card"> <div class="team-card staff-card">
${ ${
member.icon member.icon
? `<img src="/assets/${member.icon}" alt="${member.name}" onerror="this.style.display='none'">` ? `<img src="/assets/${member.icon}" alt="${member.name}" onerror="this.style.display='none'">`
: '<div style="width:100px;height:100px;border-radius:50%;background:#2a2e33;margin:0 auto 1.2rem;border:3px solid #2a2e33;"></div>' : '<div class="staff-placeholder"></div>'
} }
<div class="name">${member.name}</div> <div class="name">${member.name}</div>
<div class="role">${member.role}</div> <div class="role">${member.role}</div>
@ -150,10 +150,70 @@ async function loadTeam() {
) )
.join(""); .join("");
} catch (err) { } catch (err) {
console.error("Failed to load team:", err); console.error("Failed to load staff:", err);
} }
} }
// ─── Load Teams ───
async function loadTeams() {
try {
const response = await fetch("/api/teams.json");
const teams = await response.json();
const container = document.getElementById("teams-grid");
if (!container) return;
container.innerHTML = teams
.map((team) => {
const acronym = team.acronym || "";
const vips = team.vips || "";
const hasDiscord = team.discord && team.discord.trim() !== "";
return `
<div class="team-card-large">
<div class="team-card-image" style="background-image: url('/assets/${team.image}')">
<div class="team-card-overlay"></div>
<div class="team-card-header">
<div class="team-name">${team.title}</div>
${acronym ? `<div class="team-acronym">${acronym}</div>` : ""}
</div>
</div>
<div class="team-card-body">
<div class="fade-line"></div>
<div class="team-meta-row">
<span class="meta-label">${team.leader}</span>
</div>
${
vips
? `
<div class="team-meta-row">
<span class="meta-value vips">${vips}</span>
</div>
`
: ""
}
<div class="fade-line"></div>
<p class="team-description">${team.description}</p>
${
hasDiscord
? `
<a href="${team.discord}" target="_blank" rel="noopener" class="team-discord-btn">
<svg viewBox="0 0 24 24" fill="currentColor" class="discord-mini">
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/>
</svg>
Discord
</a>
`
: ""
}
</div>
</div>
`;
})
.join("");
} catch (err) {
console.error("Failed to load teams:", err);
}
}
// ─── Load Posts ─── // ─── Load Posts ───
async function loadPosts() { async function loadPosts() {
try { try {
@ -242,7 +302,8 @@ function initScrollEffects() {
// ─── Initialize ─── // ─── Initialize ───
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
loadBackdrops(); loadBackdrops();
loadTeam(); loadStaff();
loadTeams();
loadPosts(); loadPosts();
initVideo(); initVideo();
initScrollEffects(); initScrollEffects();

BIN
website/assets/api.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

BIN
website/assets/pitrex.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

1
website/directs/api Normal file
View file

@ -0,0 +1 @@
https://discord.gg/3NJ6ZjWTJR

1
website/directs/ct Normal file
View file

@ -0,0 +1 @@
https://discord.gg/dKuPGE32bT

View file

@ -95,47 +95,55 @@
<section id="socials" class="bg-image-section"> <section id="socials" class="bg-image-section">
<div class="content-wrapper"> <div class="content-wrapper">
<h2>Socials</h2> <h2>Socials</h2>
<a
href="https://www.youtube.com/@MoriceMC" <div class="social-links">
target="_blank" <a
rel="noopener" href="https://www.youtube.com/@MoriceMC"
class="social-btn" target="_blank"
> rel="noopener"
<svg class="social-btn"
class="social-icon"
viewBox="0 0 24 24"
fill="currentColor"
> >
<path <svg
d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z" class="social-icon"
/> viewBox="0 0 24 24"
</svg> fill="currentColor"
Youtube >
</a> <path
<a d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"
href="https://discord.com/invite/Bq4GhxDwU5" />
target="_blank" </svg>
rel="noopener" Youtube
class="social-btn" </a>
> <a
<svg href="https://discord.com/invite/Bq4GhxDwU5"
class="social-icon" target="_blank"
viewBox="0 0 24 24" rel="noopener"
fill="currentColor" class="social-btn"
> >
<path <svg
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z" class="social-icon"
/> viewBox="0 0 24 24"
</svg> fill="currentColor"
Discord >
</a> <path
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"
/>
</svg>
Discord
</a>
</div>
<div class="staff-section">
<h2>Staff</h2>
<div id="staff-grid" class="team-grid"></div>
</div>
</div> </div>
</section> </section>
<section id="team"> <section id="team">
<div class="content-wrapper"> <div class="content-wrapper">
<h2>Team</h2> <h2>Teams</h2>
<div id="team-grid" class="team-grid"></div> <div id="teams-grid" class="teams-grid"></div>
</div> </div>
</section> </section>

44
website/posts/Teams.md Normal file
View file

@ -0,0 +1,44 @@
# Teams
Hello Yall, Alex here,
Today I added a teams section & filled it with the most important teams I found, at the moment all descriptions are jokes but I hope the leaders will contact me for actual details & icons.
Now all I know about the teams:
## Anarchyphase Public Infrastructure
I work with API's a lot, though barely in Minecraft, I took part in designing the doors & walls of the new API base currently under construction.
Their leader is NiniVX,
and i know mewjo_ personally, quite the nice guy to be aroung.
They are Building public infrastructure across the server (who could guess).
## Commune
Led by StormGamer0512
They are spreading Communism around the world.
Not really it's just the first thing that comes to mind.
I think they fight a lot.
## Orbit
Led by Yippee,
with Skely, xLeggings, Luxi,
They are extremley good at PVP.
## Chunky Town
Led by the owner of Anarchyphase MoriceMC
they are building a base inside a chunk.
## Ardubia
Led byLiwitikiwi
They are builing a safe region for builders & non pvpers.
I neither know where nor how.
With some fulfilling love
Alex

View file

@ -775,3 +775,218 @@ footer a:hover {
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
background: #3a424a; background: #3a424a;
} }
/* ─── Socials Section Layout ─── */
#socials .content-wrapper {
display: flex;
flex-direction: column;
align-items: center;
gap: 2rem;
}
/* Social links come first */
.social-links {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 1.5rem;
margin-bottom: 1rem;
}
/* Staff section below socials */
.staff-section {
width: 100%;
max-width: 1200px;
}
/* Staff title */
.staff-section h2 {
text-align: center;
font-size: 2.2rem;
margin-bottom: 3rem;
text-shadow: 0 3px 15px rgba(0, 0, 0, 0.9);
color: #fff;
}
.staff-card {
padding: 1.8rem 1.2rem;
}
.staff-placeholder {
width: 80px;
height: 80px;
border-radius: 50%;
background: #2a2e33;
margin: 0 auto 1rem;
border: 3px solid #2a2e33;
}
/* ─── Team Cards ─── */
#team {
background: var(--bg-solid);
}
#team h2 {
text-align: center;
font-size: 2.2rem;
margin-bottom: 3rem;
color: #fff;
}
.teams-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 2rem;
margin-top: 2rem;
}
.team-card-large {
position: relative;
border-radius: 16px;
border: 1px solid var(--border-base);
overflow: hidden;
transition: all 0.3s;
min-height: 420px;
display: flex;
flex-direction: column;
}
.team-card-large:hover {
transform: translateY(-6px);
border-color: var(--border-hover);
}
/* background image */
.team-card-image {
position: absolute;
inset: 0;
background-size: cover;
background-position: center;
z-index: 0;
}
/* Bottom half */
.team-card-overlay {
position: absolute;
inset: 0;
background: linear-gradient(
to bottom,
transparent 0%,
transparent 35%,
rgba(10, 12, 14, 0.85) 65%,
rgba(10, 12, 14, 0.95) 100%
);
backdrop-filter: blur(0px);
z-index: 1;
}
/* Header text centered */
.team-card-header {
position: relative;
z-index: 2;
text-align: center;
padding: 2rem 1.5rem 1rem;
margin-top: auto;
}
.team-name {
font-size: 1.5rem;
font-weight: bold;
color: #fff;
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.9);
}
.team-acronym {
font-size: 0.9rem;
color: var(--accent-light);
margin-top: 0.3rem;
letter-spacing: 0.15em;
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.8);
}
.team-card-body {
position: relative;
z-index: 2;
padding: 0 1.5rem 1.5rem;
margin-top: auto;
}
/* Fade divider lines */
.fade-line {
height: 1px;
background: linear-gradient(
to right,
transparent 0%,
var(--border-hover) 30%,
var(--border-hover) 70%,
transparent 100%
);
margin: 0.8rem 0;
opacity: 0.5;
}
.team-meta-row {
display: flex;
justify-content: center;
align-items: center;
}
.meta-label {
font-size: 1.1rem;
color: #fff;
font-weight: 600;
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.8);
}
.meta-value.vips {
font-size: 0.9rem;
color: var(--text-muted);
letter-spacing: 0.15em;
}
.team-description {
color: #bbb;
line-height: 1.6;
font-size: 0.95rem;
text-align: center;
margin: 0.5rem 0 1rem;
}
/* Discord button centered at bottom */
.team-discord-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 0.6rem;
width: calc(100% - 1rem);
margin: 0.5rem auto 0;
padding: 0.7rem 1rem;
background: rgba(88, 101, 242, 0.2);
border: 1px solid rgba(88, 101, 242, 0.35);
border-radius: 10px;
color: #aab2ff;
text-decoration: none;
font-size: 0.9rem;
font-weight: 500;
transition: all 0.3s;
}
.team-discord-btn:hover {
background: rgba(88, 101, 242, 0.35);
border-color: rgba(88, 101, 242, 0.55);
color: #c5cbff;
transform: translateY(-2px);
}
.discord-mini {
width: 16px;
height: 16px;
flex-shrink: 0;
}
/* ─── Responsive ─── */
@media (max-width: 768px) {
.teams-grid {
grid-template-columns: 1fr;
}
}