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
21 changes: 17 additions & 4 deletions src/app/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {Redirect, Route, Switch} from 'wouter';

import {AppLayout, PageState} from './components/AppLayout';
import {AppLayout, HackweekLoader, PageState} from './components/AppLayout';
import {GoogleIcon} from './components/GoogleIcon';
import {AdminAnalyticsPage} from './routes/AdminAnalyticsPage';
import {AdminPage} from './routes/AdminPage';
Expand All @@ -17,7 +17,13 @@ export function App() {
const session = useSession();

if (session.status === 'loading') {
return <AuthState title="Loading Hackweek" detail="checking your Sentry account…" />;
return (
<AuthState
title="Loading Hackweek"
detail="checking your Sentry account…"
loading
/>
);
}
if (session.status === 'unauthenticated') {
const detail =
Expand Down Expand Up @@ -101,14 +107,21 @@ function AuthState({
title,
detail,
login = false,
loading = false,
}: {
title: string;
detail: string;
login?: boolean;
loading?: boolean;
}) {
return (
<main className="authShell">
<section className="authState" aria-live="polite">
<main className={`authShell${loading ? ' authShell--loading' : ''}`}>
<section
className={`authState${loading ? ' authState--loading' : ''}`}
aria-busy={loading || undefined}
aria-live="polite"
>
{loading && <HackweekLoader />}
<p className="kicker">Sentry #Hackweek</p>
<h1>{title}</h1>
<p>{detail}</p>
Expand Down
30 changes: 28 additions & 2 deletions src/app/components/AppLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,17 +92,41 @@ export function AppLayout({
);
}

export function HackweekLoader() {
return (
<div className="hackweekLoader" aria-hidden="true">
<div className="hackweekLoaderTiles">
<span />
<span />
<span />
<span />
<span />
</div>
<div className="hackweekLoaderTrack">
<span />
</div>
</div>
);
}

export function PageState({
title,
detail,
tone = 'neutral',
loading = false,
}: {
title: string;
detail: string;
tone?: 'neutral' | 'error' | 'forbidden';
loading?: boolean;
}) {
return (
<section className={`pageState pageState--${tone}`} aria-live="polite">
<section
className={`pageState pageState--${tone}${loading ? ' pageState--loading' : ''}`}
aria-busy={loading || undefined}
aria-live="polite"
>
{loading && <HackweekLoader />}
<p className="kicker">Hackweek</p>
<h1>{title}</h1>
<p>{detail}</p>
Expand All @@ -121,7 +145,9 @@ export function QueryState({
children: ReactNode;
}) {
if (loading) {
return <PageState title="Loading Hackweek" detail="Loading Hackweek records…" />;
return (
<PageState title="Loading Hackweek" detail="Loading Hackweek records…" loading />
);
}
if (error) {
return (
Expand Down
140 changes: 140 additions & 0 deletions src/app/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -1496,6 +1496,13 @@ main {
min-height: 100vh;
background: var(--soft);
}
.authShell--loading {
overflow: hidden;
background:
radial-gradient(circle at 20% 15%, rgba(255, 112, 188, 0.14), transparent 28rem),
radial-gradient(circle at 82% 78%, rgba(117, 83, 255, 0.14), transparent 32rem),
var(--soft);
}
.pageState,
.authState {
position: relative;
Expand Down Expand Up @@ -1530,6 +1537,97 @@ main {
color: var(--muted);
line-height: 1.7;
}
.authState--loading {
animation: loadingStateIn 400ms ease-out both;
}
.pageState--loading {
animation: loadingStateIn 400ms ease-out 200ms both;
}
.authState--loading {
overflow: hidden;
border-color: rgba(117, 83, 255, 0.18);
box-shadow: 0 24px 70px rgba(78, 42, 154, 0.1);
}
.authState--loading::before {
position: absolute;
top: -8rem;
left: 50%;
width: 24rem;
height: 14rem;
content: '';
background: radial-gradient(circle, rgba(117, 83, 255, 0.1), transparent 70%);
transform: translateX(-50%);
pointer-events: none;
}
.hackweekLoader {
position: relative;
width: min(13rem, 72vw);
margin: 0 auto clamp(2rem, 5vw, 3rem);
}
.hackweekLoaderTiles {
display: flex;
height: 3.4rem;
gap: 0.42rem;
align-items: flex-end;
justify-content: center;
}
.hackweekLoaderTiles span {
--loader-color: var(--blurple);
--loader-tilt: -3deg;
display: block;
width: clamp(1.35rem, 5vw, 1.8rem);
height: clamp(1.75rem, 6vw, 2.3rem);
border: 2px solid rgba(29, 17, 39, 0.12);
border-radius: 0.4rem;
background: var(--loader-color);
box-shadow: 0 0.3rem 0 rgba(29, 17, 39, 0.1);
animation: loadingTile 1.35s cubic-bezier(0.45, 0, 0.2, 1) infinite both;
}
.hackweekLoaderTiles span:nth-child(1) {
--loader-color: var(--dark-blurple);
--loader-tilt: -7deg;
animation-delay: -1.08s;
}
.hackweekLoaderTiles span:nth-child(2) {
--loader-color: var(--pink);
--loader-tilt: 3deg;
animation-delay: -0.81s;
}
.hackweekLoaderTiles span:nth-child(3) {
--loader-color: var(--green);
--loader-tilt: -2deg;
animation-delay: -0.54s;
}
.hackweekLoaderTiles span:nth-child(4) {
--loader-color: var(--yellow);
--loader-tilt: 6deg;
animation-delay: -0.27s;
}
.hackweekLoaderTiles span:nth-child(5) {
--loader-tilt: -4deg;
}
.hackweekLoaderTrack {
position: relative;
overflow: hidden;
height: 0.22rem;
margin-top: 0.7rem;
border-radius: 999px;
background: var(--lavender);
}
.hackweekLoaderTrack span {
position: absolute;
inset: 0;
border-radius: inherit;
background: linear-gradient(
90deg,
transparent 0%,
var(--pink) 28%,
var(--blurple) 50%,
var(--green) 72%,
transparent 100%
);
animation: loadingTrack 1.8s ease-in-out infinite;
}
.googleLogin {
display: inline-flex;
gap: 0.65rem;
Expand Down Expand Up @@ -2474,6 +2572,48 @@ kbd {
}
}

@keyframes loadingStateIn {
from {
opacity: 0;
transform: translateY(0.6rem) scale(0.985);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}

@keyframes loadingTile {
0%,
100% {
opacity: 0.5;
transform: translateY(0) rotate(var(--loader-tilt)) scale(0.9);
}
38% {
opacity: 1;
transform: translateY(-0.72rem) rotate(var(--loader-tilt)) scale(1);
}
68% {
opacity: 0.72;
transform: translateY(0) rotate(var(--loader-tilt)) scale(0.94);
}
}

@keyframes loadingTrack {
0% {
opacity: 0;
transform: translateX(-100%);
}
25%,
70% {
opacity: 1;
}
100% {
opacity: 0;
transform: translateX(100%);
}
}

@media (max-width: 900px) {
.archiveHero,
.projectsHero,
Expand Down
21 changes: 21 additions & 0 deletions test/app/LoadingState.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {render, screen} from '@testing-library/react';
import {describe, expect, it} from 'vitest';

import {PageState} from '../../src/app/components/AppLayout';

describe('Hackweek loading state', () => {
it('shows the branded loading indicator and exposes its busy state', () => {
const {container} = render(
<PageState title="Loading Hackweek" detail="Loading Hackweek records…" loading />,
);

const state = screen
.getByRole('heading', {name: 'Loading Hackweek'})
.closest('section');
expect(state?.getAttribute('aria-busy')).toBe('true');
expect(container.querySelectorAll('.hackweekLoaderTiles span')).toHaveLength(5);
expect(container.querySelector('.hackweekLoader')?.getAttribute('aria-hidden')).toBe(
'true',
);
});
});
Loading