diff --git a/file_graph_generator.py b/file_graph_generator.py
new file mode 100644
index 0000000..8bc662e
--- /dev/null
+++ b/file_graph_generator.py
@@ -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()
diff --git a/file_tree.txt b/file_tree.txt
new file mode 100644
index 0000000..06a6a3a
--- /dev/null
+++ b/file_tree.txt
@@ -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
+│ │
+│ │
+│ │
+│ │
+│ │
+│ │ ... (163 more lines)
+│ ├── md-render.js
+│ │ // Basic Markdown renderer
+│ │ function renderMarkdown(md) {
+│ │ let html = md
+│ │ .replace(/&/g, "&")
+│ │ .replace(/
+│ │
+│ │
+│ │
+│ │
+│ │ ... (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)
diff --git a/server/src/main.rs b/server/src/main.rs
index a134e9b..1b2ae8d 100644
--- a/server/src/main.rs
+++ b/server/src/main.rs
@@ -18,6 +18,46 @@ struct AppState {
website_root: PathBuf,
}
+async fn git_pull(State(state): State) -> 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]
async fn main() {
tracing_subscriber::fmt::init();
@@ -31,9 +71,12 @@ async fn main() {
.route("/", get(serve_index))
.route("/post/{file}", get(serve_post_page))
.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/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("/posts", ServeDir::new(website_root.join("posts")))
.fallback(get(serve_static))
@@ -90,8 +133,12 @@ async fn serve_backdrops(State(state): State) -> impl IntoResponse {
serve_file(state.website_root.join("api/backdrops.json")).await
}
-async fn serve_team(State(state): State) -> impl IntoResponse {
- serve_file(state.website_root.join("api/team.json")).await
+async fn serve_staff(State(state): State) -> impl IntoResponse {
+ serve_file(state.website_root.join("api/staff.json")).await
+}
+
+async fn serve_teams(State(state): State) -> impl IntoResponse {
+ serve_file(state.website_root.join("api/teams.json")).await
}
async fn serve_posts(State(state): State) -> impl IntoResponse {
@@ -152,3 +199,40 @@ async fn serve_file(path: PathBuf) -> Response {
.body(Body::from(content))
.unwrap()
}
+
+use axum::http::HeaderValue;
+
+async fn serve_direct(
+ State(state): State,
+ Path(name): Path,
+) -> 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()
+}
diff --git a/website/api/posts.json b/website/api/posts.json
index 4b6a7d9..f26d1e6 100644
--- a/website/api/posts.json
+++ b/website/api/posts.json
@@ -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",
"file": "JoinAnarchyphase.md",
@@ -9,7 +18,7 @@
"description": "Join the Anarchyphase community today!"
},
{
- "date": "2026-04-21",
+ "date": "2026-04-21, by AlexEmmet",
"head_img": "horse_rainbow.png",
"file": "Home.md",
@@ -18,7 +27,7 @@
"description": "Learn about the /Home command."
},
{
- "date": "2026-04-21",
+ "date": "2026-04-21, by AlexEmmet",
"head_img": "horse_rainbow.png",
"file": "ThisWebsite.md",
@@ -27,7 +36,7 @@
"description": "Learn about this website."
},
{
- "date": "2026-04-21",
+ "date": "2026-04-21, by AlexEmmet",
"head_img": "horse_rainbow.png",
"file": "AlexsOpinion.md",
diff --git a/website/api/team.json b/website/api/staff.json
similarity index 93%
rename from website/api/team.json
rename to website/api/staff.json
index 2496fc6..40d66d2 100644
--- a/website/api/team.json
+++ b/website/api/staff.json
@@ -9,7 +9,7 @@
"icon": "ninivx.png",
"name": "NiniVX",
"priority": 1,
- "role": "Administrator"
+ "role": "Community Manager"
},
{
"icon": "burrata.png",
@@ -18,6 +18,7 @@
"role": "Administrator"
},
{
+ "icon": "pitrex.png",
"name": "Pitrex",
"priority": 2,
"role": "Moderator & Developer"
diff --git a/website/api/teams.json b/website/api/teams.json
new file mode 100644
index 0000000..c3a781c
--- /dev/null
+++ b/website/api/teams.json
@@ -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"
+ }
+]
diff --git a/website/app.js b/website/app.js
index 6e0f2c3..b62fd8c 100644
--- a/website/app.js
+++ b/website/app.js
@@ -124,24 +124,24 @@ async function loadBackdrops() {
}
}
-// ─── Load Team ───
-async function loadTeam() {
+// ─── Load Staff (in Socials section) ───
+async function loadStaff() {
try {
- const response = await fetch("/api/team.json");
- const team = await response.json();
- team.sort((a, b) => a.priority - b.priority);
+ const response = await fetch("/api/staff.json");
+ const staff = await response.json();
+ staff.sort((a, b) => a.priority - b.priority);
- const container = document.getElementById("team-grid");
+ const container = document.getElementById("staff-grid");
if (!container) return;
- container.innerHTML = team
+ container.innerHTML = staff
.map(
(member) => `
-
+
${
member.icon
? `

`
- : '
'
+ : '
'
}
${member.name}
${member.role}
@@ -150,10 +150,70 @@ async function loadTeam() {
)
.join("");
} 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 `
+
+
+
+
+
+ ${team.leader}
+
+ ${
+ vips
+ ? `
+
+ ${vips}
+
+ `
+ : ""
+ }
+
+
${team.description}
+ ${
+ hasDiscord
+ ? `
+
+
+ Discord
+
+ `
+ : ""
+ }
+
+
+ `;
+ })
+ .join("");
+ } catch (err) {
+ console.error("Failed to load teams:", err);
+ }
+}
// ─── Load Posts ───
async function loadPosts() {
try {
@@ -242,7 +302,8 @@ function initScrollEffects() {
// ─── Initialize ───
document.addEventListener("DOMContentLoaded", () => {
loadBackdrops();
- loadTeam();
+ loadStaff();
+ loadTeams();
loadPosts();
initVideo();
initScrollEffects();
diff --git a/website/assets/api.png b/website/assets/api.png
new file mode 100644
index 0000000..2f1bf03
Binary files /dev/null and b/website/assets/api.png differ
diff --git a/website/assets/harrpy.png b/website/assets/harrpy.png
deleted file mode 100644
index 5ed9a23..0000000
Binary files a/website/assets/harrpy.png and /dev/null differ
diff --git a/website/assets/pitrex.png b/website/assets/pitrex.png
new file mode 100644
index 0000000..4be7d9e
Binary files /dev/null and b/website/assets/pitrex.png differ
diff --git a/website/directs/api b/website/directs/api
new file mode 100644
index 0000000..1f29ef4
--- /dev/null
+++ b/website/directs/api
@@ -0,0 +1 @@
+https://discord.gg/3NJ6ZjWTJR
diff --git a/website/directs/ct b/website/directs/ct
new file mode 100644
index 0000000..70e162d
--- /dev/null
+++ b/website/directs/ct
@@ -0,0 +1 @@
+https://discord.gg/dKuPGE32bT
diff --git a/website/index.html b/website/index.html
index 3efac2f..bfb2d35 100644
--- a/website/index.html
+++ b/website/index.html
@@ -95,47 +95,55 @@
diff --git a/website/posts/Teams.md b/website/posts/Teams.md
new file mode 100644
index 0000000..3037996
--- /dev/null
+++ b/website/posts/Teams.md
@@ -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
diff --git a/website/style.css b/website/style.css
index db056ce..ce14c93 100644
--- a/website/style.css
+++ b/website/style.css
@@ -775,3 +775,218 @@ footer a:hover {
::-webkit-scrollbar-thumb:hover {
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;
+ }
+}
Socials
- - - Youtube - - - + Youtube + + -Staff
+ +