1
0
mirror of https://github.com/khrysse/khrysse.github.io.git synced 2025-06-27 06:31:56 +00:00
This commit is contained in:
Kryscau 2025-06-09 03:20:05 +02:00
parent 7f169ad4ba
commit 1eef073e0e
48 changed files with 4695 additions and 14228 deletions

2
.env.example Normal file
View File

@ -0,0 +1,2 @@
PUBLIC_GITHUB_USERNAME=your_github_username
PUBLIC_GITHUB_API_URL="https://api.github.com"

55
.github/workflows/deploy.yml vendored Normal file
View File

@ -0,0 +1,55 @@
name: Deploy to GitHub Pages
on:
push:
branches: 'main'
jobs:
build_site:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
# If you're using pnpm, add this step then change the commands and cache key below to use `pnpm`
# - name: Install pnpm
# uses: pnpm/action-setup@v3
# with:
# version: 8
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Install dependencies
run: npm install
- name: build
env:
BASE_PATH: '/${{ github.event.repository.name }}'
run: |
npm run build
- name: Upload Artifacts
uses: actions/upload-pages-artifact@v3
with:
path: 'build/'
deploy:
needs: build_site
runs-on: ubuntu-latest
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy
id: deployment
uses: actions/deploy-pages@v4

831
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -18,7 +18,7 @@
"@eslint/js": "^9.18.0", "@eslint/js": "^9.18.0",
"@fontsource/fira-mono": "^5.0.0", "@fontsource/fira-mono": "^5.0.0",
"@neoconfetti/svelte": "^2.0.0", "@neoconfetti/svelte": "^2.0.0",
"@sveltejs/adapter-auto": "^6.0.0", "@sveltejs/adapter-static": "^3.0.8",
"@sveltejs/kit": "^2.16.0", "@sveltejs/kit": "^2.16.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0", "@sveltejs/vite-plugin-svelte": "^5.0.0",
"@tailwindcss/forms": "^0.5.9", "@tailwindcss/forms": "^0.5.9",
@ -36,5 +36,9 @@
"tailwindcss": "^4.0.0", "tailwindcss": "^4.0.0",
"typescript": "^5.0.0", "typescript": "^5.0.0",
"vite": "^6.2.6" "vite": "^6.2.6"
},
"dependencies": {
"date-fns": "^4.1.0",
"vite-plugin-html": "^3.2.2"
} }
} }

View File

