create-bailaya-site 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/index.mjs +198 -0
  2. package/package.json +20 -4
  3. package/templates/next/.env.example +5 -0
  4. package/templates/next/_gitignore +8 -0
  5. package/templates/next/next.config.ts +5 -0
  6. package/templates/next/package.json +26 -0
  7. package/templates/next/postcss.config.mjs +1 -0
  8. package/templates/next/src/app/about/page.tsx +5 -0
  9. package/templates/next/src/app/classes/[style]/page.tsx +6 -0
  10. package/templates/next/src/app/classes/page.tsx +5 -0
  11. package/templates/next/src/app/contact/page.tsx +5 -0
  12. package/templates/next/src/app/events/page.tsx +5 -0
  13. package/templates/next/src/app/instructors/page.tsx +5 -0
  14. package/templates/next/src/app/layout.tsx +48 -0
  15. package/templates/next/src/app/not-found.tsx +5 -0
  16. package/templates/next/src/app/page.tsx +5 -0
  17. package/templates/next/src/app/private-lessons/page.tsx +5 -0
  18. package/templates/next/src/client.tsx +11 -0
  19. package/templates/next/src/lib/config.ts +13 -0
  20. package/templates/next/src/lib/router.tsx +14 -0
  21. package/templates/next/tsconfig.json +21 -0
  22. package/templates/react/.env.example +5 -0
  23. package/templates/react/_gitignore +5 -0
  24. package/templates/react/index.html +15 -0
  25. package/templates/react/package.json +29 -0
  26. package/templates/react/src/App.tsx +38 -0
  27. package/templates/react/src/lib/config.ts +13 -0
  28. package/templates/react/src/lib/router.tsx +12 -0
  29. package/templates/react/src/main.tsx +16 -0
  30. package/templates/react/tsconfig.json +16 -0
  31. package/templates/react/vite.config.ts +11 -0
  32. package/templates/shared/README.md +60 -0
  33. package/templates/shared/src/components/app-shell.tsx +71 -0
  34. package/templates/shared/src/components/site-footer.tsx +70 -0
  35. package/templates/shared/src/components/site-header.tsx +94 -0
  36. package/templates/shared/src/components/ui.tsx +87 -0
  37. package/templates/shared/src/components/visit-us.tsx +69 -0
  38. package/templates/shared/src/lib/i18n.ts +292 -0
  39. package/templates/shared/src/styles.css +66 -0
  40. package/templates/shared/src/views/classes.tsx +74 -0
  41. package/templates/shared/src/views/home.tsx +90 -0
  42. package/templates/shared/src/views/more.tsx +114 -0
  43. package/templates/shared/src/views/not-found.tsx +14 -0
  44. package/README.md +0 -3
