Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/app.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
<meta name="description" content="Personal website for AJ Bienz" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
document.documentElement.classList.add("js");
</script>
%sveltekit.head%
</head>
<body style="margin: 0">
Expand Down
6 changes: 5 additions & 1 deletion src/lib/components/family.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
<div class="flex justify-center sm:justify-start">
<img
src="family.webp"
loading="lazy"
loading="eager"
fetchpriority="high"
decoding="async"
width="2726"
height="2726"
class="rounded-full sm:ml-24 w-56 ring-2 ring-black"
alt="me, my wife, dog, and two cats"
/>
Expand Down
172 changes: 128 additions & 44 deletions src/routes/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,76 +1,160 @@
<script lang="ts">
import { onMount } from "svelte";

import intro from "$lib/markdown/intro.md";
import family from "$lib/markdown/family.md";

let cursorHidden = $state(false);
setInterval(() => {
cursorHidden = !cursorHidden;
}, 400);

let commands = $state([
const commands = [
{
text: "whoami",
completed: false,
ouputComponent: intro,
},
{
text: "cat family.png",
completed: false,
ouputComponent: family,
},
]);
];

let useAnimation = $state(false);
let completedCommandCount = $state(commands.length);

let commandIndex = $state(0);
let commandIndex = $state(commands.length);
let currentCommandIndex = $state(0);
let currentCommand = $derived(commands[commandIndex]);

(() => {
let intervalHandle: ReturnType<typeof setInterval>;
onMount(() => {
const reduceMotion = window.matchMedia(
"(prefers-reduced-motion: reduce)",
).matches;

if (reduceMotion) {
return;
}

let typingInterval: ReturnType<typeof setInterval>;
let commandDelay: ReturnType<typeof setTimeout>;

useAnimation = true;
completedCommandCount = 0;
commandIndex = 0;
currentCommandIndex = 0;

const intervalHandler = () => {
if (currentCommand && currentCommandIndex < currentCommand.text.length) {
currentCommandIndex++;
} else if (currentCommand) {
clearInterval(intervalHandle);
setTimeout(() => {
currentCommand.completed = true;
clearInterval(typingInterval);
commandDelay = setTimeout(() => {
completedCommandCount++;
currentCommandIndex = 0;
commandIndex++;
commands = commands;
if (currentCommand) {
intervalHandle = setInterval(intervalHandler, 200);
typingInterval = setInterval(intervalHandler, 200);
}
}, 1200);
}
};
intervalHandle = setInterval(intervalHandler, 200);
})();

typingInterval = setInterval(intervalHandler, 200);

return () => {
clearInterval(typingInterval);
clearTimeout(commandDelay);
};
});
</script>

{#each commands as command}
<div class:hidden={!command.completed}>
<p class="my-0">
<span class="font-semibold">aj@rva $</span>
{command.text}
</p>
<div class="my-16 mx-8 space-y-10">
<command.ouputComponent />
<svelte:head>
<link
rel="preload"
as="image"
href="/family.webp"
type="image/webp"
fetchpriority="high"
/>
</svelte:head>

<section
class="home-content"
class:sr-only={useAnimation}
aria-label="Introduction"
data-testid="home-content"
>
{#each commands as command}
<div>
<p class="my-0" aria-hidden="true">
<span class="font-semibold">aj@rva $</span>
{command.text}
</p>
<div class="my-16 mx-8 space-y-10">
<command.ouputComponent />
</div>
</div>
{/each}
</section>

{#if useAnimation}
<div aria-hidden="true">
{#each commands.slice(0, completedCommandCount) as command}
<div>
<p class="my-0">
<span class="font-semibold">aj@rva $</span>
{command.text}
</p>
<div class="my-16 mx-8 space-y-10">
<command.ouputComponent />
</div>
</div>
{/each}

{#if currentCommand}
<p class="my-0">
<span class="font-semibold">aj@rva $</span>
{currentCommand.text.slice(0, currentCommandIndex)}<span
class="cursor"
aria-hidden="true">_</span
>
</p>
<div class="invisible my-16 ml-8 space-y-10">
<currentCommand.ouputComponent />
</div>
{:else}
<p class="my-0">
<span class="font-semibold">aj@rva $</span>
<span class="cursor" aria-hidden="true">_</span>
</p>
{/if}
</div>
{/each}

{#if currentCommand}
<p class="my-0">
<span class="font-semibold">aj@rva $</span>
{currentCommand.text.slice(0, currentCommandIndex)}<span
class:hidden={cursorHidden}>_</span
>
</p>
<div class="invisible my-16 ml-8 space-y-10">
<currentCommand.ouputComponent />
</div>
{:else}
<p class="my-0">
<span class="font-semibold">aj@rva $</span>
<span class:hidden={cursorHidden}>_</span>
</p>
{/if}

<style>
@media (prefers-reduced-motion: no-preference) {
:global(html.js) .home-content {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
}

.cursor {
animation: blink 800ms steps(1, end) infinite;
}

@keyframes blink {
50% {
opacity: 0;
}
}

@media (prefers-reduced-motion: reduce) {
.cursor {
animation: none;
}
}
</style>
34 changes: 25 additions & 9 deletions tests/hi.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,44 @@ test.beforeEach(async ({ page }) => {
});

test("first paragraph shows up", async ({ page }) => {
const target = page
.getByRole("paragraph")
.filter({ hasText: "Hey!" })
.first();
const target = page.locator("p:visible", { hasText: "Hey!" }).first();
await expect(target).toBeVisible();
});

test("second paragraph shows up", async ({ page }) => {
const target = page
.getByRole("paragraph")
.filter({ hasText: "This is my little corner of the internet" })
.locator("p:visible", {
hasText: "This is my little corner of the internet",
})
.first();
await expect(target).toBeVisible();
});

test("photo shows up", async ({ page }) => {
const text = page
.getByRole("paragraph")
.filter({ hasText: "picture of me, my wife, and all my pets" })
.locator("p:visible", {
hasText: "picture of me, my wife, and all my pets",
})
.first();
const image = page
.locator('img[alt="me, my wife, dog, and two cats"]:visible')
.first();
const image = page.getByAltText("me, my wife, dog, and two cats").first();

await expect(text).toBeVisible({ timeout: 10000 });
await expect(image).toBeVisible();
});

test.describe("without JavaScript", () => {
test.use({ javaScriptEnabled: false });

test("home content is visible", async ({ page }) => {
await expect(page.getByTestId("home-content")).toBeVisible();
await expect(page.getByText("Hey!").first()).toBeVisible();
await expect(
page.getByText("This is my little corner of the internet").first(),
).toBeVisible();
await expect(
page.getByAltText("me, my wife, dog, and two cats"),
).toBeVisible();
});
});
Loading