Skip to content

Commit 3f8761d

Browse files
authored
Merge pull request #46 from GlebkaF/fast-create-preset
Fast create preset
2 parents 12b5286 + e686df9 commit 3f8761d

File tree

16 files changed

+2295
-52
lines changed

16 files changed

+2295
-52
lines changed

.github/workflows/nextjs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ jobs:
8686

8787
# Deployment job
8888
deploy:
89+
if: github.ref == 'refs/heads/main'
8990
environment:
9091
name: github-pages
9192
url: ${{ steps.deployment.outputs.page_url }}

TODO.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
## Мысли вслух
1212

1313
- Надо сделать контракты между этапами генерации более строгой типизации
14-
- Надо сделать рантайм проверку на тип Chain
1514

1615
## ЗАметки
1716

@@ -26,3 +25,11 @@
2625
Оставить тут коммент:
2726
https://www.youtube.com/watch?v=IBe7XMhCKWQ
2827
https://www.youtube.com/watch?v=hB4DUgAb0QA
28+
29+
На гитарист ру написать
30+
в группе в ФБ написать
31+
32+
## Новые страницы
33+
34+
- Разводящая артистов
35+
- хлебные крошки
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { Metadata } from "next";
2+
import { generationDb } from "../../../../../lib/jsondb";
3+
import artistsRaw from "../../../../../data/artists.json";
4+
import { PresetCreateForm } from "components/PresetCreateForm";
5+
6+
interface PresetCreatePageProps {
7+
params: {
8+
id: string;
9+
};
10+
}
11+
12+
// Исключаем этот API роут из статической генерации
13+
export function generateStaticParams() {
14+
return [{ id: "this-is-a-dummy-id-for-static-build" }];
15+
}
16+
17+
export const metadata: Metadata = {
18+
title: "Создать пресет | Guitar Chain Generator",
19+
description: "Создание нового пресета на основе генерации",
20+
};
21+
22+
async function getGenerationData(generationId: string | undefined) {
23+
if (!generationId) {
24+
return { generation: null, error: "ID генерации не указан" };
25+
}
26+
27+
try {
28+
const generation = await generationDb.getGenerationById(generationId);
29+
if (!generation) {
30+
return { generation: null, error: "Генерация не найдена" };
31+
}
32+
return { generation, error: null };
33+
} catch (error) {
34+
console.error("Error fetching generation:", error);
35+
return { generation: null, error: "Ошибка при загрузке генерации" };
36+
}
37+
}
38+
39+
export default async function PresetCreatePage({
40+
params,
41+
}: PresetCreatePageProps) {
42+
const { generation, error } = await getGenerationData(params.id);
43+
44+
if (error || !generation) {
45+
return (
46+
<div className="min-h-screen bg-gray-100 py-8">
47+
<div className="max-w-4xl mx-auto px-4">
48+
<div className="text-center">
49+
<h1 className="text-2xl font-bold text-gray-900 mb-4">Ошибка</h1>
50+
<p className="text-gray-600 mb-8">
51+
{error || "Генерация не найдена"}
52+
</p>
53+
<a
54+
href="/admin"
55+
className="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
56+
>
57+
← Назад к каталогу
58+
</a>
59+
</div>
60+
</div>
61+
</div>
62+
);
63+
}
64+
65+
return (
66+
<div className="min-h-screen bg-gray-100">
67+
<header className="bg-white shadow-sm border-b">
68+
<div className="max-w-6xl mx-auto px-4 py-4">
69+
<div className="flex items-center justify-between">
70+
<div className="flex items-center gap-2">
71+
<a
72+
href="/admin"
73+
className="inline-flex items-center px-3 py-2 text-sm bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
74+
>
75+
← Назад к каталогу
76+
</a>
77+
<a
78+
href={`/admin/generation/${generation.id}`}
79+
className="inline-flex items-center px-3 py-2 text-sm bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
80+
>
81+
← К генерации
82+
</a>
83+
</div>
84+
<h1 className="text-xl font-bold text-gray-900">Создать пресет</h1>
85+
<div className="text-sm text-gray-500">
86+
Из генерации {generation.id}
87+
</div>
88+
</div>
89+
</div>
90+
</header>
91+
92+
<main className="max-w-4xl mx-auto px-4 py-8">
93+
<PresetCreateForm generation={generation} artists={artistsRaw} />
94+
</main>
95+
</div>
96+
);
97+
}