package/index.mjs ADDED
@@ -0,0 +1,198 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * create-bailaya-site — scaffold a dance studio website powered by BailaYa.
4
+ *
5
+ * npx create-bailaya-site my-studio
6
+ * npx create-bailaya-site my-studio --next --studio-id <id> --locale es
7
+ *
8
+ * Zero dependencies on purpose: it runs straight from `npx` in a second.
9
+ */
10
+ import { spawnSync } from "node:child_process";
11
+ import { cpSync, existsSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
12
+ import { basename, dirname, join, resolve } from "node:path";
13
+ import { createInterface } from "node:readline/promises";
14
+ import { fileURLToPath } from "node:url";
15
+
16
+ const here = dirname(fileURLToPath(import.meta.url));
17
+ const pkg = JSON.parse(readFileSync(join(here, "package.json"), "utf8"));
18
+ const LOCALES = ["en", "es", "fr", "de", "ru", "ka"];
19
+ const FRAMEWORKS = {
20
+ react: { label: "React (Vite)", envFile: ".env", envPrefix: "VITE_", dev: "http://localhost:5173" },
21
+ next: { label: "Next.js", envFile: ".env.local", envPrefix: "NEXT_PUBLIC_", dev: "http://localhost:3000" },
22
+ };
23
+
24
+ const color = (code) => (s) => (process.stdout.isTTY ? `\x1b[${code}m${s}\x1b[0m` : s);
25
+ const bold = color(1);
26
+ const dim = color(2);
27
+ const pink = color(35);
28
+ const red = color(31);
29
+ const green = color(32);
30
+
31
+ function parseArgs(argv) {
32
+ const opts = { dir: undefined, framework: undefined, studioId: undefined, locale: undefined, pm: undefined, install: true, yes: false };
33
+ for (let i = 0; i < argv.length; i++) {
34
+ const arg = argv[i];
35
+ const value = () => {
36
+ const v = arg.includes("=") ? arg.split("=").slice(1).join("=") : argv[++i];
37
+ if (v === undefined) fail(`${arg} needs a value`);
38
+ return v;
39
+ };
40
+ if (arg === "-h" || arg === "--help") help();
41
+ else if (arg === "-v" || arg === "--version") (console.log(pkg.version), process.exit(0));
42
+ else if (arg === "--next") opts.framework = "next";
43
+ else if (arg === "--react") opts.framework = "react";
44
+ else if (arg.startsWith("--framework")) opts.framework = value();
45
+ else if (arg.startsWith("--studio-id")) opts.studioId = value();
46
+ else if (arg.startsWith("--locale")) opts.locale = value();
47
+ else if (arg.startsWith("--use-")) opts.pm = arg.slice(6);
48
+ else if (arg === "--no-install") opts.install = false;
49
+ else if (arg === "-y" || arg === "--yes") opts.yes = true;
50
+ else if (arg.startsWith("-")) fail(`Unknown option ${arg}. Run with --help to see the options.`);
51
+ else if (!opts.dir) opts.dir = arg;
52
+ else fail(`Unexpected argument ${arg}`);
53
+ }
54
+ if (opts.framework && !FRAMEWORKS[opts.framework]) fail(`--framework must be "react" or "next"`);
55
+ return opts;
56
+ }
57
+
58
+ function help() {
59
+ console.log(`
60
+ ${bold("create-bailaya-site")} ${dim(`v${pkg.version}`)}
61
+ Scaffold a dance studio website that loads everything from BailaYa.
62
+
63
+ ${bold("Usage")}
64
+ npx create-bailaya-site [directory] [options]
65
+
66
+ ${bold("Options")}
67
+ --studio-id <id> Your BailaYa studio ID (asked for if omitted)
68
+ --locale <code> Site language: ${LOCALES.join(", ")} (default en)
69
+ --next Use Next.js instead of React (Vite)
70
+ --framework <name> react (default) or next
71
+ --use-npm | --use-pnpm | --use-yarn | --use-bun
72
+ Package manager (default: the one running this)
73
+ --no-install Skip installing dependencies
74
+ -y, --yes Accept defaults without prompting
75
+ -h, --help Show this help
76
+ `);
77
+ process.exit(0);
78
+ }
79
+
80
+ function fail(message) {
81
+ console.error(`\n${red("✖")} ${message}\n`);
82
+ process.exit(1);
83
+ }
84
+
85
+ function detectPackageManager() {
86
+ const agent = process.env.npm_config_user_agent ?? "";
87
+ for (const pm of ["pnpm", "yarn", "bun"]) if (agent.startsWith(pm)) return pm;
88
+ return "npm";
89
+ }
90
+
91
+ function toPackageName(dir) {
92
+ return (
93
+ basename(resolve(dir))
94
+ .toLowerCase()
95
+ .replace(/[^a-z0-9-~._]+/g, "-")
96
+ .replace(/^[-._]+|[-._]+$/g, "") || "bailaya-studio-site"
97
+ );
98
+ }
99
+
100
+ /** Looks the studio up so the user knows straight away whether the ID is right. */
101
+ async function lookUpStudio(studioId) {
102
+ try {
103
+ const res = await fetch(`https://www.bailaya.com/api/public/studio/${encodeURIComponent(studioId)}/profile`, {
104
+ signal: AbortSignal.timeout(10_000),
105
+ });
106
+ if (res.status === 404) return { ok: false, reason: "not found" };
107
+ if (!res.ok) return { ok: null, reason: `BailaYa answered ${res.status}` };
108
+ const profile = await res.json();
109
+ return { ok: true, name: profile.name };
110
+ } catch (err) {
111
+ return { ok: null, reason: err?.name === "TimeoutError" ? "timed out" : "could not reach BailaYa" };
112
+ }
113
+ }
114
+
115
+ async function main() {
116
+ const opts = parseArgs(process.argv.slice(2));
117
+ const interactive = process.stdin.isTTY && !opts.yes;
118
+ const rl = interactive ? createInterface({ input: process.stdin, output: process.stdout }) : null;
119
+ const ask = async (question, fallback) => {
120
+ if (!rl) return fallback;
121
+ const answer = (await rl.question(`${pink("?")} ${question}${fallback ? dim(` (${fallback})`) : ""} `)).trim();
122
+ return answer || fallback;
123
+ };
124
+
125
+ console.log(`\n${pink("💃")} ${bold("create-bailaya-site")} ${dim(`v${pkg.version}`)}\n`);
126
+
127
+ const dir = opts.dir ?? (await ask("Project directory:", "my-dance-studio"));
128
+ const target = resolve(dir);
129
+ if (existsSync(target) && readdirSync(target).length > 0) fail(`${dir} already exists and is not empty.`);
130
+
131
+ let framework = opts.framework;
132
+ if (!framework) {
133
+ const answer = await ask("Framework — react or next:", "react");
134
+ framework = answer.toLowerCase().startsWith("n") ? "next" : "react";
135
+ }
136
+ const fw = FRAMEWORKS[framework];
137
+
138
+ let studioId = opts.studioId ?? (await ask("BailaYa studio ID (leave blank to add later):", ""));
139
+ studioId = studioId?.trim() || "";
140
+ if (studioId) {
141
+ const found = await lookUpStudio(studioId);
142
+ if (found.ok) console.log(` ${green("✔")} Found ${bold(found.name)}`);
143
+ else if (found.ok === false) fail(`No BailaYa studio has the ID ${studioId}. Check it in your BailaYa dashboard.`);
144
+ else console.log(` ${dim(`Could not check the studio ID (${found.reason}); using it anyway.`)}`);
145
+ }
146
+
147
+ let locale = opts.locale ?? (await ask(`Site language — ${LOCALES.join("/")}:`, "en"));
148
+ if (!LOCALES.includes(locale.split("-")[0])) fail(`Unsupported locale ${locale}. Use one of ${LOCALES.join(", ")}.`);
149
+
150
+ rl?.close();
151
+
152
+ // Copy the shared site, then the framework layer on top of it.
153
+ cpSync(join(here, "templates", "shared"), target, { recursive: true });
154
+ cpSync(join(here, "templates", framework), target, { recursive: true });
155
+ renameSync(join(target, "_gitignore"), join(target, ".gitignore"));
156
+
157
+ const pkgPath = join(target, "package.json");
158
+ const sitePkg = JSON.parse(readFileSync(pkgPath, "utf8"));
159
+ sitePkg.name = toPackageName(dir);
160
+ writeFileSync(pkgPath, `${JSON.stringify(sitePkg, null, 2)}\n`);
161
+
162
+ const env = readFileSync(join(target, ".env.example"), "utf8")
163
+ .replace(/^(\w+_BAILAYA_STUDIO_ID)=.*$/m, `$1=${studioId}`)
164
+ .replace(/^(\w+_BAILAYA_LOCALE)=.*$/m, `$1=${locale}`);
165
+ writeFileSync(join(target, fw.envFile), env);
166
+
167
+ const pm = opts.pm ?? detectPackageManager();
168
+ const readmePath = join(target, "README.md");
169
+ writeFileSync(
170
+ readmePath,
171
+ readFileSync(readmePath, "utf8")
172
+ .replaceAll("{{FRAMEWORK}}", fw.label)
173
+ .replaceAll("{{ENV_FILE}}", fw.envFile)
174
+ .replaceAll("{{ENV_PREFIX}}", fw.envPrefix)
175
+ .replaceAll("{{DEV_URL}}", fw.dev)
176
+ .replaceAll("{{PM}}", pm),
177
+ );
178
+
179
+ console.log(`\n ${green("✔")} Created a ${fw.label} site in ${bold(target)}`);
180
+
181
+ if (opts.install) {
182
+ console.log(`\n Installing dependencies with ${pm}…\n`);
183
+ const result = spawnSync(pm, ["install"], { cwd: target, stdio: "inherit", shell: process.platform === "win32" });
184
+ if (result.status !== 0) console.log(`\n ${red("!")} ${pm} install failed — run it yourself in ${dir}.`);
185
+ }
186
+
187
+ const run = pm === "npm" ? "npm run" : pm;
188
+ console.log(`
189
+ ${bold("Next steps")}
190
+ cd ${dir}${opts.install ? "" : `\n ${pm} install`}${studioId ? "" : `\n ${dim(`# add your studio ID to ${fw.envFile}`)}`}
191
+ ${run} dev ${dim(`→ ${fw.dev}`)}
192
+
193
+ Everything about the studio — name, styles, timetable, instructors, prices,
194
+ locations — comes from BailaYa. Edit it there, not in the code.
195
+ `);
196
+ }
197
+
198
+ main().catch((err) => fail(err?.message ?? String(err)));
package/package.json CHANGED
@@ -1,6 +1,22 @@
1
1
  {
2
2
  "name": "create-bailaya-site",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Scaffold a dance studio website powered by BailaYa — React (Vite) or Next.js.",
5
+ "keywords": ["bailaya", "dance", "studio", "react", "nextjs", "vite", "create"],
6
+ "license": "ISC",
7
+ "type": "module",
8
+ "bin": {
9
+ "create-bailaya-site": "index.mjs"
10
+ },
11
+ "files": [
12
+ "index.mjs",
13
+ "templates"
14
+ ],
15
+ "engines": {
16
+ "node": ">=18.17"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/Infinity-Curve/create-bailaya-site.git"
21
+ }
22
+ }
@@ -0,0 +1,5 @@
1
+ # Your BailaYa studio ID (BailaYa dashboard). Required.
2
+ NEXT_PUBLIC_BAILAYA_STUDIO_ID=
3
+
4
+ # Site and widget language: en, es, fr, de, ru or ka (regional forms like es-MX work too).
5
+ NEXT_PUBLIC_BAILAYA_LOCALE=en
@@ -0,0 +1,8 @@
1
+ node_modules
2
+ .next
3
+ out
4
+ next-env.d.ts
5
+ .env
6
+ .env.local
7
+ *.log
8
+ *.tsbuildinfo
@@ -0,0 +1,5 @@
1
+ import type { NextConfig } from "next";
2
+
3
+ const nextConfig: NextConfig = {};
4
+
5
+ export default nextConfig;
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "bailaya-studio-site",
3
+ "private": true,
4
+ "version": "0.1.0",
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "typecheck": "tsc --noEmit"
10
+ },
11
+ "dependencies": {
12
+ "@bailaya/core": "^1.9.0",
13
+ "@bailaya/react": "^1.9.0",
14
+ "next": "^15.5.0",
15
+ "react": "^19.2.0",
16
+ "react-dom": "^19.2.0"
17
+ },
18
+ "devDependencies": {
19
+ "@tailwindcss/postcss": "^4.1.0",
20
+ "@types/node": "^22.0.0",
21
+ "@types/react": "^19.2.0",
22
+ "@types/react-dom": "^19.2.0",
23
+ "tailwindcss": "^4.1.0",
24
+ "typescript": "^5.9.0"
25
+ }
26
+ }
@@ -0,0 +1 @@
1
+ export default { plugins: { "@tailwindcss/postcss": {} } };
@@ -0,0 +1,5 @@
1
+ import { AboutPage } from "@/client";
2
+
3
+ export default function Page() {
4
+ return <AboutPage />;
5
+ }
@@ -0,0 +1,6 @@
1
+ import { ClassStylePage } from "@/client";
2
+
3
+ export default async function Page({ params }: { params: Promise<{ style: string }> }) {
4
+ const { style } = await params;
5
+ return <ClassStylePage slug={style} />;
6
+ }
@@ -0,0 +1,5 @@
1
+ import { ClassesPage } from "@/client";
2
+
3
+ export default function Page() {
4
+ return <ClassesPage />;
5
+ }
@@ -0,0 +1,5 @@
1
+ import { ContactPage } from "@/client";
2
+
3
+ export default function Page() {
4
+ return <ContactPage />;
5
+ }
@@ -0,0 +1,5 @@
1
+ import { EventsPage } from "@/client";
2
+
3
+ export default function Page() {
4
+ return <EventsPage />;
5
+ }
@@ -0,0 +1,5 @@
1
+ import { InstructorsPage } from "@/client";
2
+
3
+ export default function Page() {
4
+ return <InstructorsPage />;
5
+ }
@@ -0,0 +1,48 @@
1
+ import "@bailaya/react/styles.css";
2
+ import "../styles.css";
3
+
4
+ import { BailayaClient } from "@bailaya/core";
5
+ import type { Metadata } from "next";
6
+ import type { ReactNode } from "react";
7
+
8
+ import { AppShell } from "@/client";
9
+ import { bailayaBaseUrl, locale, studioId } from "@/lib/config";
10
+
11
+ const FONTS =
12
+ "https://fonts.googleapis.com/css2?family=DM+Sans:opsz,wght@9..40,400..700&family=Playfair+Display:wght@500;600;700&display=swap";
13
+
14
+ /** Title, description and icon come from the studio's BailaYa profile, rendered on the server. */
15
+ export async function generateMetadata(): Promise<Metadata> {
16
+ if (!studioId) return {};
17
+ try {
18
+ const profile = await new BailayaClient({
19
+ studioId,
20
+ ...(bailayaBaseUrl ? { baseUrl: bailayaBaseUrl } : {}),
21
+ }).getStudioProfile();
22
+ const lang = locale.split("-")[0];
23
+ const description = profile.description[locale] || profile.description[lang] || profile.description.en || "";
24
+ return {
25
+ title: { default: profile.name, template: `%s · ${profile.name}` },
26
+ description: description.replace(/<[^>]+>/g, " ").slice(0, 160),
27
+ icons: profile.logo ? { icon: profile.logo } : undefined,
28
+ };
29
+ } catch {
30
+ return {};
31
+ }
32
+ }
33
+
34
+ export default function RootLayout({ children }: { children: ReactNode }) {
35
+ return (
36
+ <html lang={locale}>
37
+ <head>
38
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
39
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="" />
40
+ {/* eslint-disable-next-line @next/next/no-page-custom-font */}
41
+ <link rel="stylesheet" href={FONTS} />
42
+ </head>
43
+ <body>
44
+ <AppShell>{children}</AppShell>
45
+ </body>
46
+ </html>
47
+ );
48
+ }
@@ -0,0 +1,5 @@
1
+ import { NotFoundPage } from "@/client";
2
+
3
+ export default function NotFound() {
4
+ return <NotFoundPage />;
5
+ }
@@ -0,0 +1,5 @@
1
+ import { HomePage } from "@/client";
2
+
3
+ export default function Page() {
4
+ return <HomePage />;
5
+ }
@@ -0,0 +1,5 @@
1
+ import { PrivateLessonsPage } from "@/client";
2
+
3
+ export default function Page() {
4
+ return <PrivateLessonsPage />;
5
+ }
@@ -0,0 +1,11 @@
1
+ "use client";
2
+
3
+ /**
4
+ * The site is built from client components that load studio data with the
5
+ * BailaYa hooks; this file is the boundary the server route files render.
6
+ */
7
+ export { AppShell } from "@/components/app-shell";
8
+ export { ClassesPage, ClassStylePage } from "@/views/classes";
9
+ export { HomePage } from "@/views/home";
10
+ export { AboutPage, ContactPage, EventsPage, InstructorsPage, PrivateLessonsPage } from "@/views/more";
11
+ export { NotFoundPage } from "@/views/not-found";
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Site configuration. The studio ID is the only required setting — everything
3
+ * else about the studio is loaded from BailaYa. Set these in `.env.local`.
4
+ */
5
+ export const envFile = ".env.local";
6
+ export const studioIdEnvName = "NEXT_PUBLIC_BAILAYA_STUDIO_ID";
7
+ export const studioId: string | undefined = process.env.NEXT_PUBLIC_BAILAYA_STUDIO_ID || undefined;
8
+
9
+ /** Language for the site and the BailaYa widgets: en, es, fr, de, ru or ka (e.g. "es-MX"). */
10
+ export const locale: string = process.env.NEXT_PUBLIC_BAILAYA_LOCALE || "en";
11
+
12
+ /** Only needed to point at a non-production BailaYa API. */
13
+ export const bailayaBaseUrl: string | undefined = process.env.NEXT_PUBLIC_BAILAYA_API_URL || undefined;
@@ -0,0 +1,14 @@
1
+ "use client";
2
+
3
+ import NextLink from "next/link";
4
+ import { usePathname as useNextPathname } from "next/navigation";
5
+ import type { ReactNode } from "react";
6
+
7
+ /** Router adapter, so the shared components work with Next.js and React Router alike. */
8
+ export function Link(props: { href: string; className?: string; onClick?: () => void; children: ReactNode }) {
9
+ return <NextLink {...props} />;
10
+ }
11
+
12
+ export function usePathname(): string {
13
+ return useNextPathname() ?? "/";
14
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "jsx": "preserve",
8
+ "strict": true,
9
+ "noEmit": true,
10
+ "skipLibCheck": true,
11
+ "isolatedModules": true,
12
+ "esModuleInterop": true,
13
+ "resolveJsonModule": true,
14
+ "incremental": true,
15
+ "allowJs": false,
16
+ "plugins": [{ "name": "next" }],
17
+ "paths": { "@/*": ["./src/*"] }
18
+ },
19
+ "include": ["next-env.d.ts", "src", ".next/types/**/*.ts"],
20
+ "exclude": ["node_modules"]
21
+ }
@@ -0,0 +1,5 @@
1
+ # Your BailaYa studio ID (BailaYa dashboard). Required.
2
+ VITE_BAILAYA_STUDIO_ID=
3
+
4
+ # Site and widget language: en, es, fr, de, ru or ka (regional forms like es-MX work too).
5
+ VITE_BAILAYA_LOCALE=en
@@ -0,0 +1,5 @@
1
+ node_modules
2
+ dist
3
+ .env
4
+ .env.local
5
+ *.log
@@ -0,0 +1,15 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title></title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=DM+Sans:opsz,wght@9..40,400..700&family=Playfair+Display:wght@500;600;700&display=swap" />
10
+ </head>
11
+ <body>
12
+ <div id="root"></div>
13
+ <script type="module" src="/src/main.tsx"></script>
14
+ </body>
15
+ </html>
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "bailaya-studio-site",
3
+ "private": true,
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc --noEmit && vite build",
9
+ "preview": "vite preview",
10
+ "typecheck": "tsc --noEmit"
11
+ },
12
+ "dependencies": {
13
+ "@bailaya/core": "^1.9.0",
14
+ "@bailaya/react": "^1.9.0",
15
+ "react": "^19.2.0",
16
+ "react-dom": "^19.2.0",
17
+ "react-router": "^7.9.0"
18
+ },
19
+ "devDependencies": {
20
+ "@tailwindcss/vite": "^4.1.0",
21
+ "@types/node": "^22.0.0",
22
+ "@types/react": "^19.2.0",
23
+ "@types/react-dom": "^19.2.0",
24
+ "@vitejs/plugin-react": "^5.0.0",
25
+ "tailwindcss": "^4.1.0",
26
+ "typescript": "^5.9.0",
27
+ "vite": "^7.1.0"
28
+ }
29
+ }
@@ -0,0 +1,38 @@
1
+ import { useEffect } from "react";
2
+ import { Route, Routes, useLocation, useParams } from "react-router";
3
+
4
+ import { AppShell } from "@/components/app-shell";
5
+ import { ClassesPage, ClassStylePage } from "@/views/classes";
6
+ import { HomePage } from "@/views/home";
7
+ import { AboutPage, ContactPage, EventsPage, InstructorsPage, PrivateLessonsPage } from "@/views/more";
8
+ import { NotFoundPage } from "@/views/not-found";
9
+
10
+ function ClassStyleRoute() {
11
+ const { style = "" } = useParams();
12
+ return <ClassStylePage slug={style} />;
13
+ }
14
+
15
+ function ScrollToTop() {
16
+ const { pathname } = useLocation();
17
+ useEffect(() => window.scrollTo(0, 0), [pathname]);
18
+ return null;
19
+ }
20
+
21
+ export function App() {
22
+ return (
23
+ <AppShell>
24
+ <ScrollToTop />
25
+ <Routes>
26
+ <Route path="/" element={<HomePage />} />
27
+ <Route path="/classes" element={<ClassesPage />} />
28
+ <Route path="/classes/:style" element={<ClassStyleRoute />} />
29
+ <Route path="/events" element={<EventsPage />} />
30
+ <Route path="/private-lessons" element={<PrivateLessonsPage />} />
31
+ <Route path="/instructors" element={<InstructorsPage />} />
32
+ <Route path="/about" element={<AboutPage />} />
33
+ <Route path="/contact" element={<ContactPage />} />
34
+ <Route path="*" element={<NotFoundPage />} />
35
+ </Routes>
36
+ </AppShell>
37
+ );
38
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Site configuration. The studio ID is the only required setting — everything
3
+ * else about the studio is loaded from BailaYa. Set these in `.env`.
4
+ */
5
+ export const envFile = ".env";
6
+ export const studioIdEnvName = "VITE_BAILAYA_STUDIO_ID";
7
+ export const studioId: string | undefined = import.meta.env.VITE_BAILAYA_STUDIO_ID || undefined;
8
+
9
+ /** Language for the site and the BailaYa widgets: en, es, fr, de, ru or ka (e.g. "es-MX"). */
10
+ export const locale: string = import.meta.env.VITE_BAILAYA_LOCALE || "en";
11
+
12
+ /** Only needed to point at a non-production BailaYa API. */
13
+ export const bailayaBaseUrl: string | undefined = import.meta.env.VITE_BAILAYA_API_URL || undefined;
@@ -0,0 +1,12 @@
1
+ import type { ReactNode } from "react";
2
+ import { Link as RouterLink, useLocation } from "react-router";
3
+
4
+ /** Router adapter, so the shared components work with React Router and Next.js alike. */
5
+ export function Link(props: { href: string; className?: string; onClick?: () => void; children: ReactNode }) {
6
+ const { href, ...rest } = props;
7
+ return <RouterLink to={href} {...rest} />;
8
+ }
9
+
10
+ export function usePathname(): string {
11
+ return useLocation().pathname;
12
+ }
@@ -0,0 +1,16 @@
1
+ import "@bailaya/react/styles.css";
2
+ import "./styles.css";
3
+
4
+ import { StrictMode } from "react";
5
+ import { createRoot } from "react-dom/client";
6
+ import { BrowserRouter } from "react-router";
7
+
8
+ import { App } from "./App";
9
+
10
+ createRoot(document.getElementById("root")!).render(
11
+ <StrictMode>
12
+ <BrowserRouter>
13
+ <App />
14
+ </BrowserRouter>
15
+ </StrictMode>,
16
+ );
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "jsx": "react-jsx",
8
+ "strict": true,
9
+ "noEmit": true,
10
+ "skipLibCheck": true,
11
+ "isolatedModules": true,
12
+ "types": ["vite/client", "node"],
13
+ "paths": { "@/*": ["./src/*"] }
14
+ },
15
+ "include": ["src", "vite.config.ts"]
16
+ }
@@ -0,0 +1,11 @@
1
+ import tailwindcss from "@tailwindcss/vite";
2
+ import react from "@vitejs/plugin-react";
3
+ import { fileURLToPath, URL } from "node:url";
4
+ import { defineConfig } from "vite";
5
+
6
+ export default defineConfig({
7
+ plugins: [react(), tailwindcss()],
8
+ resolve: {
9
+ alias: { "@": fileURLToPath(new URL("./src", import.meta.url)) },
10
+ },
11
+ });