custom homapage
This commit is contained in:
20
src/lib/components/Card.svelte
Normal file
20
src/lib/components/Card.svelte
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let title: string;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h1>{title}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.card {
|
||||||
|
margin: 1rem 0;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
BIN
src/lib/images/profil.webp
Normal file
BIN
src/lib/images/profil.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 352 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 113 KiB |
@@ -1,5 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import Header from './Header.svelte';
|
import Header from './Header.svelte';
|
||||||
|
import Footer from './Footer.svelte';
|
||||||
import './styles.css';
|
import './styles.css';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -10,9 +11,7 @@
|
|||||||
<slot />
|
<slot />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer>
|
<Footer />
|
||||||
<p>visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to learn SvelteKit</p>
|
|
||||||
</footer>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -32,22 +31,4 @@
|
|||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
box-sizing: border-box;
|
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>
|
</style>
|
||||||
|
|||||||
@@ -1,31 +1,14 @@
|
|||||||
<script>
|
<script>
|
||||||
import Counter from './Counter.svelte';
|
import Hero from './Hero.svelte';
|
||||||
import welcome from '$lib/images/svelte-welcome.webp';
|
|
||||||
import welcome_fallback from '$lib/images/svelte-welcome.png';
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>Home</title>
|
<title>Home</title>
|
||||||
<meta name="description" content="Svelte demo app" />
|
<meta name="description" content="Portfolio" />
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h1>
|
<Hero />
|
||||||
<span class="welcome">
|
|
||||||
<picture>
|
|
||||||
<source srcset={welcome} type="image/webp" />
|
|
||||||
<img src={welcome_fallback} alt="Welcome" />
|
|
||||||
</picture>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
to your new<br />SvelteKit app
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<h2>
|
|
||||||
try editing <strong>src/routes/+page.svelte</strong>
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<Counter />
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -36,24 +19,4 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
flex: 0.6;
|
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>
|
</style>
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { spring } from 'svelte/motion';
|
|
||||||
|
|
||||||
let count = 0;
|
|
||||||
|
|
||||||
const displayed_count = spring();
|
|
||||||
$: displayed_count.set(count);
|
|
||||||
$: offset = modulo($displayed_count, 1);
|
|
||||||
|
|
||||||
function modulo(n: number, m: number) {
|
|
||||||
// handle negative numbers
|
|
||||||
return ((n % m) + m) % m;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="counter">
|
|
||||||
<button on:click={() => (count -= 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($displayed_count + 1)}</strong>
|
|
||||||
<strong>{Math.floor($displayed_count)}</strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button on:click={() => (count += 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>
|
|
||||||
14
src/routes/Footer.svelte
Normal file
14
src/routes/Footer.svelte
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<footer>
|
||||||
|
<div>
|
||||||
|
<p>© 2020 - 2021 Created by <a href="Whidix">Whidix</a></p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header>
|
<header>
|
||||||
|
<div class="header">
|
||||||
<div class="corner">
|
<div class="corner">
|
||||||
<a href="https://kit.svelte.dev">
|
<a href="https://kit.svelte.dev">
|
||||||
<img src={logo} alt="SvelteKit" />
|
<img src={logo} alt="SvelteKit" />
|
||||||
@@ -12,9 +13,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav>
|
<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>
|
<ul>
|
||||||
<li aria-current={$page.url.pathname === '/' ? 'page' : undefined}>
|
<li aria-current={$page.url.pathname === '/' ? 'page' : undefined}>
|
||||||
<a href="/">Home</a>
|
<a href="/">Home</a>
|
||||||
@@ -22,13 +20,19 @@
|
|||||||
<li aria-current={$page.url.pathname === '/about' ? 'page' : undefined}>
|
<li aria-current={$page.url.pathname === '/about' ? 'page' : undefined}>
|
||||||
<a href="/about">About</a>
|
<a href="/about">About</a>
|
||||||
</li>
|
</li>
|
||||||
<li aria-current={$page.url.pathname.startsWith('/sverdle') ? 'page' : undefined}>
|
<li aria-current={$page.url.pathname === '/blog' ? 'page' : undefined}>
|
||||||
<a href="/sverdle">Sverdle</a>
|
<a href="/blog">Blog</a>
|
||||||
|
</li>
|
||||||
|
<li aria-current={$page.url.pathname.startsWith('/projects') ? 'page' : undefined}>
|
||||||
|
<a href="/projects">Projects</a>
|
||||||
|
</li>
|
||||||
|
<li aria-current={$page.url.pathname.startsWith('/shs') ? 'page' : undefined}>
|
||||||
|
<a href="/shs">SHS</a>
|
||||||
|
</li>
|
||||||
|
<li aria-current={$page.url.pathname.startsWith('/contact') ? 'page' : undefined}>
|
||||||
|
<a href="/contact">Contact</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</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>
|
</nav>
|
||||||
|
|
||||||
<div class="corner">
|
<div class="corner">
|
||||||
@@ -36,12 +40,28 @@
|
|||||||
<img src={github} alt="GitHub" />
|
<img src={github} alt="GitHub" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
header {
|
header {
|
||||||
display: flex;
|
border-top: 3px solid var(--color-nav);
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
background-color: white;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 80%;
|
||||||
|
margin: 0 auto;
|
||||||
|
position: relative;
|
||||||
|
top: 20px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgb(0 0 0/5%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.corner {
|
.corner {
|
||||||
@@ -66,21 +86,9 @@
|
|||||||
nav {
|
nav {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
--background: rgba(255, 255, 255, 0.7);
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
|
||||||
width: 2em;
|
|
||||||
height: 3em;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
path {
|
|
||||||
fill: var(--background);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
position: relative;
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
height: 3em;
|
height: 3em;
|
||||||
@@ -89,7 +97,6 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
background-size: contain;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
li {
|
li {
|
||||||
@@ -111,7 +118,7 @@
|
|||||||
|
|
||||||
nav a {
|
nav a {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 100%;
|
height: 80%;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 0.5rem;
|
padding: 0 0.5rem;
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
@@ -124,6 +131,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
a:hover {
|
||||||
color: var(--color-theme-1);
|
color: var(--color-nav);
|
||||||
|
background-color: var(--color-bg-nav);
|
||||||
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
46
src/routes/Hero.svelte
Normal file
46
src/routes/Hero.svelte
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<script>
|
||||||
|
import profil from '$lib/images/profil.webp';
|
||||||
|
import { confetti } from '@neoconfetti/svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div use:confetti />
|
||||||
|
<div class="hero">
|
||||||
|
<div>
|
||||||
|
<h1 class="hero-title">Theodor Vallier</h1>
|
||||||
|
<p class="hero-body">
|
||||||
|
Welcome on my portfolio, I'm an engineer in computer science and I'm passionate about web
|
||||||
|
development.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<img src={profil} alt="Profil" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.hero {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
font-size: 2.5em;
|
||||||
|
font-weight: 600;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background: linear-gradient(130deg, #5183f5, #af002d 41.07%, #c79191 76.05%);
|
||||||
|
background-clip: text;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-body {
|
||||||
|
font-size: 1.2em;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero img {
|
||||||
|
max-width: 300px;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 5%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -3,7 +3,3 @@ import { dev } from '$app/environment';
|
|||||||
// we don't need any JS on this page, though we'll load
|
// we don't need any JS on this page, though we'll load
|
||||||
// it in dev so that we get hot module replacement
|
// it in dev so that we get hot module replacement
|
||||||
export const csr = dev;
|
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;
|
|
||||||
|
|||||||
30
src/routes/projects/+page.svelte
Normal file
30
src/routes/projects/+page.svelte
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<script>
|
||||||
|
import Card from '$lib/components/Card.svelte';
|
||||||
|
import { projects } from './+page';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Projects</title>
|
||||||
|
<meta name="description" content="projects" />
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h1>Projects</h1>
|
||||||
|
|
||||||
|
<p>This is the page where all projects will live.</p>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<section class="card_list">
|
||||||
|
{#each projects as { title }}
|
||||||
|
<Card {title} />
|
||||||
|
{/each}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.card_list {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
}
|
||||||
|
</style>
|
||||||
1
src/routes/projects/+page.ts
Normal file
1
src/routes/projects/+page.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const projects = [{ title: 'Projet 1' }, { title: 'Projet 2' }];
|
||||||
30
src/routes/shs/+page.svelte
Normal file
30
src/routes/shs/+page.svelte
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<script>
|
||||||
|
import Card from '$lib/components/Card.svelte';
|
||||||
|
import { works } from './+page';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>SHS</title>
|
||||||
|
<meta name="description" content="SHS" />
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h1>SHS</h1>
|
||||||
|
|
||||||
|
<p>This is a page for SHS.</p>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<section class="card_list">
|
||||||
|
{#each works as title}
|
||||||
|
<Card {title} />
|
||||||
|
{/each}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.card_list {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
}
|
||||||
|
</style>
|
||||||
1
src/routes/shs/+page.ts
Normal file
1
src/routes/shs/+page.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const works = ['Home', 'About', 'Contact'];
|
||||||
20
src/routes/shs/works/internship.md
Normal file
20
src/routes/shs/works/internship.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
title: 🎚️ Internship report
|
||||||
|
date: 2022-11-13T21:57:10.852Z
|
||||||
|
summary:
|
||||||
|
tags:
|
||||||
|
- web
|
||||||
|
- svelte
|
||||||
|
---
|
||||||
|
|
||||||
|
During the year, I developed a website for a new association in Toulouse related to TISA (Toulouse, Computer Science, Security and Association). I developed this website using the Svelte framework because I was intrigued by its promise of performance and simplicity of development.
|
||||||
|
|
||||||
|
During this internship, I learned to use Svelte effectively and to set up a solid project architecture. I also had to work closely with the members of the association to understand their needs and vision for the website, in order to create a final product that met their expectations.
|
||||||
|
|
||||||
|
I started by setting up the development environment, installing all necessary tools and configuring the project. I also spent time learning the basics of Svelte and understanding how it worked. Once I had a good understanding of these concepts, I began developing the website using an iterative approach.
|
||||||
|
|
||||||
|
For each iteration, I first defined the goals and features to be implemented, and then created a detailed work plan. I then worked on implementing these features, ensuring that I followed best practices for development and quality standards. I also spent time testing the website to ensure it was functioning properly and meeting the requirements of the association.
|
||||||
|
|
||||||
|
During this internship, I learned to work independently and to make responsible decisions. I also learned to work as part of a team and to communicate effectively with the members of the association to understand their needs and vision. I also gained strong web development skills, particularly using Svelte, and was able to apply these skills to create a high-quality website for the association.
|
||||||
|
|
||||||
|
In conclusion, this internship was a very enriching experience for me, both professionally and personally. I learned new skills and was able to put my knowledge into practice on a concrete project. I also had the opportunity to work with a team and to contribute to the success of a new association.
|
||||||
@@ -7,8 +7,8 @@
|
|||||||
--color-bg-0: rgb(202, 216, 228);
|
--color-bg-0: rgb(202, 216, 228);
|
||||||
--color-bg-1: hsl(209, 36%, 86%);
|
--color-bg-1: hsl(209, 36%, 86%);
|
||||||
--color-bg-2: hsl(224, 44%, 95%);
|
--color-bg-2: hsl(224, 44%, 95%);
|
||||||
--color-theme-1: #ff3e00;
|
--color-nav: #2f5bc2;
|
||||||
--color-theme-2: #4075a6;
|
--color-bg-nav: #f7f7f7;
|
||||||
--color-text: rgba(0, 0, 0, 0.7);
|
--color-text: rgba(0, 0, 0, 0.7);
|
||||||
--column-width: 42rem;
|
--column-width: 42rem;
|
||||||
--column-margin-top: 4rem;
|
--column-margin-top: 4rem;
|
||||||
@@ -20,88 +20,7 @@ body {
|
|||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background-attachment: fixed;
|
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 {
|
a {
|
||||||
color: var(--color-theme-1);
|
|
||||||
text-decoration: none;
|
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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
import { fail } from '@sveltejs/kit';
|
|
||||||
import { Game } from './game';
|
|
||||||
import type { PageServerLoad, Actions } from './$types';
|
|
||||||
|
|
||||||
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 PageServerLoad;
|
|
||||||
|
|
||||||
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());
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 = data.getAll('guess') as string[];
|
|
||||||
|
|
||||||
if (!game.enter(guess)) {
|
|
||||||
return fail(400, { badGuess: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
cookies.set('sverdle', game.toString());
|
|
||||||
},
|
|
||||||
|
|
||||||
restart: async ({ cookies }) => {
|
|
||||||
cookies.delete('sverdle');
|
|
||||||
}
|
|
||||||
} satisfies Actions;
|
|
||||||
@@ -1,406 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { confetti } from '@neoconfetti/svelte';
|
|
||||||
import { enhance } from '$app/forms';
|
|
||||||
import type { PageData, ActionData } from './$types';
|
|
||||||
import { reduced_motion } from './reduced-motion';
|
|
||||||
|
|
||||||
export let data: PageData;
|
|
||||||
|
|
||||||
export let form: ActionData;
|
|
||||||
|
|
||||||
/** Whether or not the user has won */
|
|
||||||
$: won = data.answers.at(-1) === 'xxxxx';
|
|
||||||
|
|
||||||
/** The index of the current guess */
|
|
||||||
$: i = won ? -1 : data.answers.length;
|
|
||||||
|
|
||||||
/** Whether the current guess can be submitted */
|
|
||||||
$: submittable = data.guesses[i]?.length === 5;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A map of classnames for all letters that have been guessed,
|
|
||||||
* used for styling the keyboard
|
|
||||||
*/
|
|
||||||
let classnames: Record<string, 'exact' | 'close' | 'missing'>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A map of descriptions for all letters that have been guessed,
|
|
||||||
* used for adding text for assistive technology (e.g. screen readers)
|
|
||||||
*/
|
|
||||||
let description: Record<string, string>;
|
|
||||||
|
|
||||||
$: {
|
|
||||||
classnames = {};
|
|
||||||
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';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Modify the game state without making a trip to the server,
|
|
||||||
* if client-side JavaScript is enabled
|
|
||||||
*/
|
|
||||||
function update(event: MouseEvent) {
|
|
||||||
const guess = data.guesses[i];
|
|
||||||
const key = (event.target as HTMLButtonElement).getAttribute(
|
|
||||||
'data-key'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (key === 'backspace') {
|
|
||||||
data.guesses[i] = guess.slice(0, -1);
|
|
||||||
if (form?.badGuess) form.badGuess = false;
|
|
||||||
} else if (guess.length < 5) {
|
|
||||||
data.guesses[i] += key;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Trigger form logic in response to a keydown event, so that
|
|
||||||
* desktop users can use the keyboard to play the game
|
|
||||||
*/
|
|
||||||
function keydown(event: KeyboardEvent) {
|
|
||||||
if (event.metaKey) return;
|
|
||||||
|
|
||||||
document
|
|
||||||
.querySelector(`[data-key="${event.key}" i]`)
|
|
||||||
?.dispatchEvent(new MouseEvent('click', { cancelable: true }));
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svelte:window on:keydown={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(6) as _, row}
|
|
||||||
{@const current = row === i}
|
|
||||||
<h2 class="visually-hidden">Row {row + 1}</h2>
|
|
||||||
<div class="row" class:current>
|
|
||||||
{#each Array(5) as _, column}
|
|
||||||
{@const answer = data.answers[row]?.[column]}
|
|
||||||
{@const value = data.guesses[row]?.[column] ?? ''}
|
|
||||||
{@const selected = current && column === data.guesses[row].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
|
|
||||||
on:click|preventDefault={update}
|
|
||||||
data-key="backspace"
|
|
||||||
formaction="?/update"
|
|
||||||
name="key"
|
|
||||||
value="backspace"
|
|
||||||
>
|
|
||||||
back
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{#each ['qwertyuiop', 'asdfghjkl', 'zxcvbnm'] as row}
|
|
||||||
<div class="row">
|
|
||||||
{#each row as letter}
|
|
||||||
<button
|
|
||||||
on:click|preventDefault={update}
|
|
||||||
data-key={letter}
|
|
||||||
class={classnames[letter]}
|
|
||||||
disabled={data.guesses[i].length === 5}
|
|
||||||
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: $reduced_motion ? 0 : undefined,
|
|
||||||
force: 0.7,
|
|
||||||
stageWidth: window.innerWidth,
|
|
||||||
stageHeight: window.innerHeight,
|
|
||||||
colors: ['#ff3e00', '#40b3ff', '#676778']
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{/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>
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
import { words, allowed } from './words.server';
|
|
||||||
|
|
||||||
export class Game {
|
|
||||||
index: number;
|
|
||||||
guesses: string[];
|
|
||||||
answers: string[];
|
|
||||||
answer: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a game object from the player's cookie, or initialise a new game
|
|
||||||
*/
|
|
||||||
constructor(serialized: string | undefined = 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 = [] ;
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
*/
|
|
||||||
enter(letters: string[]) {
|
|
||||||
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(' ')}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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><form></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>
|
|
||||||
@@ -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;
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { readable } from 'svelte/store';
|
|
||||||
import { browser } from '$app/environment';
|
|
||||||
|
|
||||||
const reduced_motion_query = '(prefers-reduced-motion: reduce)';
|
|
||||||
|
|
||||||
const get_initial_motion_preference = () => {
|
|
||||||
if (!browser) return false;
|
|
||||||
return window.matchMedia(reduced_motion_query).matches;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const reduced_motion = readable(get_initial_motion_preference(), (set) => {
|
|
||||||
if (browser) {
|
|
||||||
const set_reduced_motion = (event: MediaQueryListEvent) => {
|
|
||||||
set(event.matches);
|
|
||||||
};
|
|
||||||
const media_query_list = window.matchMedia(reduced_motion_query);
|
|
||||||
media_query_list.addEventListener('change', set_reduced_motion);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
media_query_list.removeEventListener('change', set_reduced_motion);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user