app/api/preset/create/route.ts

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
import fs from "fs/promises";
3+
import path from "path";
4+
import { generationDb } from "../../../../lib/jsondb";
5+
import { validatePresetWithArtist } from "../../../../lib/public/schemas/preset";
6+
import { validateArtist } from "../../../../lib/public/schemas/artist";
7+
import { createSlug } from "../../../../lib/utils/create-slug";
8+
import { createPresetSlugBase } from "lib/utils/urls";
9+
import { Preset } from "lib/public/interface";
10+
11+
interface CreatePresetRequest {
12+
generationId: string;
13+
artistId?: number | null;
14+
newArtist?: {
15+
title: string;
16+
description: string;
17+
} | null;
18+
song: string;
19+
part: string;
20+
imageUrl?: string | null;
21+
tabsUrl?: string;
22+
pickup: {
23+
type: string;
24+
tone: number;
25+
position: string;
26+
};
27+
}
28+
29+
export async function POST(request: NextRequest) {
30+
try {
31+
const body: CreatePresetRequest = await request.json();
32+
33+
// Валидация входных данных
34+
if (!body.generationId) {
35+
return NextResponse.json(
36+
{ error: "Generation ID is required" },
37+
{ status: 400 },
38+
);
39+
}
40+
if (!body.song || !body.song.trim()) {
41+
return NextResponse.json(
42+
{ error: "Song name is required" },
43+
{ status: 400 },
44+
);
45+
}
46+
if (!body.part || !body.part.trim()) {
47+
return NextResponse.json(
48+
{ error: "Song part is required" },
49+
{ status: 400 },
50+
);
51+
}
52+
if (
53+
!body.artistId &&
54+
(!body.newArtist || !body.newArtist.title || !body.newArtist.title.trim())
55+
) {
56+
return NextResponse.json(
57+
{ error: "Artist ID or new artist data is required" },
58+
{ status: 400 },
59+
);
60+
}
61+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
62+
if (!body.pickup?.type?.trim()) {
63+
return NextResponse.json(
64+
{ error: "Pickup type is required" },
65+
{ status: 400 },
66+
);
67+
}
68+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
69+
if (!body.pickup?.tone || body.pickup.tone < 1 || body.pickup.tone > 10) {
70+
return NextResponse.json(
71+
{ error: "Pickup tone must be between 1 and 10" },
72+
{ status: 400 },
73+
);
74+
}
75+
76+
// Получаем генерацию
77+
const generation = await generationDb.getGenerationById(body.generationId);
78+
if (!generation) {
79+
return NextResponse.json(
80+
{ error: "Generation not found" },
81+
{ status: 404 },
82+
);
83+
}
84+
85+
// Получаем последнюю версию chain
86+
if (generation.versions.length === 0) {
87+
return NextResponse.json(
88+
{ error: "Generation has no versions" },
89+
{ status: 400 },
90+
);
91+
}
92+
const latestVersion = generation.versions[generation.versions.length - 1];
93+
if (!latestVersion) {
94+
return NextResponse.json(
95+
{ error: "Latest version not found" },
96+
{ status: 400 },
97+
);
98+
}
99+
const latestChain = latestVersion.chain;
100+
101+
// Читаем существующие данные
102+
const presetsPath = path.join(process.cwd(), "data", "presets.json");
103+
const artistsPath = path.join(process.cwd(), "data", "artists.json");
104+
105+
const [presetsData, artistsData] = await Promise.all([
106+
fs.readFile(presetsPath, "utf-8").then((data) => JSON.parse(data)),
107+
fs.readFile(artistsPath, "utf-8").then((data) => JSON.parse(data)),
108+
]);
109+
110+
let artist;
111+
let artistId: number;
112+
113+
// Обрабатываем артиста
114+
if (body.newArtist) {
115+
// Создаем нового артиста
116+
const maxId = Math.max(
117+
...(artistsData as Array<{ id: number }>).map((a) => a.id),
118+
0,
119+
);
120+
artistId = maxId + 1;
121+
122+
artist = {
123+
id: artistId,
124+
title: body.newArtist.title.trim(),
125+
slug: createSlug(body.newArtist.title.trim()),
126+
description:
127+
body.newArtist.description.trim() ||
128+
`Описание для ${body.newArtist.title.trim()}`,
129+
};
130+
131+
// Валидируем нового артиста
132+
validateArtist(artist);
133+
134+
// Добавляем в массив артистов
135+
artistsData.push(artist);
136+
} else {
137+
// Используем существующего артиста
138+
artistId = body.artistId as number;
139+
artist = (
140+
artistsData as Array<{
141+
id: number;
142+
title: string;
143+
slug: string;
144+
description: string;
145+
}>
146+
).find((a) => a.id === artistId);
147+
if (!artist) {
148+
return NextResponse.json(
149+
{ error: "Artist not found" },
150+
{ status: 404 },
151+
);
152+
}
153+
}
154+
155+
const presetId = generation.id;
156+
157+
const song = body.song.trim();
158+
const part = body.part.trim();
159+
const slug = createPresetSlugBase(song, part);
160+
161+
// Проверяем уникальность комбинации slug + artistId
162+
const existingPreset = (
163+
presetsData as Array<{
164+
slug: string;
165+
origin: { artistId: number };
166+
}>
167+
).find(
168+
(preset) => preset.slug === slug && preset.origin.artistId === artistId,
169+
);
170+
171+
if (existingPreset) {
172+
return NextResponse.json(
173+
{
174+
error: `Preset with slug "${slug}" for artist ID ${String(artistId)} already exists`,
175+
details: {
176+
slug,
177+
artistId,
178+
artistTitle: artist.title,
179+
},
180+
},
181+
{ status: 409 },
182+
);
183+
}
184+
185+
// Создаем объект пресета
186+
const preset = {
187+
id: presetId,
188+
origin: {
189+
artistId: artistId,
190+
song: song,
191+
part: part,
192+
imageUrl: body.imageUrl?.trim() || null,
193+
},
194+
description: generation.proDescription.sound_description,
195+
chain: latestChain,
196+
pickup: {
197+
type: body.pickup.type.trim(),
198+
tone: body.pickup.tone,
199+
position: body.pickup.position,
200+
},
201+
slug: slug,
202+
tabsUrl: body.tabsUrl?.trim() || undefined,
203+
};
204+
205+
// Создаем объект для валидации с полным артистом
206+
const presetWithArtist: Preset = {
207+
id: preset.id,
208+
origin: {
209+
artist: artist,
210+
song: preset.origin.song,
211+
part: preset.origin.part,
212+
imageUrl: preset.origin.imageUrl,
213+
},
214+
description: preset.description,
215+
chain: preset.chain,
216+
// @ts-expect-error this is fine
217+
pickup: preset.pickup,
218+
slug: preset.slug,
219+
tabsUrl: preset.tabsUrl,
220+
};
221+
222+
// Валидируем пресет
223+
validatePresetWithArtist(presetWithArtist);
224+
225+
// Добавляем пресет в массив
226+
presetsData.push(preset);
227+
228+
// Сохраняем файлы
229+
await Promise.all([
230+
fs.writeFile(presetsPath, JSON.stringify(presetsData, null, 2), "utf-8"),
231+
fs.writeFile(artistsPath, JSON.stringify(artistsData, null, 2), "utf-8"),
232+
]);
233+
234+
console.log(`✅ Пресет создан: ${presetId} для артиста ${artist.title}`);
235+
236+
return NextResponse.json({
237+
success: true,
238+
presetId: presetId,
239+
presetSlug: slug,
240+
artistSlug: artist.slug,
241+
message: "Preset created successfully",
242+
});
243+
} catch (error) {
244+
console.error("Error creating preset:", error);
245+
246+
if (error instanceof Error) {
247+
// Если это ошибка валидации Zod
248+
if (error.message.includes("validation")) {
249+
return NextResponse.json(
250+
{ error: `Validation error: ${error.message}` },
251+
{ status: 400 },
252+
);
253+
}
254+
return NextResponse.json({ error: error.message }, { status: 500 });
255+
}
256+
257+
return NextResponse.json(
258+
{ error: "Internal server error" },
259+
{ status: 500 },
260+
);
261+
}
262+
}

0 commit comments

Comments
 (0)