first commit
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 212 KiB |
+382
@@ -0,0 +1,382 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Winnie | Developer & Gamer</title>
|
||||||
|
<meta name="description" content="Hi, I'm Winnie - a passionate developer building digital experiences with code and creativity. Check out my projects and get in touch!"/>
|
||||||
|
<meta property="og:title" content="Winnie | Developer & Gamer" />
|
||||||
|
<meta property="og:description" content="Building digital experiences with code and creativity. Passionate about crafting elegant solutions and open-source projects."/>
|
||||||
|
<meta property="og:url" content="https://winniepat.de" />
|
||||||
|
<link rel="canonical" href="https://winniepat.de" />
|
||||||
|
<link rel="icon" href="favicon.ico" />
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet"/>
|
||||||
|
<link rel="stylesheet" href="styles.css" />
|
||||||
|
</head>
|
||||||
|
<body class="noise-bg">
|
||||||
|
<div class="animated-background" aria-hidden="true">
|
||||||
|
<div class="bg-grid"></div>
|
||||||
|
<div class="orb orb-primary"></div>
|
||||||
|
<div class="orb orb-accent"></div>
|
||||||
|
<div class="orb orb-primary-small"></div>
|
||||||
|
<div class="particles" id="particles"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<header class="site-header" id="site-header">
|
||||||
|
<div class="container header-inner">
|
||||||
|
<a class="logo" href="#">
|
||||||
|
<img class="avatar" src="profile-image.png" alt="Winnie" />
|
||||||
|
<span>winnie</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<nav class="nav-links" aria-label="Primary">
|
||||||
|
<a href="#about">About</a>
|
||||||
|
<a href="#projects">Projects</a>
|
||||||
|
<a href="#skills">Skills</a>
|
||||||
|
<a href="#contact">Contact</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<a class="btn btn-primary desktop-only" href="#contact">Get in touch</a>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="menu-toggle"
|
||||||
|
id="menu-toggle"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-controls="mobile-menu"
|
||||||
|
>
|
||||||
|
<span class="sr-only">Toggle menu</span>
|
||||||
|
<i class="icon" data-lucide="menu"></i>
|
||||||
|
<i class="icon icon-close" data-lucide="x"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mobile-menu" id="mobile-menu">
|
||||||
|
<nav class="mobile-nav" aria-label="Mobile">
|
||||||
|
<a href="#about">About</a>
|
||||||
|
<a href="#projects">Projects</a>
|
||||||
|
<a href="#skills">Skills</a>
|
||||||
|
<a href="#contact">Contact</a>
|
||||||
|
<a class="btn btn-primary" href="#contact">Get in touch</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section class="hero" id="top">
|
||||||
|
<div class="container hero-inner">
|
||||||
|
<div class="hero-content">
|
||||||
|
<h1 class="hero-title reveal" data-reveal style="--delay: 0s;">
|
||||||
|
Hey, I'm <span class="text-gradient">Winnie</span>
|
||||||
|
</h1>
|
||||||
|
<p class="hero-subtitle reveal" data-reveal style="--delay: 0.1s;">
|
||||||
|
<span class="mono text-primary"><</span>
|
||||||
|
Developer & Gamer
|
||||||
|
<span class="mono text-primary">/></span>
|
||||||
|
</p>
|
||||||
|
<p class="hero-description reveal" data-reveal style="--delay: 0.2s;">
|
||||||
|
Building cool software with code and creativity. Passionate about
|
||||||
|
learning new things and open-source projects.
|
||||||
|
</p>
|
||||||
|
<div class="hero-social reveal" data-reveal style="--delay: 0.3s;">
|
||||||
|
<a
|
||||||
|
class="social-link social-link--github"
|
||||||
|
href="https://git.winniepat.de/winnie"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
aria-label="Gitea"
|
||||||
|
>
|
||||||
|
<i class="icon" data-lucide="git-branch"></i>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="social-link"
|
||||||
|
href="mailto:winniepatgg@web.de"
|
||||||
|
aria-label="Email"
|
||||||
|
>
|
||||||
|
<i class="icon" data-lucide="mail"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a class="scroll-indicator" href="#projects">
|
||||||
|
<span class="mono">scroll</span>
|
||||||
|
<i class="icon" data-lucide="arrow-down"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section" id="about">
|
||||||
|
<div class="container">
|
||||||
|
<div class="section-header reveal" data-reveal>
|
||||||
|
<span class="mono text-primary">// about me</span>
|
||||||
|
<h2>Who <span class="text-gradient">I Am</span></h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="about-grid">
|
||||||
|
<div class="about-text reveal" data-reveal style="--delay: 0.1s;">
|
||||||
|
<p>
|
||||||
|
Hi! I'm a developer who loves turning ideas into reality through
|
||||||
|
code. I specialize in building minecraft plugins and web
|
||||||
|
applications with a focus on user experiences.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
When I'm not coding, you'll find me exploring new repositories,
|
||||||
|
pushing to docker hub, or enjoying a good can of monster while
|
||||||
|
brainstorming the next big idea.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
I believe in writing code that not only works but is also clean,
|
||||||
|
scalable, and elegant. Every project is an opportunity to learn
|
||||||
|
and grow.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="facts">
|
||||||
|
<span class="tag">
|
||||||
|
<i class="icon" data-lucide="map-pin"></i>
|
||||||
|
Based in Germany
|
||||||
|
</span>
|
||||||
|
<span class="tag">
|
||||||
|
<i class="icon" data-lucide="coffee"></i>
|
||||||
|
Monster Energy Enthusiast
|
||||||
|
</span>
|
||||||
|
<span class="tag">
|
||||||
|
<i class="icon" data-lucide="sparkles"></i>
|
||||||
|
Open Source Lover
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stats-card reveal" data-reveal style="--delay: 0.2s;">
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-value text-gradient">2+</div>
|
||||||
|
<div class="stat-label">Years Coding</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-value text-gradient" id="stat-repos">...</div>
|
||||||
|
<div class="stat-label">Projects</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-value text-gradient" id="stat-commits">
|
||||||
|
...
|
||||||
|
</div>
|
||||||
|
<div class="stat-label">Commits</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-value text-gradient">∞</div>
|
||||||
|
<div class="stat-label">Monster Energy</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="terminal">
|
||||||
|
<div class="terminal-bar">
|
||||||
|
<span class="dot dot-red"></span>
|
||||||
|
<span class="dot dot-yellow"></span>
|
||||||
|
<span class="dot dot-green"></span>
|
||||||
|
</div>
|
||||||
|
<code>
|
||||||
|
<span class="text-primary">const</span> winnie = {
|
||||||
|
<br />
|
||||||
|
passion:
|
||||||
|
<span class="terminal-string">"coding"</span>,
|
||||||
|
<br />
|
||||||
|
focus:
|
||||||
|
<span class="terminal-string">"web dev"</span>,
|
||||||
|
<br />
|
||||||
|
learning:
|
||||||
|
<span class="terminal-boolean">true</span>
|
||||||
|
<br />
|
||||||
|
};
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section" id="projects">
|
||||||
|
<div class="container">
|
||||||
|
<div class="section-header reveal" data-reveal>
|
||||||
|
<span class="mono text-primary">// my work</span>
|
||||||
|
<h2>Open Source <span class="text-gradient">Projects</span></h2>
|
||||||
|
<p class="section-subtitle">
|
||||||
|
A collection of my public repositories on Gitea
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="repos-state" id="repos-loading">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
</div>
|
||||||
|
<div class="repos-state hidden" id="repos-error">
|
||||||
|
<p>Unable to load repositories. Please try again later.</p>
|
||||||
|
</div>
|
||||||
|
<div class="repo-grid" id="repo-grid"></div>
|
||||||
|
|
||||||
|
<div class="section-footer reveal" data-reveal style="--delay: 0.2s;">
|
||||||
|
<a
|
||||||
|
class="btn btn-ghost"
|
||||||
|
href="https://git.winniepat.de/winnie"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
View all on Gitea
|
||||||
|
<i class="icon" data-lucide="external-link"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section" id="skills">
|
||||||
|
<div class="container">
|
||||||
|
<div class="section-header reveal" data-reveal>
|
||||||
|
<span class="mono text-primary">// skills & tools</span>
|
||||||
|
<h2>Tech <span class="text-gradient">Stack</span></h2>
|
||||||
|
<p class="section-subtitle">
|
||||||
|
Technologies and tools I work with daily
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="skills-grid">
|
||||||
|
<div class="skill-card reveal" data-reveal>
|
||||||
|
<div class="skill-header">
|
||||||
|
<span class="skill-icon">
|
||||||
|
<i class="icon" data-lucide="globe"></i>
|
||||||
|
</span>
|
||||||
|
<h3>Frontend</h3>
|
||||||
|
</div>
|
||||||
|
<ul>
|
||||||
|
<li>HTML</li>
|
||||||
|
<li>CSS</li>
|
||||||
|
<li>Next.js</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="skill-card reveal" data-reveal style="--delay: 0.05s;">
|
||||||
|
<div class="skill-header">
|
||||||
|
<span class="skill-icon">
|
||||||
|
<i class="icon" data-lucide="database"></i>
|
||||||
|
</span>
|
||||||
|
<h3>Backend</h3>
|
||||||
|
</div>
|
||||||
|
<ul>
|
||||||
|
<li>Node.js</li>
|
||||||
|
<li>Python</li>
|
||||||
|
<li>Express.js</li>
|
||||||
|
<li>Flask</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="skill-card reveal" data-reveal style="--delay: 0.1s;">
|
||||||
|
<div class="skill-header">
|
||||||
|
<span class="skill-icon">
|
||||||
|
<i class="icon" data-lucide="cloud"></i>
|
||||||
|
</span>
|
||||||
|
<h3>DevOps</h3>
|
||||||
|
</div>
|
||||||
|
<ul>
|
||||||
|
<li>Docker</li>
|
||||||
|
<li>Portainer</li>
|
||||||
|
<li>Nginx</li>
|
||||||
|
<li>Dash.</li>
|
||||||
|
<li>Ubuntu</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="skill-card reveal" data-reveal style="--delay: 0.15s;">
|
||||||
|
<div class="skill-header">
|
||||||
|
<span class="skill-icon">
|
||||||
|
<i class="icon" data-lucide="terminal"></i>
|
||||||
|
</span>
|
||||||
|
<h3>Tools</h3>
|
||||||
|
</div>
|
||||||
|
<ul>
|
||||||
|
<li>Git</li>
|
||||||
|
<li>VSCode</li>
|
||||||
|
<li>Webstorm</li>
|
||||||
|
<li>Intellij</li>
|
||||||
|
<li>Termius</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section" id="contact">
|
||||||
|
<div class="container">
|
||||||
|
<div class="section-header reveal" data-reveal>
|
||||||
|
<span class="mono text-primary">// get in touch</span>
|
||||||
|
<h2>Let's <span class="text-gradient">Connect</span></h2>
|
||||||
|
<p class="section-subtitle">
|
||||||
|
Have a project in mind or just want to say hello? I'm always open
|
||||||
|
to discussing new opportunities and ideas.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cta-actions reveal" data-reveal style="--delay: 0.1s;">
|
||||||
|
<a class="btn btn-primary" href="mailto:winniepatgg@web.de">
|
||||||
|
<i class="icon" data-lucide="mail"></i>
|
||||||
|
Send an Email
|
||||||
|
<i class="icon icon-sm" data-lucide="send"></i>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="btn btn-outline btn-github"
|
||||||
|
href="https://git.winniepat.de/winnie"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<i class="icon" data-lucide="git-branch"></i>
|
||||||
|
Follow on Gitea
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider reveal" data-reveal style="--delay: 0.2s;"></div>
|
||||||
|
|
||||||
|
<div class="email-pill reveal" data-reveal style="--delay: 0.3s;">
|
||||||
|
<i class="icon" data-lucide="message-square"></i>
|
||||||
|
<span class="mono">winniepatgg@web.de</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="site-footer">
|
||||||
|
<div class="container footer-content">
|
||||||
|
<div class="footer-brand">
|
||||||
|
<img class="avatar avatar--sm" src="profile-image.png" alt="Winnie" />
|
||||||
|
<span>winniepat.de</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="footer-links" aria-label="Footer">
|
||||||
|
<a href="https://status.winniepat.de">Status</a>
|
||||||
|
<a href="https://server.winniepat.de">Server Load</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="footer-social">
|
||||||
|
<a
|
||||||
|
href="https://git.winniepat.de/winnie"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
aria-label="Gitea"
|
||||||
|
class="footer-social-link footer-social-link--github"
|
||||||
|
>
|
||||||
|
<i class="icon" data-lucide="git-branch"></i>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="mailto:winniepatgg@web.de"
|
||||||
|
aria-label="Email"
|
||||||
|
class="footer-social-link"
|
||||||
|
>
|
||||||
|
<i class="icon" data-lucide="mail"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer-bottom">
|
||||||
|
<p>
|
||||||
|
© <span id="current-year"></span> WinniePatGG. Built with 🍯 and lots of Monster Energy.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="https://unpkg.com/lucide@latest"></script>
|
||||||
|
<script src="main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+358
@@ -0,0 +1,358 @@
|
|||||||
|
const LANGUAGE_COLORS = {
|
||||||
|
TypeScript: "#3b82f6",
|
||||||
|
JavaScript: "#facc15",
|
||||||
|
Python: "#22c55e",
|
||||||
|
Rust: "#f97316",
|
||||||
|
Go: "#22d3ee",
|
||||||
|
Java: "#ef4444",
|
||||||
|
HTML: "#f97316",
|
||||||
|
CSS: "#a855f7",
|
||||||
|
Vue: "#10b981",
|
||||||
|
React: "#22d3ee",
|
||||||
|
};
|
||||||
|
|
||||||
|
const GITEA_BASE_URL = "https://git.winniepat.de";
|
||||||
|
const GITEA_USER = "winnie";
|
||||||
|
const GITEA_FEED_URL = "/gitea-feed";
|
||||||
|
const GITEA_REPO_FEED_BASE = "/gitea-repo-feed";
|
||||||
|
const FALLBACK_STATS = { totalRepos: 10, totalCommits: 500 };
|
||||||
|
|
||||||
|
const initNavbar = () => {
|
||||||
|
const header = document.getElementById("site-header");
|
||||||
|
if (!header) return;
|
||||||
|
const updateHeader = () => {
|
||||||
|
header.classList.toggle("scrolled", window.scrollY > 50);
|
||||||
|
};
|
||||||
|
|
||||||
|
updateHeader();
|
||||||
|
window.addEventListener("scroll", updateHeader);
|
||||||
|
};
|
||||||
|
|
||||||
|
const initMobileMenu = () => {
|
||||||
|
const toggle = document.getElementById("menu-toggle");
|
||||||
|
const menu = document.getElementById("mobile-menu");
|
||||||
|
if (!toggle || !menu) return;
|
||||||
|
const closeMenu = () => {
|
||||||
|
document.body.classList.remove("menu-open");
|
||||||
|
menu.classList.remove("open");
|
||||||
|
toggle.setAttribute("aria-expanded", "false");
|
||||||
|
};
|
||||||
|
|
||||||
|
toggle.addEventListener("click", () => {
|
||||||
|
const isOpen = document.body.classList.toggle("menu-open");
|
||||||
|
menu.classList.toggle("open", isOpen);
|
||||||
|
toggle.setAttribute("aria-expanded", String(isOpen));
|
||||||
|
});
|
||||||
|
|
||||||
|
menu.querySelectorAll("a").forEach((link) => {
|
||||||
|
link.addEventListener("click", closeMenu);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const initParticles = () => {
|
||||||
|
const particleWrap = document.getElementById("particles");
|
||||||
|
if (!particleWrap) return;
|
||||||
|
|
||||||
|
for (let i = 0; i < 20; i += 1) {
|
||||||
|
const dot = document.createElement("span");
|
||||||
|
dot.style.left = `${Math.random() * 100}%`;
|
||||||
|
dot.style.top = `${Math.random() * 100}%`;
|
||||||
|
dot.style.animationDuration = `${3 + Math.random() * 4}s`;
|
||||||
|
dot.style.animationDelay = `${Math.random() * 2}s`;
|
||||||
|
particleWrap.appendChild(dot);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const initReveal = () => {
|
||||||
|
const elements = document.querySelectorAll("[data-reveal]");
|
||||||
|
if (!elements.length) return;
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
entry.target.classList.add("is-visible");
|
||||||
|
observer.unobserve(entry.target);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ threshold: 0.2 }
|
||||||
|
);
|
||||||
|
|
||||||
|
elements.forEach((el) => observer.observe(el));
|
||||||
|
};
|
||||||
|
|
||||||
|
const initCardGlow = () => {
|
||||||
|
document.querySelectorAll(".card-glow").forEach((card) => {
|
||||||
|
card.addEventListener("mousemove", (event) => {
|
||||||
|
const rect = card.getBoundingClientRect();
|
||||||
|
const x = event.clientX - rect.left;
|
||||||
|
const y = event.clientY - rect.top;
|
||||||
|
card.style.setProperty("--mouse-x", `${x}px`);
|
||||||
|
card.style.setProperty("--mouse-y", `${y}px`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateYear = () => {
|
||||||
|
const year = document.getElementById("current-year");
|
||||||
|
if (year) {
|
||||||
|
year.textContent = new Date().getFullYear();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toCount = (value) => {
|
||||||
|
if (value === null || value === undefined) return null;
|
||||||
|
const numberValue = Number(value);
|
||||||
|
return Number.isFinite(numberValue) ? numberValue : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const htmlToText = (value) => {
|
||||||
|
if (!value) return "";
|
||||||
|
const textarea = document.createElement("textarea");
|
||||||
|
textarea.innerHTML = value;
|
||||||
|
const decoded = textarea.value;
|
||||||
|
const doc = new DOMParser().parseFromString(decoded, "text/html");
|
||||||
|
return doc.body.textContent || "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const stripSha = (value) =>
|
||||||
|
value.replace(/\b[0-9a-f]{7,40}\b/gi, "").replace(/\s+/g, " ").trim();
|
||||||
|
|
||||||
|
const isActivityText = (value) =>
|
||||||
|
/created branch|created repository|pushed to|opened pull request|merged pull request|opened issue|closed issue/i.test(
|
||||||
|
value
|
||||||
|
);
|
||||||
|
|
||||||
|
const getItemText = (item, tagName) => {
|
||||||
|
const node = item.getElementsByTagName(tagName)[0];
|
||||||
|
return node ? node.textContent : "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseItemDate = (value) => {
|
||||||
|
const date = new Date(value);
|
||||||
|
return Number.isNaN(date.getTime()) ? null : date;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRepoInfoFromLink = (link) => {
|
||||||
|
if (!link) return null;
|
||||||
|
try {
|
||||||
|
const url = new URL(link);
|
||||||
|
const segments = url.pathname.split("/").filter(Boolean);
|
||||||
|
if (segments.length < 2) return null;
|
||||||
|
const [owner, repo] = segments;
|
||||||
|
return {
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
full_name: `${owner}/${repo}`,
|
||||||
|
html_url: `${GITEA_BASE_URL}/${owner}/${repo}`,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildRepoDescription = (item) => {
|
||||||
|
const rawDescription =
|
||||||
|
getItemText(item, "content:encoded") || getItemText(item, "description");
|
||||||
|
const descriptionText = stripSha(htmlToText(rawDescription));
|
||||||
|
if (descriptionText && !isActivityText(descriptionText)) return descriptionText;
|
||||||
|
const titleText = stripSha(htmlToText(getItemText(item, "title")));
|
||||||
|
if (titleText && !isActivityText(titleText)) return titleText;
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const getChannelText = (xml, tagName) => {
|
||||||
|
const channel = xml.getElementsByTagName("channel")[0];
|
||||||
|
if (!channel) return "";
|
||||||
|
const node = channel.getElementsByTagName(tagName)[0];
|
||||||
|
return node ? node.textContent : "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchRepoFeedDescription = async (repoInfo) => {
|
||||||
|
try {
|
||||||
|
const repoPath = `${encodeURIComponent(
|
||||||
|
repoInfo.owner
|
||||||
|
)}/${encodeURIComponent(repoInfo.repo)}`;
|
||||||
|
const response = await fetch(`${GITEA_REPO_FEED_BASE}/${repoPath}`);
|
||||||
|
if (!response.ok) return "";
|
||||||
|
const xmlText = await response.text();
|
||||||
|
const xml = new DOMParser().parseFromString(xmlText, "text/xml");
|
||||||
|
const description = stripSha(htmlToText(getChannelText(xml, "description")));
|
||||||
|
return description.trim();
|
||||||
|
} catch (error) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const updateStats = (stats) => {
|
||||||
|
const reposEl = document.getElementById("stat-repos");
|
||||||
|
const commitsEl = document.getElementById("stat-commits");
|
||||||
|
if (reposEl) reposEl.textContent = stats.totalRepos.toString();
|
||||||
|
if (commitsEl)
|
||||||
|
commitsEl.textContent = stats.totalCommits.toLocaleString();
|
||||||
|
};
|
||||||
|
|
||||||
|
const createRepoCard = (repo) => {
|
||||||
|
const card = document.createElement("a");
|
||||||
|
card.href = repo.html_url;
|
||||||
|
card.target = "_blank";
|
||||||
|
card.rel = "noopener noreferrer";
|
||||||
|
card.className = "repo-card card-glow";
|
||||||
|
|
||||||
|
const languageColor = LANGUAGE_COLORS[repo.language] || "#6b7280";
|
||||||
|
const description = repo.description || "No description available";
|
||||||
|
const stars = toCount(repo.stars_count ?? repo.stargazers_count);
|
||||||
|
const forks = toCount(repo.forks_count);
|
||||||
|
const statsItems = [
|
||||||
|
stars !== null
|
||||||
|
? `<span><i class="icon icon-sm" data-lucide="star"></i> ${stars}</span>`
|
||||||
|
: "",
|
||||||
|
forks !== null
|
||||||
|
? `<span><i class="icon icon-sm" data-lucide="git-fork"></i> ${forks}</span>`
|
||||||
|
: "",
|
||||||
|
].filter(Boolean);
|
||||||
|
const statsMarkup = statsItems.length
|
||||||
|
? `<div class="repo-stats">${statsItems.join("")}</div>`
|
||||||
|
: "";
|
||||||
|
const languageMarkup = repo.language
|
||||||
|
? `<span class="repo-language"><span class="language-dot" style="background:${languageColor}"></span>${repo.language}</span>`
|
||||||
|
: "";
|
||||||
|
const metaMarkup =
|
||||||
|
statsMarkup || languageMarkup
|
||||||
|
? `<div class="repo-meta">${statsMarkup}${languageMarkup}</div>`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
card.innerHTML = `
|
||||||
|
<div class="repo-header">
|
||||||
|
<i class="icon icon-badge" data-lucide="code-2"></i>
|
||||||
|
<i class="icon icon-fade" data-lucide="external-link"></i>
|
||||||
|
</div>
|
||||||
|
<h3>${repo.name}</h3>
|
||||||
|
<p>${description}</p>
|
||||||
|
${metaMarkup}
|
||||||
|
`;
|
||||||
|
|
||||||
|
return card;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderRepos = (repos) => {
|
||||||
|
const grid = document.getElementById("repo-grid");
|
||||||
|
const loading = document.getElementById("repos-loading");
|
||||||
|
const error = document.getElementById("repos-error");
|
||||||
|
|
||||||
|
if (!grid || !loading || !error) return;
|
||||||
|
|
||||||
|
grid.innerHTML = "";
|
||||||
|
|
||||||
|
loading.classList.add("hidden");
|
||||||
|
error.classList.add("hidden");
|
||||||
|
|
||||||
|
repos.forEach((repo) => {
|
||||||
|
grid.appendChild(createRepoCard(repo));
|
||||||
|
});
|
||||||
|
|
||||||
|
initCardGlow();
|
||||||
|
if (window.lucide) {
|
||||||
|
window.lucide.createIcons();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderRepoError = () => {
|
||||||
|
const loading = document.getElementById("repos-loading");
|
||||||
|
const error = document.getElementById("repos-error");
|
||||||
|
if (!loading || !error) return;
|
||||||
|
loading.classList.add("hidden");
|
||||||
|
error.classList.remove("hidden");
|
||||||
|
};
|
||||||
|
|
||||||
|
const sortRepos = (repos) =>
|
||||||
|
repos
|
||||||
|
.slice()
|
||||||
|
.sort(
|
||||||
|
(a, b) => new Date(b.updated_at || 0) - new Date(a.updated_at || 0)
|
||||||
|
)
|
||||||
|
.slice(0, 12);
|
||||||
|
|
||||||
|
const fetchGiteaFeed = async () => {
|
||||||
|
const response = await fetch(GITEA_FEED_URL);
|
||||||
|
if (!response.ok) throw new Error("Failed to fetch feed");
|
||||||
|
const xmlText = await response.text();
|
||||||
|
const xml = new DOMParser().parseFromString(xmlText, "text/xml");
|
||||||
|
const items = Array.from(xml.getElementsByTagName("item"));
|
||||||
|
if (!items.length) throw new Error("No feed items");
|
||||||
|
|
||||||
|
const repoMap = new Map();
|
||||||
|
let commitCount = 0;
|
||||||
|
|
||||||
|
items.forEach((item) => {
|
||||||
|
const link = getItemText(item, "link");
|
||||||
|
const titleText = htmlToText(getItemText(item, "title"));
|
||||||
|
const repoInfo = getRepoInfoFromLink(link);
|
||||||
|
if (!repoInfo) return;
|
||||||
|
|
||||||
|
const pubDate = parseItemDate(getItemText(item, "pubDate")) || new Date(0);
|
||||||
|
const description = buildRepoDescription(item);
|
||||||
|
|
||||||
|
const existing = repoMap.get(repoInfo.full_name);
|
||||||
|
if (!existing || pubDate > new Date(existing.updated_at || 0)) {
|
||||||
|
repoMap.set(repoInfo.full_name, {
|
||||||
|
...repoInfo,
|
||||||
|
name: repoInfo.repo,
|
||||||
|
description,
|
||||||
|
updated_at: pubDate.toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/pushed to/i.test(titleText) || /\/commit\//i.test(link)) {
|
||||||
|
commitCount += 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const repos = Array.from(repoMap.values());
|
||||||
|
const enrichedRepos = await Promise.all(
|
||||||
|
repos.map(async (repo) => {
|
||||||
|
const description = await fetchRepoFeedDescription(repo);
|
||||||
|
return description ? { ...repo, description } : repo;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const hasItems = items.length > 0;
|
||||||
|
const totalRepos =
|
||||||
|
enrichedRepos.length || (hasItems ? 0 : FALLBACK_STATS.totalRepos);
|
||||||
|
const totalCommits = hasItems ? commitCount : FALLBACK_STATS.totalCommits;
|
||||||
|
|
||||||
|
return {
|
||||||
|
repos: enrichedRepos,
|
||||||
|
stats: { totalRepos, totalCommits },
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadGiteaFeed = async () => {
|
||||||
|
try {
|
||||||
|
const { repos, stats } = await fetchGiteaFeed();
|
||||||
|
renderRepos(sortRepos(repos));
|
||||||
|
updateStats(stats);
|
||||||
|
} catch (error) {
|
||||||
|
renderRepoError();
|
||||||
|
updateStats(FALLBACK_STATS);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const initIcons = () => {
|
||||||
|
if (window.lucide) {
|
||||||
|
window.lucide.createIcons();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
initIcons();
|
||||||
|
initNavbar();
|
||||||
|
initMobileMenu();
|
||||||
|
initParticles();
|
||||||
|
initReveal();
|
||||||
|
initCardGlow();
|
||||||
|
updateYear();
|
||||||
|
loadGiteaFeed();
|
||||||
|
});
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 184 KiB |
@@ -0,0 +1,14 @@
|
|||||||
|
User-agent: Googlebot
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
User-agent: Bingbot
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
User-agent: Twitterbot
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
User-agent: facebookexternalhit
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
+1037
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,8 @@
|
|||||||
|
services:
|
||||||
|
web:
|
||||||
|
image: nginx:alpine
|
||||||
|
ports:
|
||||||
|
- "4001:80"
|
||||||
|
volumes:
|
||||||
|
- ./app:/usr/share/nginx/html:ro
|
||||||
|
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /gitea-feed {
|
||||||
|
proxy_pass https://git.winniepat.de/winnie.rss;
|
||||||
|
proxy_set_header Host git.winniepat.de;
|
||||||
|
proxy_ssl_server_name on;
|
||||||
|
|
||||||
|
add_header Access-Control-Allow-Origin "*" always;
|
||||||
|
add_header Access-Control-Allow-Methods "GET, OPTIONS" always;
|
||||||
|
add_header Access-Control-Allow-Headers "Content-Type" always;
|
||||||
|
|
||||||
|
if ($request_method = OPTIONS) {
|
||||||
|
return 204;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
location /gitea-repo-feed/ {
|
||||||
|
rewrite ^/gitea-repo-feed/(.+)$ /$1.rss break;
|
||||||
|
proxy_pass https://git.winniepat.de;
|
||||||
|
proxy_set_header Host git.winniepat.de;
|
||||||
|
proxy_ssl_server_name on;
|
||||||
|
|
||||||
|
add_header Access-Control-Allow-Origin "*" always;
|
||||||
|
add_header Access-Control-Allow-Methods "GET, OPTIONS" always;
|
||||||
|
add_header Access-Control-Allow-Headers "Content-Type" always;
|
||||||
|
|
||||||
|
if ($request_method = OPTIONS) {
|
||||||
|
return 204;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
location /gitea-api/ {
|
||||||
|
proxy_pass https://git.winniepat.de/api/v1/;
|
||||||
|
proxy_set_header Host git.winniepat.de;
|
||||||
|
proxy_ssl_server_name on;
|
||||||
|
|
||||||
|
add_header Access-Control-Allow-Origin "*" always;
|
||||||
|
add_header Access-Control-Allow-Methods "GET, OPTIONS" always;
|
||||||
|
add_header Access-Control-Allow-Headers "Content-Type" always;
|
||||||
|
|
||||||
|
if ($request_method = OPTIONS) {
|
||||||
|
return 204;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user