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.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.title}
+ ${acronym ? `
${acronym}
` : ""} +
+
+
+
+
+ ${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 @@
+ +
+

Staff

+
+
-

Team

-
+

Teams

+
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; + } +}