@ -1,108 +0,0 @@
@import 'tailwindcss';
@import '@fontsource/fira-mono';
@plugin '@tailwindcss/forms';
@plugin '@tailwindcss/typography';
:root {
--font-body:
Arial, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell,
'Open Sans', 'Helvetica Neue', sans-serif;
--font-mono: 'Fira Mono', monospace;
--color-bg-0: rgb(202, 216, 228);
--color-bg-1: hsl(209, 36%, 86%);
--color-bg-2: hsl(224, 44%, 95%);
--color-theme-1: #ff3e00;
--color-theme-2: #4075a6;
--color-text: rgba(0, 0, 0, 0.7);
--column-width: 42rem;
--column-margin-top: 4rem;
font-family: var(--font-body);
color: var(--color-text);
}
body {
min-height: 100vh;
margin: 0;
background-attachment: fixed;
background-color: var(--color-bg-1);
background-size: 100vw 100vh;
background-image:
radial-gradient(50% 50% at 50% 50%, rgba(255, 255, 255, 0.75) 0%, rgba(255, 255, 255, 0) 100%),
linear-gradient(180deg, var(--color-bg-0) 0%, var(--color-bg-1) 15%, var(--color-bg-2) 50%);
}
h1,
h2,
p {
font-weight: 400;
}
p {
line-height: 1.5;
}
a {
color: var(--color-theme-1);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
h1 {
font-size: 2rem;
text-align: center;
}
h2 {
font-size: 1rem;
}
pre {
font-size: 16px;
font-family: var(--font-mono);
background-color: rgba(255, 255, 255, 0.45);
border-radius: 3px;
box-shadow: 2px 2px 6px rgb(255 255 255 / 25%);
padding: 0.5em;
overflow-x: auto;
color: var(--color-text);
}
.text-column {
display: flex;
max-width: 48rem;
flex: 0.6;
flex-direction: column;
justify-content: center;
margin: 0 auto;
}
input,
button {
font-size: inherit;
font-family: inherit;
}
button:focus:not(:focus-visible) {
outline: none;
}
@media (min-width: 720px) {
h1 {
font-size: 2.4rem;
}
}
.visually-hidden {
border: 0;
clip: rect(0 0 0 0);
height: auto;
margin: 0;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
white-space: nowrap;
}

View File

@ -1,12 +1,45 @@
<!--
___ _ _ ___ _ _
/ __(_) |_| _ \___ _ __ ___ __(_) |_ ___ _ _ _ _
| (_ | | _| / -_) '_ \/ _ (_-< | _/ _ \ '_| || | Mode %MODE%
\___|_|\__|_|_\___| .__/\___/__/_|\__\___/_| \_, | Vite v<%= viteVersion %>
|_| by github.com/kryscau |__/ SvelteKit v<%= sveltekitVersion %>
-->
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" /> <link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/png" href="/assets/favicon/favicon-96x96.png" sizes="96x96" />
<link rel="icon" type="image/svg+xml" href="/assets/favicon/favicon.svg" />
<link rel="shortcut icon" href="/assets/favicon/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/assets/favicon/apple-touch-icon.png" />
<link rel="manifest" href="/assets/favicon/site.webmanifest" />
<noscript>
<link rel="stylesheet" href="/assets/css/noscript.css" />
</noscript>
<meta name="twitter:card" content="summary_large_image" />
<meta name="description" content="Svelte demo app" />
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
%sveltekit.head% %sveltekit.head%
</head> </head>
<body data-sveltekit-preload-data="hover"> <body class="bg-gh-bg text-gh-text min-h-screen" data-sveltekit-preload-data="hover">
<noscript>
<div class="noscript-message">
<h1>Oups!</h1>
<p>
This application requires JavaScript to be enabled. Please enable JavaScript in your
browser settings and refresh the page. <br />
<a href="https://www.enable-javascript.com/" target="_blank" rel="noopener noreferrer">
How to enable JavaScript?
</a>
</p>
</div>
</noscript>
<div style="display: contents">%sveltekit.body%</div> <div style="display: contents">%sveltekit.body%</div>
</body> </body>
</html> </html>

View File

@ -0,0 +1,24 @@
<script>
import { PUBLIC_GITHUB_USERNAME } from '$env/static/public';
export let title = 'Default Title';
export let description = 'Default description for the page.';
export let imageUrl = 'https://example.com/default-image.jpg';
export let author = `Kryscau for @${PUBLIC_GITHUB_USERNAME}`;
export let keywords = 'default, keywords, for, seo';
export let robots = 'index, follow';
</script>
<svelte:head>
<title>{title}</title>
<meta name="description" content={description} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={imageUrl} />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={imageUrl} />
<meta name="author" content={author} />
<meta name="keywords" content={keywords} />
<meta name="robots" content={robots} />
</svelte:head>

View File

@ -0,0 +1,58 @@
<script>
import { page } from '$app/stores';
import { derived } from 'svelte/store';
import { onMount } from 'svelte';
export let username = '';
const formattedUsername = username.charAt(0).toUpperCase() + username.slice(1).toLowerCase();
const isRepositoryPage = derived(page, ($page) => {
return (
$page.url.pathname.startsWith('/repository/') || $page.url.hash.startsWith('#/repository/')
);
});
function goBack() {
history.back();
}
// (Optionnel) Menu mobile toggle si besoin
onMount(() => {
const menuButton = document.querySelector('button[data-toggle="mobile-menu"]');
if (menuButton) {
menuButton.addEventListener('click', () => {
console.log('Mobile menu clicked');
});
}
});
</script>
<nav
class="gh-bg-secondary gh-border sticky top-0 z-50 border-b"
style="border-bottom: 1px solid var(--gh-border)"
>
<div class="mx-auto max-w-6xl px-4 sm:px-6 lg:px-8">
<div class="flex h-16 items-center justify-between">
<a href="/" rel="noopener nofollow">
<div class="flex items-center space-x-3">
<i class="fab fa-github text-gh-text text-2xl"></i>
<span class="text-gh-text text-xl font-semibold">
<span id="username">{formattedUsername}</span>'s Repositories
</span>
</div>
</a>
{#if $isRepositoryPage}
<div class="flex items-center space-x-3 text-white">
<button
on:click={goBack}
class="gh-accent flex items-center space-x-2 text-white transition-colors hover:opacity-80"
>
<i class="fas fa-arrow-left"></i>
<span class="hidden text-white sm:inline">Back to Repositories</span>
</button>
</div>
{/if}
</div>
</div>
</nav>

View File

@ -0,0 +1,116 @@
<script>
export let nickname = '';
export let username = '';
export let avatarUrl = '';
export let stats = { repositories: 0, followers: 0, following: 0 };
export let bio = '';
export let location = '';
export let blog = '';
export let company = '';
</script>
<!-- Profile Section -->
<div
class="gh-bg-secondary mb-8 rounded-2xl border p-6 sm:p-8"
style="border: 1px solid var(--gh-border)"
>
<div
class="flex flex-col items-center space-y-6 lg:flex-row lg:items-start lg:space-y-0 lg:space-x-8"
>
<!-- Left side: Avatar + Info -->
<div
class="flex flex-1 flex-col items-center space-y-4 sm:flex-row sm:items-start sm:space-y-0 sm:space-x-6"
>
<!-- Avatar -->
<div class="relative">
<img
src={avatarUrl || '/assets/img/placeholder.jpg'}
alt={`${username || 'Placeholder'}'s avatar`}
draggable="false"
class="h-24 w-24 rounded-full border-4 sm:h-32 sm:w-32"
style="border-color: var(--gh-border)"
/>
</div>
<!-- Profile Info -->
<div class="flex-1 text-center sm:text-left">
{#if nickname}
<h1 class="text-gh-text mb-2 text-2xl font-bold sm:text-3xl">{nickname}</h1>
{/if}
{#if username}
<p class="text-gh-text-secondary mb-3">@{username}</p>
{/if}
{#if location || blog || company}
<div
class="gh-text-secondary mb-4 flex flex-wrap items-center justify-center gap-4 text-sm sm:justify-start"
>
{#if location}
<div class="flex items-center space-x-1">
<i class="fas fa-map-marker-alt gh-text-muted"></i>
<span>{location}</span>
</div>
{/if}
{#if company}
<div class="flex items-center space-x-1">
<i class="fas fa-building gh-text-muted"></i>
<span>{company}</span>
</div>
{/if}
{#if blog}
<div class="flex items-center space-x-1">
<i class="fas fa-blog gh-text-muted"></i>
<a
href={blog}
target="_blank"
title="Visit my blog"
rel="noopener noreferrer"
class="transition-colors duration-300 hover:text-white"
>
<span>{blog.replace(/^https?:\/\/(www\.)?/, '')}</span>
</a>
</div>
{/if}
</div>
{/if}
{#if bio}
<p class="text-gh-text-secondary mb-6 max-w-md" title={bio}>{bio}</p>
{/if}
</div>
</div>
<!-- Right side: Stats boxes -->
<div class="flex flex-col space-y-3 lg:min-w-[200px]">
<!-- Repos Box -->
<div
class="gh-bg-tertiary hover:border-gh-accent rounded-lg border p-1 text-center transition-colors"
style="border: 1px solid var(--gh-border)"
>
<div class="gh-text text-2xl font-bold">{stats.repositories}</div>
<div class="gh-text-secondary text-sm">Owned Repos</div>
</div>
<!-- Followers Box -->
<div
class="gh-bg-tertiary hover:border-gh-accent rounded-lg border p-1 text-center transition-colors"
style="border: 1px solid var(--gh-border)"
>
<div class="gh-text text-2xl font-bold">{stats.followers}</div>
<div class="gh-text-secondary text-sm">Followers</div>
</div>
<!-- Following Box -->
<div
class="gh-bg-tertiary hover:border-gh-accent rounded-lg border p-1 text-center transition-colors"
style="border: 1px solid var(--gh-border)"
>
<div class="gh-text text-2xl font-bold">{stats.following}</div>
<div class="gh-text-secondary text-sm">Following</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,37 @@
<script>
import { formatDistanceToNow } from 'date-fns';
import LangBadge from './ui/LangBadge.svelte';
import RepoCard from './ui/RepoCard.svelte';
/**
* @type {any[]}
*/
export let repositories = [];
function formatDate(date) {
if (!date) return 'Unknown date';
return formatDistanceToNow(new Date(date), { addSuffix: true });
}
</script>
<div
class="gh-bg-secondary rounded-2xl border p-6 sm:p-8"
style="border: 1px solid var(--gh-border)"
>
<div class="mb-6 flex items-center space-x-3">
<i class="fas fa-folder-open text-gh-accent text-xl"></i>
<h2 class="text-gh-text text-xl font-bold sm:text-2xl">My public repositories</h2>
</div>
<div class="space-y-4">
{#each repositories as repo (repo.id)}
<RepoCard {repo} />
{/each}
</div>
</div>
<style>
.repo-card:hover {
border-color: rgba(var(--gh-accent-rgba), 0.5) !important;
}
</style>

View File

@ -0,0 +1,36 @@
<script>
import colors from '$lib/data/colors.json';
export let lang = '';
export let percent = 0;
export let mode = 'badge'; // badge ou bar
// couleur récupérée dans le JSON
const color = colors[lang]?.color || '#4b5563'; // gris par défaut
// opacity 20%
const opacity20 = color + '33';
</script>
{#if mode === 'badge'}
<div
class="inline-flex items-center space-x-1 rounded px-2 py-0.5 text-sm font-medium"
style="background-color: {opacity20}; color: {color};"
>
<span class="h-2.5 w-2.5 rounded-full" style="background-color: {color};"></span>
<span>{lang}</span>
</div>
{:else if mode === 'bar'}
<div>
<div class="mb-2 flex items-center justify-between">
<div class="flex items-center space-x-2">
<div class="h-3 w-3 rounded-full" style="background-color: {color};"></div>
<span class="gh-text font-medium">{lang}</span>
</div>
<span class="gh-text-secondary text-sm">{percent.toFixed(1)}%</span>
</div>
<div class="bg-gh-border-muted h-2 w-full rounded-full">
<div class="h-2 rounded-full" style="width: {percent}%; background-color: {color};"></div>
</div>
</div>
{/if}

View File

@ -0,0 +1,191 @@
<script>
import LangBadge from './LangBadge.svelte';
import { formatDistanceToNow } from 'date-fns';
export let repo;
const formatDate = (date) => {
if (!date) return 'Unknown date';
return formatDistanceToNow(new Date(date), { addSuffix: true });
};
const repoFullName = repo.full_name || `${repo.owner.login}/${repo.name}`;
const repoUrl = `https://github.com/${repoFullName}`;
const stargazersUrl = `${repoUrl}/stargazers`;
const forksUrl = `${repoUrl}/network/members`;
const langSearchUrl = `https://github.com/search?q=user:${repo.owner.login}+language:${repo.language}`;
</script>
<article
class="repo-card bg-gh-bg-secondary border-gh-border hover:border-gh-accent relative cursor-default rounded-2xl border p-6 shadow-md transition-shadow hover:shadow-lg"
aria-label={`Repository card for ${repoFullName}`}
>
<header class="mb-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div class="text-gh-accent flex max-w-full items-center gap-2 truncate text-lg font-semibold">
<!-- svelte-ignore a11y_consider_explicit_label -->
<a
href={`/repository/${repoFullName.toLowerCase()}`}
rel="noopener noreferrer"
title={`Open ${repoFullName}'s summary`}
>
<span
class="text-gh-accent hover:text-gh-accent-emphasis cursor-help"
title="Click to see detailed summary on this site"
aria-label="Repository summary available"
>
<i class="fas fa-eye"></i>
</span>
</a>
{repoFullName}
</div>
<div class="text-gh-text-secondary mt-3 flex space-x-5 text-sm sm:mt-0">
<a
href={stargazersUrl}
target="_blank"
rel="noopener noreferrer"
class="hover:text-gh-attention flex cursor-pointer items-center space-x-1 transition-colors"
title="View stargazers"
>
<i class="fas fa-star"></i>
<span>{repo.stargazers_count || 0}</span>
</a>
<a
href={forksUrl}
target="_blank"
rel="noopener noreferrer"
class="hover:text-gh-accent flex cursor-pointer items-center space-x-1 transition-colors"
title="View forks"
>
<i class="fas fa-code-branch"></i>
<span>{repo.forks_count || 0}</span>
</a>
</div>
</header>
<p class="text-gh-text-secondary min-h-[3rem]">
{repo.description || 'No description has been set.'}
</p>
<footer
class="text-gh-text-secondary flex flex-col items-center justify-between text-sm sm:flex-row"
>
<div class="flex flex-col items-center gap-2 sm:flex-row">
{#if repo.language}
<a
href={langSearchUrl}
target="_blank"
rel="noopener noreferrer"
class="hover:text-gh-accent flex cursor-pointer items-center space-x-2 transition-colors"
title={`See all repos by language: ${repo.language}`}
>
<LangBadge lang={repo.language} />
</a>
{/if}
{#if repo.updated_at}
<span title={`Last updated on ${new Date(repo.updated_at).toLocaleDateString()}`}>
Updated {formatDate(repo.updated_at)}
</span>
{/if}
{#if repo.created_at}
<span title={`Created on ${new Date(repo.created_at).toLocaleDateString()}`}>
Created {formatDate(repo.created_at)}
</span>
{/if}
</div>
<div class="flex items-center gap-4" role="group" aria-label="Repository external links">
{#if repo.html_url}
<a href={repo.html_url} target="_blank" rel="noopener noreferrer">
<button
class="gh-bg-tertiary gh-text flex items-center justify-center space-x-2 rounded-lg border px-4 py-2 transition-colors hover:bg-gray-600"
style="border: 1px solid var(--gh-border)"
type="button"
>
<i class="fas fa-code"></i>
<span>View Code</span>
</button>
</a>
{/if}
{#if repo.homepage}
<a href={repo.homepage} target="_blank" rel="noopener noreferrer">
<button
class="bg-gh-accent-emphasis hover:bg-gh-accent flex items-center justify-center space-x-2 rounded-lg px-4 py-2 text-white transition-colors"
type="button"
>
<i class="fas fa-external-link-alt"></i>
<span>View Site</span>
</button>
</a>
{/if}
</div>
</footer>
</article>
<style>
.repo-card {
background-color: var(--gh-bg-secondary);
border-color: var(--gh-border);
color: var(--gh-text-secondary);
position: relative;
}
.repo-card:hover {
border-color: var(--gh-accent);
box-shadow: 0 8px 16px rgba(59, 130, 246, 0.3);
color: var(--gh-text);
}
a {
text-decoration: none;
}
/* Styles pour les écrans de PC */
@media (min-width: 641px) {
.repo-card {
padding: 1.5rem;
}
.repo-card footer {
flex-direction: row;
justify-content: space-between;
}
.repo-card footer div[role='group'] {
flex-direction: row;
margin-top: 15px;
}
}
/* Styles pour les écrans de téléphone */
@media (max-width: 640px) {
.repo-card {
padding: 1rem;
}
.repo-card header {
flex-direction: column;
align-items: center;
}
.repo-card footer {
flex-direction: column;
align-items: center;
}
.repo-card footer div:first-child {
flex-direction: column;
align-items: center;
}
.repo-card footer div[role='group'] {
flex-direction: row;
justify-content: center;
width: 100%;
margin-top: 15px;
}
.repo-card footer button {
flex: 1;
}
}
</style>

105
src/lib/css/main.css Normal file
View File

@ -0,0 +1,105 @@
@import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css');
#username::first-letter {
text-transform: capitalize !important;
}
:root {
--gh-bg: #0d1117;
--gh-bg-secondary: #161b22;
--gh-bg-tertiary: #21262d;
--gh-border: #30363d;
--gh-border-muted: #21262d;
--gh-text: #f0f6fc;
--gh-text-secondary: #7d8590;
--gh-text-muted: #656d76;
--gh-accent: #58a6ff;
--gh-accent-emphasis: #1f6feb;
--gh-success: #3fb950;
--gh-attention: #d29922;
--gh-severe: #db6d28;
--gh-danger: #f85149;
}
.gh-bg {
background-color: var(--gh-bg);
}
.gh-bg-secondary {
background-color: var(--gh-bg-secondary);
}
.gh-bg-tertiary {
background-color: var(--gh-bg-tertiary);
}
.gh-border {
border-color: var(--gh-border);
}
.gh-border-muted {
border-color: var(--gh-border-muted);
}
.gh-text {
color: var(--gh-text);
}
.gh-text-secondary {
color: var(--gh-text-secondary);
}
.gh-text-muted {
color: var(--gh-text-muted);
}
.gh-accent {
color: var(--gh-accent);
}
.gh-accent-emphasis {
color: var(--gh-accent-emphasis);
}
.gh-success {
color: var(--gh-success);
}
.gh-attention {
color: var(--gh-attention);
}
.gh-danger {
color: var(--gh-danger);
}
.bg-gh-accent {
background-color: var(--gh-accent);
}
.bg-gh-accent-emphasis {
background-color: var(--gh-accent-emphasis);
}
.bg-gh-success {
background-color: var(--gh-success);
}
.bg-gh-attention {
background-color: var(--gh-attention);
}
.bg-gh-danger {
background-color: var(--gh-danger);
}
.border-gh-accent {
border-color: var(--gh-accent);
}
.border-gh-success {
border-color: var(--gh-success);
}
.border-gh-attention {
border-color: var(--gh-attention);
}
.border-gh-danger {
border-color: var(--gh-danger);
}
.hover\:border-gh-accent:hover {
border-color: var(--gh-accent);
}
.hover\:bg-gh-accent:hover {
background-color: var(--gh-accent);
}
.hover\:bg-gh-accent-emphasis:hover {
background-color: var(--gh-accent-emphasis);
}
body {
background-color: var(--gh-bg);
color: var(--gh-text);
}

3
src/lib/css/tailwind.css Normal file
View File

@ -0,0 +1,3 @@
@import 'tailwindcss';
@plugin '@tailwindcss/forms';
@plugin '@tailwindcss/typography';

2742
src/lib/data/colors.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1 +0,0 @@
<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#24292f"/></svg>

Before

Width:  |  Height:  |  Size: 963 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.1566,22.8189c-10.4-14.8851-30.94-19.2971-45.7914-9.8348L22.2825,29.6078A29.9234,29.9234,0,0,0,8.7639,49.6506a31.5136,31.5136,0,0,0,3.1076,20.2318A30.0061,30.0061,0,0,0,7.3953,81.0653a31.8886,31.8886,0,0,0,5.4473,24.1157c10.4022,14.8865,30.9423,19.2966,45.7914,9.8348L84.7167,98.3921A29.9177,29.9177,0,0,0,98.2353,78.3493,31.5263,31.5263,0,0,0,95.13,58.117a30,30,0,0,0,4.4743-11.1824,31.88,31.88,0,0,0-5.4473-24.1157" style="fill:#ff3e00"/><path d="M45.8171,106.5815A20.7182,20.7182,0,0,1,23.58,98.3389a19.1739,19.1739,0,0,1-3.2766-14.5025,18.1886,18.1886,0,0,1,.6233-2.4357l.4912-1.4978,1.3363.9815a33.6443,33.6443,0,0,0,10.203,5.0978l.9694.2941-.0893.9675a5.8474,5.8474,0,0,0,1.052,3.8781,6.2389,6.2389,0,0,0,6.6952,2.485,5.7449,5.7449,0,0,0,1.6021-.7041L69.27,76.281a5.4306,5.4306,0,0,0,2.4506-3.631,5.7948,5.7948,0,0,0-.9875-4.3712,6.2436,6.2436,0,0,0-6.6978-2.4864,5.7427,5.7427,0,0,0-1.6.7036l-9.9532,6.3449a19.0329,19.0329,0,0,1-5.2965,2.3259,20.7181,20.7181,0,0,1-22.2368-8.2427,19.1725,19.1725,0,0,1-3.2766-14.5024,17.9885,17.9885,0,0,1,8.13-12.0513L55.8833,23.7472a19.0038,19.0038,0,0,1,5.3-2.3287A20.7182,20.7182,0,0,1,83.42,29.6611a19.1739,19.1739,0,0,1,3.2766,14.5025,18.4,18.4,0,0,1-.6233,2.4357l-.4912,1.4978-1.3356-.98a33.6175,33.6175,0,0,0-10.2037-5.1l-.9694-.2942.0893-.9675a5.8588,5.8588,0,0,0-1.052-3.878,6.2389,6.2389,0,0,0-6.6952-2.485,5.7449,5.7449,0,0,0-1.6021.7041L37.73,51.719a5.4218,5.4218,0,0,0-2.4487,3.63,5.7862,5.7862,0,0,0,.9856,4.3717,6.2437,6.2437,0,0,0,6.6978,2.4864,5.7652,5.7652,0,0,0,1.602-.7041l9.9519-6.3425a18.978,18.978,0,0,1,5.2959-2.3278,20.7181,20.7181,0,0,1,22.2368,8.2427,19.1725,19.1725,0,0,1,3.2766,14.5024,17.9977,17.9977,0,0,1-8.13,12.0532L51.1167,104.2528a19.0038,19.0038,0,0,1-5.3,2.3287" style="fill:#fff"/></svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 352 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

50
src/routes/+error.svelte Normal file
View File

@ -0,0 +1,50 @@
<script lang="ts">
import { page } from '$app/state';
</script>
<div class="not-found">
<h1>{page.error.message}</h1>
<a href="/" class="btn-home" aria-label="Retour à l'accueil">Back to homepage</a>
</div>
<style>
.not-found {
height: 80vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
color: var(--gh-text-primary, #333);
padding: 1rem;
}
.not-found h1 {
font-size: 6rem;
font-weight: 900;
margin-bottom: 1rem;
color: var(--gh-accent, #ff4500);
}
.not-found p {
font-size: 1.5rem;
margin-bottom: 2rem;
color: var(--gh-text-secondary, #666);
}
.btn-home {
background-color: var(--gh-accent-emphasis, #ff6347);
color: white;
border: none;
padding: 0.75rem 2rem;
border-radius: 0.5rem;
font-weight: 600;
cursor: pointer;
transition: background-color 0.3s ease;
text-decoration: none;
}
.btn-home:hover {
background-color: var(--gh-accent, #ff4500);
}
</style>

View File

@ -1,57 +1,37 @@
<script> <script>
import Header from './Header.svelte'; import '$lib/css/tailwind.css';
import '../app.css'; import '$lib/css/main.css';
import { page } from '$app/stores';
import { derived } from 'svelte/store';
import Navigation from '$lib/components/Navigation.svelte';
import { PUBLIC_GITHUB_USERNAME } from '$env/static/public';
let { children } = $props(); let { children } = $props();
const canonicalUrl = derived(page, ($page) => {
// Ex : https://username.github.io/#/path
// Remplace '/' au début
const path = $page.url.pathname.replace(/^\//, '');
return `https://${PUBLIC_GITHUB_USERNAME}.github.io/${path}`;
});
</script> </script>
<svelte:head>
<link rel="canonical" href={$canonicalUrl} />
<meta property="og:url" content={$canonicalUrl} />
</svelte:head>
<div class="app"> <div class="app">
<Header /> <Navigation username={PUBLIC_GITHUB_USERNAME} />
<div class="mx-auto max-w-6xl px-4 py-8 sm:px-6 lg:px-8">
<main> <div
{@render children()} class="gh-bg-secondary mb-8 rounded-2xl border p-6 sm:p-8"
</main> style="border: 1px solid var(--gh-border)"
>
<footer> <main>
<p> {@render children()}
visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to learn about SvelteKit </main>
</p> </div>
</footer> </div>
</div> </div>
<style>
.app {
display: flex;
flex-direction: column;
min-height: 100vh;
}
main {
flex: 1;
display: flex;
flex-direction: column;
padding: 1rem;
width: 100%;
max-width: 64rem;
margin: 0 auto;
box-sizing: border-box;
}
footer {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 12px;
}
footer a {
font-weight: bold;
}
@media (min-width: 480px) {
footer {
padding: 12px 0;
}
}
</style>

View File

@ -1,3 +0,0 @@
// since there's no dynamic data here, we can prerender
// it so that it gets served as a static asset in production
export const prerender = true;

View File

@ -1,59 +1,91 @@
<script> <script>
import Counter from './Counter.svelte'; // @ts-nocheck
import welcome from '$lib/images/svelte-welcome.webp';
import welcomeFallback from '$lib/images/svelte-welcome.png'; import MetaSeo from '$lib/components/MetaSEO.svelte';
import ProfileSection from '$lib/components/ProfileSection.svelte';
import RepositorySection from '$lib/components/RepositorySection.svelte';
import { PUBLIC_GITHUB_USERNAME, PUBLIC_GITHUB_API_URL } from '$env/static/public';
import { onMount } from 'svelte';
let user = {
nickname: PUBLIC_GITHUB_USERNAME,
username: PUBLIC_GITHUB_USERNAME,
bio: '',
location: '',
blog: '',
company: '',
avatarUrl: '/assets/img/placeholder.jpg',
stats: { repositories: 0, followers: 0, following: 0 }
};
let repositories = [];
onMount(async () => {
const BASE_URL = PUBLIC_GITHUB_API_URL;
const USERNAME = PUBLIC_GITHUB_USERNAME;
try {
// Appel public pour récupérer les infos du user
const userResponse = await fetch(`${BASE_URL}/users/${USERNAME}`);
if (!userResponse.ok) throw new Error('Error recovering user data');
const userData = await userResponse.json();
user.nickname = userData.name || userData.login[0].toUpperCase() + userData.login.slice(1);
user.username = userData.login;
user.avatarUrl = userData.avatar_url;
user.stats = {
repositories: userData.public_repos,
followers: userData.followers,
following: userData.following
};
user.bio = userData.bio;
user.location = userData.location;
user.blog = userData.blog;
user.company = userData.company;
// Appel public pour récupérer les repos
const reposResponse = await fetch(`${BASE_URL}/users/${USERNAME}/repos?per_page=100`);
if (!reposResponse.ok) throw new Error('Error retrieving repositories');
const reposData = await reposResponse.json();
repositories = reposData.filter(
(/** @type {{ name: string; description: string; }} */ repo) => {
const forbiddenNames = ['.github', 'DiscussionsHost'];
const hasForbiddenWordInName = ['demo', 'backup', 'test', 'old'].some((word) =>
repo.name.toLowerCase().includes(word)
);
const hasForbiddenDescription =
repo.description && repo.description.toLowerCase().includes('just a redirect');
return (
!forbiddenNames.includes(repo.name) &&
!hasForbiddenWordInName &&
!hasForbiddenDescription
);
}
);
console.log(repositories);
} catch (error) {
console.error('Error retrieving GitHub data:', error);
}
});
</script> </script>
<svelte:head> <MetaSeo
<title>Home</title> title={`Repositories — ${user.nickname} (@${user.username})`}
<meta name="description" content="Svelte demo app" /> description={`Discover all the GitHub Repositories and primary statistics for ${user.nickname} (@${user.username}) on this page.`}
</svelte:head> />
<section> <ProfileSection
<h1> nickname={user.nickname}
<span class="welcome"> username={user.username}
<picture> avatarUrl={user.avatarUrl}
<source srcset={welcome} type="image/webp" /> stats={user.stats}
<img src={welcomeFallback} alt="Welcome" /> bio={user.bio}
</picture> location={user.location}
</span> blog={user.blog}
company={user.company}
/>
to your new<br />SvelteKit app <RepositorySection {repositories} />
</h1>
<h2>
try editing <strong>src/routes/+page.svelte</strong>
</h2>
<Counter />
</section>
<style>
section {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
flex: 0.6;
}
h1 {
width: 100%;
}
.welcome {
display: block;
position: relative;
width: 100%;
height: 0;
padding: 0 0 calc(100% * 495 / 2048) 0;
}
.welcome img {
position: absolute;
width: 100%;
height: 100%;
top: 0;
display: block;
}
</style>

View File

@ -1,103 +0,0 @@
<script>
import { Spring } from 'svelte/motion';
const count = new Spring(0);
const offset = $derived(modulo(count.current, 1));
/**
* @param {number} n
* @param {number} m
*/
function modulo(n, m) {
// handle negative numbers
return ((n % m) + m) % m;
}
</script>
<div class="counter">
<button onclick={() => (count.target -= 1)} aria-label="Decrease the counter by one">
<svg aria-hidden="true" viewBox="0 0 1 1">
<path d="M0,0.5 L1,0.5" />
</svg>
</button>
<div class="counter-viewport">
<div class="counter-digits" style="transform: translate(0, {100 * offset}%)">
<strong class="hidden" aria-hidden="true">{Math.floor(count.current + 1)}</strong>
<strong>{Math.floor(count.current)}</strong>
</div>
</div>
<button onclick={() => (count.target += 1)} aria-label="Increase the counter by one">
<svg aria-hidden="true" viewBox="0 0 1 1">
<path d="M0,0.5 L1,0.5 M0.5,0 L0.5,1" />
</svg>
</button>
</div>
<style>
.counter {
display: flex;
border-top: 1px solid rgba(0, 0, 0, 0.1);
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
margin: 1rem 0;
}
.counter button {
width: 2em;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
border: 0;
background-color: transparent;
touch-action: manipulation;
font-size: 2rem;
}
.counter button:hover {
background-color: var(--color-bg-1);
}
svg {
width: 25%;
height: 25%;
}
path {
vector-effect: non-scaling-stroke;
stroke-width: 2px;
stroke: #444;
}
.counter-viewport {
width: 8em;
height: 4em;
overflow: hidden;
text-align: center;
position: relative;
}
.counter-viewport strong {
position: absolute;
display: flex;
width: 100%;
height: 100%;
font-weight: 400;
color: var(--color-theme-1);
font-size: 4rem;
align-items: center;
justify-content: center;
}
.counter-digits {
position: absolute;
width: 100%;
height: 100%;
}
.hidden {
top: -100%;
user-select: none;
}
</style>

View File

@ -1,129 +0,0 @@
<script>
import { page } from '$app/state';
import logo from '$lib/images/svelte-logo.svg';
import github from '$lib/images/github.svg';
</script>
<header>
<div class="corner">
<a href="https://svelte.dev/docs/kit">
<img src={logo} alt="SvelteKit" />
</a>
</div>
<nav>
<svg viewBox="0 0 2 3" aria-hidden="true">
<path d="M0,0 L1,2 C1.5,3 1.5,3 2,3 L2,0 Z" />
</svg>
<ul>
<li aria-current={page.url.pathname === '/' ? 'page' : undefined}>
<a href="/">Home</a>
</li>
<li aria-current={page.url.pathname === '/about' ? 'page' : undefined}>
<a href="/about">About</a>
</li>
<li aria-current={page.url.pathname.startsWith('/sverdle') ? 'page' : undefined}>
<a href="/sverdle">Sverdle</a>
</li>
</ul>
<svg viewBox="0 0 2 3" aria-hidden="true">
<path d="M0,0 L0,3 C0.5,3 0.5,3 1,2 L2,0 Z" />
</svg>
</nav>
<div class="corner">
<a href="https://github.com/sveltejs/kit">
<img src={github} alt="GitHub" />
</a>
</div>
</header>
<style>
header {
display: flex;
justify-content: space-between;
}
.corner {
width: 3em;
height: 3em;
}
.corner a {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.corner img {
width: 2em;
height: 2em;
object-fit: contain;
}
nav {
display: flex;
justify-content: center;
--background: rgba(255, 255, 255, 0.7);
}
svg {
width: 2em;
height: 3em;
display: block;
}
path {
fill: var(--background);
}
ul {
position: relative;
padding: 0;
margin: 0;
height: 3em;
display: flex;
justify-content: center;
align-items: center;
list-style: none;
background: var(--background);
background-size: contain;
}
li {
position: relative;
height: 100%;
}
li[aria-current='page']::before {
--size: 6px;
content: '';
width: 0;
height: 0;
position: absolute;
top: 0;
left: calc(50% - var(--size));
border: var(--size) solid transparent;
border-top: var(--size) solid var(--color-theme-1);
}
nav a {
display: flex;
height: 100%;
align-items: center;
padding: 0 0.5rem;
color: var(--color-text);
font-weight: 700;
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.1em;
text-decoration: none;
transition: color 0.2s linear;
}
a:hover {
color: var(--color-theme-1);
}
</style>

View File

@ -1,9 +0,0 @@
import { dev } from '$app/environment';
// we don't need any JS on this page, though we'll load
// it in dev so that we get hot module replacement
export const csr = dev;
// since there's no dynamic data here, we can prerender
// it so that it gets served as a static asset in production
export const prerender = true;

View File

@ -1,26 +0,0 @@
<svelte:head>
<title>About</title>
<meta name="description" content="About this app" />
</svelte:head>
<div class="text-column">
<h1>About this app</h1>
<p>
This is a <a href="https://svelte.dev/docs/kit">SvelteKit</a> app. You can make your own by typing
the following into your command line and following the prompts:
</p>
<pre>npx sv create</pre>
<p>
The page you're looking at is purely static HTML, with no client-side interactivity needed.
Because of that, we don't need to load any JavaScript. Try viewing the page's source, or opening
the devtools network panel and reloading.
</p>
<p>
The <a href="/sverdle">Sverdle</a> page illustrates SvelteKit's data loading and form handling. Try
using it with JavaScript disabled!
</p>
</div>

View File

@ -0,0 +1,275 @@
<script>
// @ts-nocheck
import { PUBLIC_GITHUB_API_URL } from '$env/static/public';
import { page } from '$app/stores';
import { onDestroy } from 'svelte';
/**
* @type {string}
*/
let username;
/**
* @type {string}
*/
let repo;
const unsubscribe = page.subscribe(($page) => {
username = $page.params.username;
repo = $page.params.repo;
});
onDestroy(() => unsubscribe());
import { onMount } from 'svelte';
import LangBadge from '$lib/components/ui/LangBadge.svelte'; // adapte selon ton dossier
import { formatDistanceToNow } from 'date-fns';
let dataRepo = {
repository: null,
languages: {},
contributors: []
};
/**
* @type {null}
*/
let error = null;
const fetchData = async () => {
error = null;
try {
const repoRes = await fetch(`${PUBLIC_GITHUB_API_URL}/repos/${username}/${repo}`);
if (!repoRes.ok) throw new Error('Unable to retrieve info from repo');
const repoData = await repoRes.json();
const langRes = await fetch(`${PUBLIC_GITHUB_API_URL}/repos/${username}/${repo}/languages`);
if (!langRes.ok) throw new Error('Unable to retrieve languages');
const languagesData = await langRes.json();
const contRes = await fetch(
`${PUBLIC_GITHUB_API_URL}/repos/${username}/${repo}/contributors`
);
if (!contRes.ok) throw new Error('Unable to retrieve contributors');
const contributorsData = await contRes.json();
// Calcul pourcentages langages
const total = Object.values(languagesData).reduce((a, b) => a + b, 0);
const languagesWithPercent = {};
for (const [lang, bytes] of Object.entries(languagesData)) {
languagesWithPercent[lang] = (bytes / total) * 100;
}
dataRepo = {
repository: repoData,
languages: languagesWithPercent,
contributors: contributorsData
};
} catch (e) {
error = e.message;
dataRepo = { repository: null, languages: {}, contributors: [] };
}
};
const formatDate = (date) => {
if (!date) return 'Date inconnue';
return formatDistanceToNow(new Date(date), { addSuffix: true });
};
onMount(() => {
fetchData();
});
</script>
{#if error}
<div class="mb-6 rounded bg-red-600 p-4 text-white">
⚠️ {error} — Try again later, you may have exceeded the GitHub API limit without a token.
</div>
{:else if !dataRepo.repository}
<div>Loading...</div>
{:else}
<div
class="gh-bg-secondary mb-8 rounded-2xl border p-6 sm:p-8"
style="border: 1px solid var(--gh-border)"
>
<div class="mb-6 flex flex-col sm:flex-row sm:items-center sm:justify-between">
<div class="mb-4 flex items-center space-x-3 sm:mb-0">
<i class="fas fa-folder-open gh-accent text-2xl"></i>
<div>
<h1 class="gh-text text-2xl font-bold sm:text-3xl">
{dataRepo?.repository?.full_name}
</h1>
<span
class="gh-success mt-3 inline-block rounded-full border px-2 py-1 text-xs"
style="background-color: rgba(63, 185, 80, 0.2); border-color: rgba(63, 185, 80, 0.3)"
>
{dataRepo?.repository?.visibility?.toUpperCase()}
</span>
{#if dataRepo?.repository?.archived}
<span
class="mt-3 ml-2 inline-block rounded-full border bg-orange-700/25 px-2 py-1 text-xs text-orange-500"
>
ARCHIVED
</span>
{/if}
</div>
</div>
<div class="flex flex-col gap-3 sm:flex-row">
{#if dataRepo?.repository?.html_url}
<a href={dataRepo?.repository?.html_url} target="_blank" rel="noopener noreferrer">
<button
class="gh-bg-tertiary gh-text flex items-center justify-center space-x-2 rounded-lg border px-4 py-2 transition-colors hover:bg-gray-600"
style="border: 1px solid var(--gh-border)"
type="button"
>
<i class="fas fa-code"></i>
<span>View Code</span>
</button>
</a>
{/if}
{#if dataRepo?.repository?.homepage}
<a href={dataRepo?.repository?.homepage} target="_blank" rel="noopener noreferrer">
<button
class="bg-gh-accent-emphasis hover:bg-gh-accent flex items-center justify-center space-x-2 rounded-lg px-4 py-2 text-white transition-colors"
type="button"
>
<i class="fas fa-external-link-alt"></i>
<span>View Site</span>
</button>
</a>
{/if}
</div>
</div>
<p class="gh-text-secondary mb-4">
{dataRepo?.repository?.description || 'No description has been set.'}
</p>
{#if dataRepo?.repository?.created_at || dataRepo?.repository?.updated_at}
<div class="gh-text-secondary flex flex-wrap items-center gap-4 text-sm">
{#if dataRepo?.repository?.created_at}
<div class="flex items-center space-x-1">
<i class="fas fa-calendar-plus gh-text-muted"></i>
<span>Created {formatDate(dataRepo?.repository?.created_at)}</span>
</div>
{/if}
{#if dataRepo?.repository?.updated_at}
<div class="flex items-center space-x-1">
<i class="fas fa-sync-alt gh-text-muted"></i>
<span>Updated {formatDate(dataRepo?.repository?.updated_at)}</span>
</div>
{/if}
</div>
{/if}
</div>
<div class="grid grid-cols-1 gap-8 lg:grid-cols-3">
<!-- Statistics -->
<div class="lg:col-span-2">
<div
class="gh-bg-secondary mb-8 rounded-2xl border p-6 sm:p-8"
style="border: 1px solid var(--gh-border)"
>
<div class="mb-6 flex items-center space-x-3">
<i class="fas fa-chart-bar gh-accent text-xl"></i>
<h2 class="gh-text text-xl font-bold">Statistics</h2>
</div>
<div class="grid grid-cols-2 gap-4 sm:grid-cols-4">
<div
class="gh-bg-tertiary stat-card stars rounded-xl border border-[var(--gh-border)] p-4 text-center transition-colors hover:border-yellow-400"
>
<i class="fas fa-star gh-attention mb-2 text-2xl"></i>
<div class="gh-text text-2xl font-bold">{dataRepo?.repository?.stargazers_count}</div>
<div class="gh-text-secondary text-sm">Stars</div>
</div>
<div
class="gh-bg-tertiary stat-card forks rounded-xl border border-[var(--gh-border)] p-4 text-center transition-colors hover:border-blue-400"
>
<i class="fas fa-code-branch gh-accent mb-2 text-2xl"></i>
<div class="gh-text text-2xl font-bold">{dataRepo?.repository?.forks_count}</div>
<div class="gh-text-secondary text-sm">Forks</div>
</div>
<div
class="gh-bg-tertiary stat-card watchers rounded-xl border border-[var(--gh-border)] p-4 text-center transition-colors hover:border-green-400"
>
<i class="fas fa-eye gh-success mb-2 text-2xl"></i>
<div class="gh-text text-2xl font-bold">{dataRepo?.repository?.watchers_count}</div>
<div class="gh-text-secondary text-sm">Watchers</div>
</div>
<div
class="gh-bg-tertiary stat-card issues rounded-xl border border-[var(--gh-border)] p-4 text-center transition-colors hover:border-red-400"
>
<i class="fas fa-exclamation-triangle gh-danger mb-2 text-2xl"></i>
<div class="gh-text text-2xl font-bold">
{dataRepo?.repository?.open_issues_count}
</div>
<div class="gh-text-secondary text-sm">Issues</div>
</div>
</div>
</div>
<!-- Languages -->
<div
class="gh-bg-secondary rounded-2xl border p-6 sm:p-8"
style="border: 1px solid var(--gh-border)"
>
<div class="mb-6 flex items-center space-x-3">
<i class="fas fa-code gh-accent text-xl"></i>
<h2 class="gh-text text-xl font-bold">Languages</h2>
</div>
<div class="space-y-4">
{#each Object.entries(dataRepo.languages) as [language, percent]}
<LangBadge lang={language} {percent} mode="bar" />
{/each}
</div>
</div>
</div>
<!-- Contributors -->
<div class="lg:col-span-1">
<div
class="gh-bg-secondary rounded-2xl border p-6 sm:p-8"
style="border: 1px solid var(--gh-border)"
>
<div class="mb-6 flex items-center space-x-3">
<i class="fas fa-users gh-accent text-xl"></i>
<h2 class="gh-text text-xl font-bold">Contributors ({dataRepo.contributors.length})</h2>
</div>
<div class="space-y-4">
{#each dataRepo.contributors as contributor}
<div
class="gh-bg-tertiary flex items-center space-x-3 rounded-xl border p-3 transition-colors hover:bg-gray-600"
style="border: 1px solid var(--gh-border-muted)"
>
<img
src={contributor.avatar_url || '/assets/img/placeholder.jpg'}
alt={contributor.login}
class="h-10 w-10 rounded-full border-2"
style="border-color: var(--gh-border)"
/>
<div class="flex-1">
<a
href={contributor.html_url}
title="See him on GitHub"
target="_blank"
rel="noopener"
>
<div class="gh-text font-medium">{contributor.login}</div>
</a>
<div class="gh-text-secondary text-sm">
{contributor.contributions} contributions
</div>
</div>
</div>
{/each}
</div>
</div>
</div>
</div>
{/if}

View File

@ -1,70 +0,0 @@
import { fail } from '@sveltejs/kit';
import { Game } from './game';
/** @satisfies {import('./$types').PageServerLoad} */
export const load = ({ cookies }) => {
const game = new Game(cookies.get('sverdle'));
return {
/**
* The player's guessed words so far
*/
guesses: game.guesses,
/**
* An array of strings like '__x_c' corresponding to the guesses, where 'x' means
* an exact match, and 'c' means a close match (right letter, wrong place)
*/
answers: game.answers,
/**
* The correct answer, revealed if the game is over
*/
answer: game.answers.length >= 6 ? game.answer : null
};
};
/** @satisfies {import('./$types').Actions} */
export const actions = {
/**
* Modify game state in reaction to a keypress. If client-side JavaScript
* is available, this will happen in the browser instead of here
*/
update: async ({ request, cookies }) => {
const game = new Game(cookies.get('sverdle'));
const data = await request.formData();
const key = data.get('key');
const i = game.answers.length;
if (key === 'backspace') {
game.guesses[i] = game.guesses[i].slice(0, -1);
} else {
game.guesses[i] += key;
}
cookies.set('sverdle', game.toString(), { path: '/' });
},
/**
* Modify game state in reaction to a guessed word. This logic always runs on
* the server, so that people can't cheat by peeking at the JavaScript
*/
enter: async ({ request, cookies }) => {
const game = new Game(cookies.get('sverdle'));
const data = await request.formData();
const guess = /** @type {string[]} */ (data.getAll('guess'));
if (!game.enter(guess)) {
return fail(400, { badGuess: true });
}
cookies.set('sverdle', game.toString(), { path: '/' });
},
restart: async ({ cookies }) => {
cookies.delete('sverdle', { path: '/' });
}
};

View File

@ -1,418 +0,0 @@
<script>
import { enhance } from '$app/forms';
import { confetti } from '@neoconfetti/svelte';
import { MediaQuery } from 'svelte/reactivity';
/**
* @typedef {Object} Props
* @property {import('./$types').PageData} data
* @property {import('./$types').ActionData} form
*/
/**
* @type {Props}
*/
let { data, form = $bindable() } = $props();
/** Whether the user prefers reduced motion */
const reducedMotion = new MediaQuery('(prefers-reduced-motion: reduce)');
/** Whether or not the user has won */
let won = $derived(data.answers.at(-1) === 'xxxxx');
/** The index of the current guess */
let i = $derived(won ? -1 : data.answers.length);
/** The current guess */
let currentGuess = $derived(data.guesses[i] || '');
/** Whether the current guess can be submitted */
let submittable = $derived(currentGuess.length === 5);
const { classnames, description } = $derived.by(() => {
/**
* A map of classnames for all letters that have been guessed,
* used for styling the keyboard
* @type {Record<string, 'exact' | 'close' | 'missing'>}
*/
let classnames = {};
/**
* A map of descriptions for all letters that have been guessed,
* used for adding text for assistive technology (e.g. screen readers)
* @type {Record<string, string>}
*/
let description = {};
data.answers.forEach((answer, i) => {
const guess = data.guesses[i];
for (let i = 0; i < 5; i += 1) {
const letter = guess[i];
if (answer[i] === 'x') {
classnames[letter] = 'exact';
description[letter] = 'correct';
} else if (!classnames[letter]) {
classnames[letter] = answer[i] === 'c' ? 'close' : 'missing';
description[letter] = answer[i] === 'c' ? 'present' : 'absent';
}
}
});
return { classnames, description };
});
/**
* Modify the game state without making a trip to the server,
* if client-side JavaScript is enabled
* @param {MouseEvent} event
*/
function update(event) {
event.preventDefault();
const key = /** @type {HTMLButtonElement} */ (event.target).getAttribute('data-key');
if (key === 'backspace') {
currentGuess = currentGuess.slice(0, -1);
if (form?.badGuess) form.badGuess = false;
} else if (currentGuess.length < 5) {
currentGuess += key;
}
}
/**
* Trigger form logic in response to a keydown event, so that
* desktop users can use the keyboard to play the game
* @param {KeyboardEvent} event
*/
function keydown(event) {
if (event.metaKey) return;
if (event.key === 'Enter' && !submittable) return;
document
.querySelector(`[data-key="${event.key}" i]`)
?.dispatchEvent(new MouseEvent('click', { cancelable: true, bubbles: true }));
}
</script>
<svelte:window onkeydown={keydown} />
<svelte:head>
<title>Sverdle</title>
<meta name="description" content="A Wordle clone written in SvelteKit" />
</svelte:head>
<h1 class="visually-hidden">Sverdle</h1>
<form
method="post"
action="?/enter"
use:enhance={() => {
// prevent default callback from resetting the form
return ({ update }) => {
update({ reset: false });
};
}}
>
<a class="how-to-play" href="/sverdle/how-to-play">How to play</a>
<div class="grid" class:playing={!won} class:bad-guess={form?.badGuess}>
{#each Array.from(Array(6).keys()) as row (row)}
{@const current = row === i}
<h2 class="visually-hidden">Row {row + 1}</h2>
<div class="row" class:current>
{#each Array.from(Array(5).keys()) as column (column)}
{@const guess = current ? currentGuess : data.guesses[row]}
{@const answer = data.answers[row]?.[column]}
{@const value = guess?.[column] ?? ''}
{@const selected = current && column === guess.length}
{@const exact = answer === 'x'}
{@const close = answer === 'c'}
{@const missing = answer === '_'}
<div class="letter" class:exact class:close class:missing class:selected>
{value}
<span class="visually-hidden">
{#if exact}
(correct)
{:else if close}
(present)
{:else if missing}
(absent)
{:else}
empty
{/if}
</span>
<input name="guess" disabled={!current} type="hidden" {value} />
</div>
{/each}
</div>
{/each}
</div>
<div class="controls">
{#if won || data.answers.length >= 6}
{#if !won && data.answer}
<p>the answer was "{data.answer}"</p>
{/if}
<button data-key="enter" class="restart selected" formaction="?/restart">
{won ? 'you won :)' : `game over :(`} play again?
</button>
{:else}
<div class="keyboard">
<button data-key="enter" class:selected={submittable} disabled={!submittable}>enter</button>
<button
onclick={update}
data-key="backspace"
formaction="?/update"
name="key"
value="backspace"
>
back
</button>
{#each ['qwertyuiop', 'asdfghjkl', 'zxcvbnm'] as row (row)}
<div class="row">
{#each row as letter, index (index)}
<button
onclick={update}
data-key={letter}
class={classnames[letter]}
disabled={submittable}
formaction="?/update"
name="key"
value={letter}
aria-label="{letter} {description[letter] || ''}"
>
{letter}
</button>
{/each}
</div>
{/each}
</div>
{/if}
</div>
</form>
{#if won}
<div
style="position: absolute; left: 50%; top: 30%"
use:confetti={{
particleCount: reducedMotion.current ? 0 : undefined,
force: 0.7,
stageWidth: window.innerWidth,
stageHeight: window.innerHeight,
colors: ['#ff3e00', '#40b3ff', '#676778']
}}
></div>
{/if}
<style>
form {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1rem;
flex: 1;
}
.how-to-play {
color: var(--color-text);
}
.how-to-play::before {
content: 'i';
display: inline-block;
font-size: 0.8em;
font-weight: 900;
width: 1em;
height: 1em;
padding: 0.2em;
line-height: 1;
border: 1.5px solid var(--color-text);
border-radius: 50%;
text-align: center;
margin: 0 0.5em 0 0;
position: relative;
top: -0.05em;
}
.grid {
--width: min(100vw, 40vh, 380px);
max-width: var(--width);
align-self: center;
justify-self: center;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: flex-start;
}
.grid .row {
display: grid;
grid-template-columns: repeat(5, 1fr);
grid-gap: 0.2rem;
margin: 0 0 0.2rem 0;
}
@media (prefers-reduced-motion: no-preference) {
.grid.bad-guess .row.current {
animation: wiggle 0.5s;
}
}
.grid.playing .row.current {
filter: drop-shadow(3px 3px 10px var(--color-bg-0));
}
.letter {
aspect-ratio: 1;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
box-sizing: border-box;
text-transform: lowercase;
border: none;
font-size: calc(0.08 * var(--width));
border-radius: 2px;
background: white;
margin: 0;
color: rgba(0, 0, 0, 0.7);
}
.letter.missing {
background: rgba(255, 255, 255, 0.5);
color: rgba(0, 0, 0, 0.5);
}
.letter.exact {
background: var(--color-theme-2);
color: white;
}
.letter.close {
border: 2px solid var(--color-theme-2);
}
.selected {
outline: 2px solid var(--color-theme-1);
}
.controls {
text-align: center;
justify-content: center;
height: min(18vh, 10rem);
}
.keyboard {
--gap: 0.2rem;
position: relative;
display: flex;
flex-direction: column;
gap: var(--gap);
height: 100%;
}
.keyboard .row {
display: flex;
justify-content: center;
gap: 0.2rem;
flex: 1;
}
.keyboard button,
.keyboard button:disabled {
--size: min(8vw, 4vh, 40px);
background-color: white;
color: black;
width: var(--size);
border: none;
border-radius: 2px;
font-size: calc(var(--size) * 0.5);
margin: 0;
}
.keyboard button.exact {
background: var(--color-theme-2);
color: white;
}
.keyboard button.missing {
opacity: 0.5;
}
.keyboard button.close {
border: 2px solid var(--color-theme-2);
}
.keyboard button:focus {
background: var(--color-theme-1);
color: white;
outline: none;
}
.keyboard button[data-key='enter'],
.keyboard button[data-key='backspace'] {
position: absolute;
bottom: 0;
width: calc(1.5 * var(--size));
height: calc(1 / 3 * (100% - 2 * var(--gap)));
text-transform: uppercase;
font-size: calc(0.3 * var(--size));
padding-top: calc(0.15 * var(--size));
}
.keyboard button[data-key='enter'] {
right: calc(50% + 3.5 * var(--size) + 0.8rem);
}
.keyboard button[data-key='backspace'] {
left: calc(50% + 3.5 * var(--size) + 0.8rem);
}
.keyboard button[data-key='enter']:disabled {
opacity: 0.5;
}
.restart {
width: 100%;
padding: 1rem;
background: rgba(255, 255, 255, 0.5);
border-radius: 2px;
border: none;
}
.restart:focus,
.restart:hover {
background: var(--color-theme-1);
color: white;
outline: none;
}
@keyframes wiggle {
0% {
transform: translateX(0);
}
10% {
transform: translateX(-2px);
}
30% {
transform: translateX(4px);
}
50% {
transform: translateX(-6px);
}
70% {
transform: translateX(+4px);
}
90% {
transform: translateX(-2px);
}
100% {
transform: translateX(0);
}
}
</style>

View File

@ -1,72 +0,0 @@
import { words, allowed } from './words.server';
export class Game {
/**
* Create a game object from the player's cookie, or initialise a new game
* @param {string | undefined} serialized
*/
constructor(serialized = undefined) {
if (serialized) {
const [index, guesses, answers] = serialized.split('-');
this.index = +index;
this.guesses = guesses ? guesses.split(' ') : [];
this.answers = answers ? answers.split(' ') : [];
} else {
this.index = Math.floor(Math.random() * words.length);
this.guesses = ['', '', '', '', '', ''];
this.answers = /** @type {string[]} */ ([]);
}
this.answer = words[this.index];
}
/**
* Update game state based on a guess of a five-letter word. Returns
* true if the guess was valid, false otherwise
* @param {string[]} letters
*/
enter(letters) {
const word = letters.join('');
const valid = allowed.has(word);
if (!valid) return false;
this.guesses[this.answers.length] = word;
const available = Array.from(this.answer);
const answer = Array(5).fill('_');
// first, find exact matches
for (let i = 0; i < 5; i += 1) {
if (letters[i] === available[i]) {
answer[i] = 'x';
available[i] = ' ';
}
}
// then find close matches (this has to happen
// in a second step, otherwise an early close
// match can prevent a later exact match)
for (let i = 0; i < 5; i += 1) {
if (answer[i] === '_') {
const index = available.indexOf(letters[i]);
if (index !== -1) {
answer[i] = 'c';
available[index] = ' ';
}
}
}
this.answers.push(answer.join(''));
return true;
}
/**
* Serialize game state so it can be set as a cookie
*/
toString() {
return `${this.index}-${this.guesses.join(' ')}-${this.answers.join(' ')}`;
}
}

View File

@ -1,9 +0,0 @@
import { dev } from '$app/environment';
// we don't need any JS on this page, though we'll load
// it in dev so that we get hot module replacement
export const csr = dev;
// since there's no dynamic data here, we can prerender
// it so that it gets served as a static asset in production
export const prerender = true;

View File

@ -1,95 +0,0 @@
<svelte:head>
<title>How to play Sverdle</title>
<meta name="description" content="How to play Sverdle" />
</svelte:head>
<div class="text-column">
<h1>How to play Sverdle</h1>
<p>
Sverdle is a clone of <a href="https://www.nytimes.com/games/wordle/index.html">Wordle</a>, the
word guessing game. To play, enter a five-letter English word. For example:
</p>
<div class="example">
<span class="close">r</span>
<span class="missing">i</span>
<span class="close">t</span>
<span class="missing">z</span>
<span class="exact">y</span>
</div>
<p>
The <span class="exact">y</span> is in the right place. <span class="close">r</span> and
<span class="close">t</span>
are the right letters, but in the wrong place. The other letters are wrong, and can be discarded.
Let's make another guess:
</p>
<div class="example">
<span class="exact">p</span>
<span class="exact">a</span>
<span class="exact">r</span>
<span class="exact">t</span>
<span class="exact">y</span>
</div>
<p>This time we guessed right! You have <strong>six</strong> guesses to get the word.</p>
<p>
Unlike the original Wordle, Sverdle runs on the server instead of in the browser, making it
impossible to cheat. It uses <code>&lt;form&gt;</code> and cookies to submit data, meaning you can
even play with JavaScript disabled!
</p>
</div>
<style>
span {
display: inline-flex;
justify-content: center;
align-items: center;
font-size: 0.8em;
width: 2.4em;
height: 2.4em;
background-color: white;
box-sizing: border-box;
border-radius: 2px;
border-width: 2px;
color: rgba(0, 0, 0, 0.7);
}
.missing {
background: rgba(255, 255, 255, 0.5);
color: rgba(0, 0, 0, 0.5);
}
.close {
border-style: solid;
border-color: var(--color-theme-2);
}
.exact {
background: var(--color-theme-2);
color: white;
}
.example {
display: flex;
justify-content: flex-start;
margin: 1rem 0;
gap: 0.2rem;
}
.example span {
font-size: 1.4rem;
}
p span {
position: relative;
border-width: 1px;
border-radius: 1px;
font-size: 0.4em;
transform: scale(2) translate(0, -10%);
margin: 0 1em;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,17 @@
.noscript-message {
text-align: center;
}
.noscript-message h1 {
font-size: 2rem;
margin-bottom: 1rem;
}
.noscript-message p {
font-size: 1rem;
}
.noscript-message a {
color: #3b82f6;
text-decoration: none;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 109 KiB

View File

@ -0,0 +1,21 @@
{
"name": "",
"short_name": "",
"icons": [
{
"src": "/assets/favicon/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/assets/favicon/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"theme_color": "#000000",
"background_color": "#000000",
"display": "standalone"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

BIN
static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,5 +1,20 @@
import adapter from '@sveltejs/adapter-auto'; import adapter from '@sveltejs/adapter-static';
const config = { kit: { adapter: adapter() } }; const config = {
kit: {
adapter: adapter({
// default options are shown. On some platforms
// these options are set automatically — see below
pages: 'build',
assets: 'build/assets',
fallback: '404.html',
precompress: true,
strict: true
}),
paths: {
base: process.argv.includes('dev') ? '' : process.env.BASE_PATH
}
}
};
export default config; export default config;

32
tailwind.config.js Normal file
View File

@ -0,0 +1,32 @@
/** @type {import('tailwindcss').Config} */
import colors from './src/assets/github-colors/colors.json';
export default {
darkMode: ['class'],
content: ['./index.html', './src/**/*.{svelte,js,ts,jsx,tsx}', './*.{js,ts,jsx,tsx,mdx,html}'],
safelist: Object.entries(colors).flatMap(([lang, data]) => {
const hex = data.color?.replace('#', '');
return [`bg-[#${hex}/20]`, `text-[#${hex}]`];
}),
theme: {
extend: {
colors: {
'gh-bg': '#0d1117',
'gh-bg-secondary': '#161b22',
'gh-bg-tertiary': '#21262d',
'gh-border': '#30363d',
'gh-border-muted': '#21262d',
'gh-text': '#f0f6fc',
'gh-text-secondary': '#7d8590',
'gh-text-muted': '#656d76',
'gh-accent': '#58a6ff',
'gh-accent-emphasis': '#1f6feb',
'gh-success': '#3fb950',
'gh-attention': '#d29922',
'gh-severe': '#db6d28',
'gh-danger': '#f85149'
}
}
},
plugins: []
};

View File

@ -2,6 +2,25 @@ import tailwindcss from '@tailwindcss/vite';
import { sveltekit } from '@sveltejs/kit/vite'; import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import { createHtmlPlugin } from 'vite-plugin-html';
import { readFileSync } from 'fs';
import { resolve } from 'path';
function getPkgVersion(pkgPath) {
return JSON.parse(readFileSync(resolve(pkgPath), 'utf-8')).version;
}
export default defineConfig({ export default defineConfig({
plugins: [tailwindcss(), sveltekit()] plugins: [
tailwindcss(),
sveltekit(),
createHtmlPlugin({
inject: {
data: {
viteVersion: getPkgVersion('./node_modules/vite/package.json'),
sveltekitVersion: getPkgVersion('./node_modules/@sveltejs/kit/package.json')
}
}
})
]
}); });