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