diff --git a/src/routes/practice/(practice)/+layout.svelte b/src/routes/practice/(practice)/+layout.svelte
new file mode 100644
index 0000000..677a8c2
--- /dev/null
+++ b/src/routes/practice/(practice)/+layout.svelte
@@ -0,0 +1,90 @@
+
+
+
+
+ JazzyDalpeng / Jazz Guitar Practice
+
+
+
+
+
+
diff --git a/src/routes/practice/(practice)/+layout.ts b/src/routes/practice/(practice)/+layout.ts
new file mode 100644
index 0000000..521d52e
--- /dev/null
+++ b/src/routes/practice/(practice)/+layout.ts
@@ -0,0 +1,33 @@
+import { base } from '$app/paths';
+import { redirect } from '@sveltejs/kit';
+import { routes, type PracticeRoute, type PracticeRouteCategory } from '../data';
+import type { LayoutLoad } from './$types';
+
+export const load: LayoutLoad = (data) => {
+ const { category, slug } = data.params;
+
+ if (category !== 'core' && category !== 'custom') {
+ redirect(303, `${base}/practice`);
+ }
+
+ const currentCategoryRoutes = routes[category];
+ const currentPageIndex = currentCategoryRoutes.findIndex((route) => route.slug === slug);
+ if (currentPageIndex === -1) {
+ redirect(303, `${base}/practice/${category}/${routes[category][0].slug}`);
+ }
+
+ const pages: {
+ previous?: PracticeRoute;
+ current: PracticeRoute;
+ next?: PracticeRoute;
+ } = {
+ previous: currentPageIndex - 1 >= 0 ? currentCategoryRoutes[currentPageIndex - 1] : undefined,
+ current: currentCategoryRoutes[currentPageIndex],
+ next:
+ currentPageIndex < currentCategoryRoutes.length - 1
+ ? currentCategoryRoutes[currentPageIndex + 1]
+ : undefined
+ };
+
+ return { routes, category: category as PracticeRouteCategory, pages };
+};
diff --git a/src/routes/practice/(practice)/[category]/+page.svelte b/src/routes/practice/(practice)/[category]/+page.svelte
new file mode 100644
index 0000000..8c76075
--- /dev/null
+++ b/src/routes/practice/(practice)/[category]/+page.svelte
@@ -0,0 +1 @@
+category
\ No newline at end of file
diff --git a/src/routes/practice/(practice)/[category]/[slug]/+page.svelte b/src/routes/practice/(practice)/[category]/[slug]/+page.svelte
new file mode 100644
index 0000000..9e8d233
--- /dev/null
+++ b/src/routes/practice/(practice)/[category]/[slug]/+page.svelte
@@ -0,0 +1 @@
+slug
diff --git a/src/routes/practice/+page.svelte b/src/routes/practice/+page.svelte
index 45b983b..d6adf83 100644
--- a/src/routes/practice/+page.svelte
+++ b/src/routes/practice/+page.svelte
@@ -1 +1,5 @@
-hi
+
+
+????
diff --git a/src/routes/practice/data.ts b/src/routes/practice/data.ts
new file mode 100644
index 0000000..ed9b723
--- /dev/null
+++ b/src/routes/practice/data.ts
@@ -0,0 +1,212 @@
+import type { Practice } from '$/lib/practice/types';
+import { TUNE } from '$/utils/music/pitch';
+
+export interface PracticeRoute {
+ title: string;
+ slug: string;
+ practice: Practice;
+}
+
+export type PracticeRouteCategory = 'core' | 'custom';
+export interface PracticeRoutes extends Record {}
+
+export const routes: PracticeRoutes = {
+ core: [
+ {
+ title: 'Major Scale',
+ slug: 'major-scale',
+ practice: {
+ tempo: {
+ bpm: 120,
+ beatPerBar: 4,
+ signatureUnit: 4
+ },
+ guitar: {
+ tuning: TUNE.standard
+ },
+ scores: [
+ {
+ positions: [
+ { line: 6, fret: 8 },
+ { line: 6, fret: 10 },
+ { line: 5, fret: 7 },
+ { line: 5, fret: 8 },
+ { line: 5, fret: 10 },
+ { line: 4, fret: 7 },
+ { line: 4, fret: 9 },
+ { line: 4, fret: 10 },
+ { line: 3, fret: 7 },
+ { line: 3, fret: 9 },
+ { line: 3, fret: 10 },
+ { line: 2, fret: 8 },
+ { line: 2, fret: 10 },
+ { line: 1, fret: 7 },
+ { line: 1, fret: 8 },
+ { line: 1, fret: 10 },
+ { line: 6, fret: 7 }
+ ],
+ notes: [
+ { position: 0, time: { start: 0, duration: 0.0625 } },
+ { position: 1, time: { start: 0.0625, duration: 0.0625 } },
+ { position: 2, time: { start: 0.125, duration: 0.0625 } },
+ { position: 3, time: { start: 0.1875, duration: 0.0625 } },
+ { position: 4, time: { start: 0.25, duration: 0.0625 } },
+ { position: 5, time: { start: 0.3125, duration: 0.0625 } },
+ { position: 6, time: { start: 0.375, duration: 0.0625 } },
+ { position: 7, time: { start: 0.4375, duration: 0.0625 } },
+ { position: 8, time: { start: 0.5, duration: 0.0625 } },
+ { position: 9, time: { start: 0.5625, duration: 0.0625 } },
+ { position: 10, time: { start: 0.625, duration: 0.0625 } },
+ { position: 11, time: { start: 0.6875, duration: 0.0625 } },
+ { position: 12, time: { start: 0.75, duration: 0.0625 } },
+ { position: 13, time: { start: 0.8125, duration: 0.0625 } },
+ { position: 14, time: { start: 0.875, duration: 0.0625 } },
+ { position: 15, time: { start: 0.9375, duration: 0.0625 } },
+ { position: 14, time: { start: 1, duration: 0.0625 } },
+ { position: 13, time: { start: 1.0625, duration: 0.0625 } },
+ { position: 12, time: { start: 1.125, duration: 0.0625 } },
+ { position: 11, time: { start: 1.1875, duration: 0.0625 } },
+ { position: 10, time: { start: 1.25, duration: 0.0625 } },
+ { position: 9, time: { start: 1.3125, duration: 0.0625 } },
+ { position: 8, time: { start: 1.375, duration: 0.0625 } },
+ { position: 7, time: { start: 1.4375, duration: 0.0625 } },
+ { position: 6, time: { start: 1.5, duration: 0.0625 } },
+ { position: 5, time: { start: 1.5625, duration: 0.0625 } },
+ { position: 4, time: { start: 1.625, duration: 0.0625 } },
+ { position: 3, time: { start: 1.6875, duration: 0.0625 } },
+ { position: 2, time: { start: 1.75, duration: 0.0625 } },
+ { position: 1, time: { start: 1.8125, duration: 0.0625 } },
+ { position: 0, time: { start: 1.875, duration: 0.0625 } },
+ { position: 16, time: { start: 1.9375, duration: 0.0625 } },
+ { position: 0, time: { start: 2, duration: 0.0625 } }
+ ],
+ boards: [
+ {
+ title: 'C line 1',
+ fingers: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
+ time: { start: 0 }
+ },
+ {
+ title: 'C line 2',
+ fingers: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
+ time: { start: 15 / 16 }
+ }
+ ],
+ fretRange: {
+ start: 5,
+ end: 11,
+ visibility: 'all'
+ }
+ }
+ ]
+ }
+ },
+ {
+ title: 'Rhythm Test',
+ slug: 'rhythm-test',
+ practice: {
+ tempo: {
+ bpm: 120,
+ beatPerBar: 4,
+ signatureUnit: 4
+ },
+ guitar: {
+ tuning: TUNE.standard
+ },
+ scores: [
+ {
+ positions: [
+ { line: 5, fret: 3 },
+ { line: 6, fret: 3 },
+ { line: 1, fret: 'open' },
+ { line: 2, fret: 1 },
+ { line: 3, fret: 'open' }
+ ],
+ notes: [
+ { position: 0, time: { start: 0, duration: 1 / 4 } },
+ { position: 2, time: { start: 1 / 4, duration: 1 / 4 } },
+ { position: 3, time: { start: 1 / 4, duration: 1 / 4 } },
+ { position: 4, time: { start: 1 / 4, duration: 1 / 4 } },
+ { position: 1, time: { start: 2 / 4, duration: 1 / 4 } },
+ { position: 2, time: { start: 3 / 4, duration: 1 / 4 } },
+ { position: 3, time: { start: 3 / 4, duration: 1 / 4 } },
+ { position: 4, time: { start: 3 / 4, duration: 1 / 4 } },
+ { position: 0, time: { start: 4 / 4, duration: 1 / 4 } },
+ { position: 2, time: { start: 5 / 4, duration: 1 / 4 } },
+ { position: 3, time: { start: 5 / 4, duration: 1 / 4 } },
+ { position: 4, time: { start: 5 / 4, duration: 1 / 4 } },
+ { position: 1, time: { start: 6 / 4, duration: 1 / 4 } },
+ { position: 2, time: { start: 7 / 4, duration: 1 / 4 } },
+ { position: 3, time: { start: 7 / 4, duration: 1 / 4 } },
+ { position: 4, time: { start: 7 / 4, duration: 1 / 4 } },
+ { position: 0, time: { start: 8 / 4, duration: 1 / 4 } },
+ { position: 2, time: { start: 9 / 4, duration: 1 / 4 } },
+ { position: 3, time: { start: 9 / 4, duration: 1 / 4 } },
+ { position: 4, time: { start: 9 / 4, duration: 1 / 4 } },
+ { position: 1, time: { start: 10 / 4, duration: 1 / 4 } },
+ { position: 2, time: { start: 11 / 4, duration: 1 / 4 } },
+ { position: 3, time: { start: 11 / 4, duration: 1 / 4 } },
+ { position: 4, time: { start: 11 / 4, duration: 1 / 4 } },
+ { position: 0, time: { start: 12 / 4, duration: 1 / 4 } },
+ { position: 2, time: { start: 13 / 4, duration: 1 / 4 } },
+ { position: 3, time: { start: 13 / 4, duration: 1 / 4 } },
+ { position: 4, time: { start: 13 / 4, duration: 1 / 4 } },
+ { position: 1, time: { start: 14 / 4, duration: 1 / 4 } },
+ { position: 2, time: { start: 15 / 4, duration: 1 / 4 } },
+ { position: 3, time: { start: 15 / 4, duration: 1 / 4 } },
+ { position: 4, time: { start: 15 / 4, duration: 1 / 4 } }
+ ],
+ boards: [
+ {
+ title: 'C line 1',
+ fingers: [0, 2, 3, 4],
+ time: { start: 0 }
+ },
+ {
+ title: 'C line 1',
+ fingers: [1, 2, 3, 4],
+ time: { start: 1 / 2 }
+ },
+ {
+ title: 'C line 1',
+ fingers: [0, 2, 3, 4],
+ time: { start: 2 / 2 }
+ },
+ {
+ title: 'C line 1',
+ fingers: [1, 2, 3, 4],
+ time: { start: 3 / 2 }
+ },
+ {
+ title: 'C line 1',
+ fingers: [0, 2, 3, 4],
+ time: { start: 4 / 2 }
+ },
+ {
+ title: 'C line 1',
+ fingers: [1, 2, 3, 4],
+ time: { start: 5 / 2 }
+ },
+ {
+ title: 'C line 1',
+ fingers: [0, 2, 3, 4],
+ time: { start: 6 / 2 }
+ },
+ {
+ title: 'C line 1',
+ fingers: [1, 2, 3, 4],
+ time: { start: 7 / 2 }
+ }
+ ],
+ fretRange: {
+ start: 0,
+ end: 12,
+ visibility: 'all'
+ }
+ }
+ ]
+ }
+ }
+ ],
+ custom: []
+};
diff --git a/src/utils/hooks/click-outside.ts b/src/utils/hooks/click-outside.ts
new file mode 100644
index 0000000..dea3527
--- /dev/null
+++ b/src/utils/hooks/click-outside.ts
@@ -0,0 +1,25 @@
+import type { Action, ActionReturn } from 'svelte/action';
+
+export const clickoutside: Action<
+ Element,
+ unknown,
+ { 'on:clickoutside'?: (event: CustomEvent) => any }
+> = (node) => {
+ const handleClick = (event: MouseEvent) => {
+ const target = event.target as HTMLElement;
+ if (!event.target) {
+ return;
+ }
+ if (node && !node.contains(target) && !event.defaultPrevented) {
+ node.dispatchEvent(new CustomEvent('clickoutside', { detail: CustomEvent }));
+ }
+ };
+
+ document.addEventListener('click', handleClick, true);
+
+ return {
+ destroy() {
+ document.removeEventListener('click', handleClick, true);
+ }
+ };
+};