feat: divide page into components
This commit is contained in:
+10
-2
@@ -99,11 +99,19 @@
|
|||||||
- Tailwind CSS (опционально для стилей)
|
- Tailwind CSS (опционально для стилей)
|
||||||
|
|
||||||
### Основные компоненты
|
### Основные компоненты
|
||||||
|
|
||||||
|
#### UI компоненты (максимум 5)
|
||||||
|
- AppHeader - заголовок приложения с кнопкой загрузки изображения
|
||||||
|
- ImageManager - управление загрузкой и обрезкой изображений
|
||||||
|
- TextSection - управление текстами и настройками
|
||||||
|
- BackgroundPreview - превью фонового изображения
|
||||||
|
- PanelsList - список превью всех созданных панелей
|
||||||
|
|
||||||
|
#### Вспомогательные компоненты
|
||||||
- ImageUpload - загрузка изображений
|
- ImageUpload - загрузка изображений
|
||||||
- ImageCropper - обрезка изображений
|
- ImageCropper - обрезка изображений
|
||||||
- TextManager - добавление, редактирование и удаление текстов, настройки текста
|
- TextManager - добавление, редактирование и удаление текстов
|
||||||
- PanelPreview - превью отдельной панели (только для просмотра)
|
- PanelPreview - превью отдельной панели (только для просмотра)
|
||||||
- PanelList - список превью всех созданных панелей
|
|
||||||
|
|
||||||
### Хранилище данных
|
### Хранилище данных
|
||||||
- panelStore - хранение состояния панелей и функций для их создания
|
- panelStore - хранение состояния панелей и функций для их создания
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Button } from "../lib/components/ui";
|
||||||
|
|
||||||
|
let {
|
||||||
|
onUploadNewImage
|
||||||
|
}: {
|
||||||
|
onUploadNewImage?: () => void;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="app-header">
|
||||||
|
<h1>Twitch Panels Creator</h1>
|
||||||
|
{#if onUploadNewImage}
|
||||||
|
<Button variant="primary" onclick={onUploadNewImage}>Загрузить изображение</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.app-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1.5rem;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-radius: 12px;
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Button } from "../lib/components/ui";
|
||||||
|
|
||||||
|
let {
|
||||||
|
backgroundImage,
|
||||||
|
onUploadNewImage
|
||||||
|
}: {
|
||||||
|
backgroundImage?: string | null;
|
||||||
|
onUploadNewImage: () => void;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if backgroundImage}
|
||||||
|
<div class="background-preview">
|
||||||
|
<div class="background-header">
|
||||||
|
<h3>Фоновое изображение</h3>
|
||||||
|
<Button variant="secondary" size="sm" onclick={onUploadNewImage}>Загрузить</Button>
|
||||||
|
</div>
|
||||||
|
<img src={backgroundImage} alt="Фон" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.background-preview {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.background-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.background-preview h3 {
|
||||||
|
margin: 0;
|
||||||
|
color: #333;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.background-preview img {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 320px;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import ImageUpload from "./ImageUpload.svelte";
|
||||||
|
import ImageCropper from "./ImageCropper.svelte";
|
||||||
|
|
||||||
|
let {
|
||||||
|
currentStep,
|
||||||
|
uploadedImage,
|
||||||
|
onImageUpload,
|
||||||
|
onCropComplete,
|
||||||
|
onCropCancel
|
||||||
|
}: {
|
||||||
|
currentStep: string;
|
||||||
|
uploadedImage: string | null;
|
||||||
|
onImageUpload: (image: string) => void;
|
||||||
|
onCropComplete: (croppedImage: string) => void;
|
||||||
|
onCropCancel: () => void;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if currentStep === "upload"}
|
||||||
|
<ImageUpload onImageSelect={onImageUpload} />
|
||||||
|
{:else if currentStep === "crop" && uploadedImage}
|
||||||
|
<ImageCropper imageSrc={uploadedImage} onCropComplete={onCropComplete} onCancel={onCropCancel} />
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Panel } from "../lib/types/panel";
|
||||||
|
import PanelPreview from "./PanelPreview.svelte";
|
||||||
|
import { Button } from "../lib/components/ui";
|
||||||
|
|
||||||
|
let {
|
||||||
|
panels,
|
||||||
|
onDownload,
|
||||||
|
onDownloadAll
|
||||||
|
}: {
|
||||||
|
panels: Panel[];
|
||||||
|
onDownload: (panel: Panel) => void;
|
||||||
|
onDownloadAll: () => void;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if panels.length > 0}
|
||||||
|
<div class="panels-container">
|
||||||
|
<div class="panels-header">
|
||||||
|
<h2>Созданные панели ({panels.length})</h2>
|
||||||
|
<Button variant="primary" onclick={onDownloadAll}>Скачать все</Button>
|
||||||
|
</div>
|
||||||
|
<div class="panels-list">
|
||||||
|
{#each panels as panel (panel.id)}
|
||||||
|
<div class="panel-item">
|
||||||
|
<PanelPreview
|
||||||
|
panel={panel}
|
||||||
|
onDownload={() => onDownload(panel)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.panels-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panels-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panels-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panels-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-item {
|
||||||
|
border: 2px solid #e9ecef;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import TextManager from "./TextManager.svelte";
|
||||||
|
|
||||||
|
let {
|
||||||
|
texts,
|
||||||
|
onTextAdd,
|
||||||
|
onTextUpdate,
|
||||||
|
onTextDelete
|
||||||
|
}: {
|
||||||
|
texts: Array<{ id: string; text: string }>;
|
||||||
|
onTextAdd: (text: string) => void;
|
||||||
|
onTextUpdate: (id: string, text: string) => void;
|
||||||
|
onTextDelete: (id: string) => void;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="text-section">
|
||||||
|
<h2>Добавьте тексты для панелей</h2>
|
||||||
|
<TextManager
|
||||||
|
onTextAdd={onTextAdd}
|
||||||
|
onTextUpdate={onTextUpdate}
|
||||||
|
onTextDelete={onTextDelete}
|
||||||
|
texts={texts}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.text-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-section h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: #333;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+26
-174
@@ -6,11 +6,11 @@
|
|||||||
import { exportService } from "../lib/services/exportService";
|
import { exportService } from "../lib/services/exportService";
|
||||||
import type { Panel } from "../lib/types/panel";
|
import type { Panel } from "../lib/types/panel";
|
||||||
|
|
||||||
import ImageUpload from "../components/ImageUpload.svelte";
|
import AppHeader from "../components/AppHeader.svelte";
|
||||||
import ImageCropper from "../components/ImageCropper.svelte";
|
import ImageManager from "../components/ImageManager.svelte";
|
||||||
import TextManager from "../components/TextManager.svelte";
|
import TextSection from "../components/TextSection.svelte";
|
||||||
import PanelPreview from "../components/PanelPreview.svelte";
|
import BackgroundPreview from "../components/BackgroundPreview.svelte";
|
||||||
import { Button } from "../lib/components/ui";
|
import PanelsList from "../components/PanelsList.svelte";
|
||||||
|
|
||||||
let uploadedImage = $state<string | null>(null);
|
let uploadedImage = $state<string | null>(null);
|
||||||
let panels = $state<Panel[]>([]);
|
let panels = $state<Panel[]>([]);
|
||||||
@@ -84,12 +84,10 @@
|
|||||||
if (existingPanel) {
|
if (existingPanel) {
|
||||||
return updatePanelText(existingPanel, textItem.text);
|
return updatePanelText(existingPanel, textItem.text);
|
||||||
}
|
}
|
||||||
return createPanelFromText(backgroundImage, textItem.text);
|
return createPanelFromText(backgroundImage!, textItem.text);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async function handleDownload(panel: Panel) {
|
async function handleDownload(panel: Panel) {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -117,14 +115,10 @@
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="app-container">
|
<div class="app-container">
|
||||||
<div class="app-header">
|
<AppHeader onUploadNewImage={handleUploadNewImage} />
|
||||||
<h1>Twitch Panels Creator</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if errorMessage}
|
{#if errorMessage}
|
||||||
<div class="error-message">
|
<div class="error-message">
|
||||||
@@ -134,50 +128,34 @@
|
|||||||
|
|
||||||
<div class="app-content">
|
<div class="app-content">
|
||||||
<div class="main-section">
|
<div class="main-section">
|
||||||
{#if $uiStore.currentStep === "upload"}
|
<ImageManager
|
||||||
<ImageUpload onImageSelect={handleImageUpload} />
|
currentStep={$uiStore.currentStep}
|
||||||
{:else if $uiStore.currentStep === "crop" && uploadedImage}
|
uploadedImage={uploadedImage}
|
||||||
<ImageCropper imageSrc={uploadedImage} onCropComplete={handleCropComplete} onCancel={handleCropCancel} />
|
onImageUpload={handleImageUpload}
|
||||||
{:else if $uiStore.currentStep === "text"}
|
onCropComplete={handleCropComplete}
|
||||||
<div class="text-section">
|
onCropCancel={handleCropCancel}
|
||||||
<h2>Добавьте тексты для панелей</h2>
|
/>
|
||||||
<TextManager
|
{#if $uiStore.currentStep === "text"}
|
||||||
|
<TextSection
|
||||||
|
texts={texts}
|
||||||
onTextAdd={handleAddText}
|
onTextAdd={handleAddText}
|
||||||
onTextUpdate={handleUpdateText}
|
onTextUpdate={handleUpdateText}
|
||||||
onTextDelete={handleDeleteText}
|
onTextDelete={handleDeleteText}
|
||||||
texts={texts}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sidebar">
|
<div class="sidebar">
|
||||||
{#if $uiStore.currentStep === "text" && backgroundImage}
|
{#if $uiStore.currentStep === "text"}
|
||||||
<div class="background-preview">
|
<BackgroundPreview
|
||||||
<div class="background-header">
|
backgroundImage={backgroundImage}
|
||||||
<h3>Фоновое изображение</h3>
|
onUploadNewImage={handleUploadNewImage}
|
||||||
<Button variant="secondary" size="sm" onclick={handleUploadNewImage}>Загрузить</Button>
|
/>
|
||||||
</div>
|
<PanelsList
|
||||||
<img src={backgroundImage} alt="Фон" />
|
panels={panels}
|
||||||
</div>
|
onDownload={handleDownload}
|
||||||
{/if}
|
onDownloadAll={handleDownloadAll}
|
||||||
{#if $uiStore.currentStep === "text" && panels.length > 0}
|
|
||||||
<div class="panels-container">
|
|
||||||
<div class="panels-header">
|
|
||||||
<h2>Созданные панели ({panels.length})</h2>
|
|
||||||
<Button variant="primary" onclick={handleDownloadAll}>Скачать все</Button>
|
|
||||||
</div>
|
|
||||||
<div class="panels-list">
|
|
||||||
{#each panels as panel (panel.id)}
|
|
||||||
<div class="panel-item">
|
|
||||||
<PanelPreview
|
|
||||||
panel={panel}
|
|
||||||
onDownload={() => handleDownload(panel)}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -193,23 +171,6 @@
|
|||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 1.5rem;
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
border-radius: 12px;
|
|
||||||
color: white;
|
|
||||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-header h1 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 2rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-content {
|
.app-content {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
@@ -222,91 +183,12 @@
|
|||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-section {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-section h2 {
|
|
||||||
margin: 0;
|
|
||||||
color: #333;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.background-preview {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5rem;
|
|
||||||
padding: 1rem;
|
|
||||||
background: white;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.background-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.background-preview h3 {
|
|
||||||
margin: 0;
|
|
||||||
color: #333;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.background-preview img {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 320px;
|
|
||||||
height: auto;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panels-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panels-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 1rem;
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
border-radius: 8px;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panels-header h2 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panels-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-item {
|
|
||||||
border: 2px solid #e9ecef;
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
background: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-message {
|
.error-message {
|
||||||
background: #ffebee;
|
background: #ffebee;
|
||||||
color: #c62828;
|
color: #c62828;
|
||||||
@@ -316,36 +198,6 @@
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading {
|
|
||||||
text-align: center;
|
|
||||||
padding: 2rem;
|
|
||||||
color: #666;
|
|
||||||
background: #f9f9f9;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 2px solid #e9ecef;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
padding: 0.75rem 1.5rem;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 1rem;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
background: white;
|
|
||||||
color: #667eea;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover {
|
|
||||||
background: #f0f0f0;
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
@media (max-width: 1024px) {
|
||||||
.app-content {
|
.app-content {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|||||||
Reference in New Issue
Block a user