feat: divide page into components

This commit is contained in:
2026-02-04 10:25:24 +05:00
parent 3aa5ada9ba
commit aa5c55f95f
7 changed files with 285 additions and 299 deletions
+72
View File
@@ -0,0 +1,72 @@
<script lang="ts">
import AppHeader from "./AppHeader.svelte";
import ErrorMessage from "./ErrorMessage.svelte";
import AppContent from "./AppContent.svelte";
interface Props {
errorMessage: string | null;
uploadedImage: string | null;
texts: Array<{ id: string; text: string }>;
backgroundImage: string | null;
panels: any[];
onUploadNewImage: () => void;
onImageUpload: (image: string) => void;
onCropComplete: (croppedImage: string) => void;
onCropCancel: () => void;
onTextAdd: (text: string) => void;
onTextUpdate: (id: string, newText: string) => void;
onTextDelete: (id: string) => void;
onDownload: (panel: any) => Promise<void>;
onDownloadAll: () => Promise<void>;
}
let {
errorMessage,
uploadedImage,
texts,
backgroundImage,
panels,
onUploadNewImage,
onImageUpload,
onCropComplete,
onCropCancel,
onTextAdd,
onTextUpdate,
onTextDelete,
onDownload,
onDownloadAll
}: Props = $props();
</script>
<div class="app-container">
<AppHeader onUploadNewImage={onUploadNewImage} />
<ErrorMessage {errorMessage} />
<AppContent
{uploadedImage}
{texts}
{backgroundImage}
{panels}
onImageUpload={onImageUpload}
onCropComplete={onCropComplete}
onCropCancel={onCropCancel}
onTextAdd={onTextAdd}
onTextUpdate={onTextUpdate}
onTextDelete={onTextDelete}
onUploadNewImage={onUploadNewImage}
onDownload={onDownload}
onDownloadAll={onDownloadAll}
/>
</div>
<style>
.app-container {
display: flex;
flex-direction: column;
gap: 1.5rem;
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
</style>
+70
View File
@@ -0,0 +1,70 @@
<script lang="ts">
import MainSection from "./MainSection.svelte";
import Sidebar from "./Sidebar.svelte";
interface Props {
uploadedImage: string | null;
texts: Array<{ id: string; text: string }>;
backgroundImage: string | null;
panels: any[];
onImageUpload: (image: string) => void;
onCropComplete: (croppedImage: string) => void;
onCropCancel: () => void;
onTextAdd: (text: string) => void;
onTextUpdate: (id: string, newText: string) => void;
onTextDelete: (id: string) => void;
onUploadNewImage: () => void;
onDownload: (panel: any) => Promise<void>;
onDownloadAll: () => Promise<void>;
}
let {
uploadedImage,
texts,
backgroundImage,
panels,
onImageUpload,
onCropComplete,
onCropCancel,
onTextAdd,
onTextUpdate,
onTextDelete,
onUploadNewImage,
onDownload,
onDownloadAll
}: Props = $props();
</script>
<div class="app-content">
<MainSection
{uploadedImage}
{texts}
onImageUpload={onImageUpload}
onCropComplete={onCropComplete}
onCropCancel={onCropCancel}
onTextAdd={onTextAdd}
onTextUpdate={onTextUpdate}
onTextDelete={onTextDelete}
/>
<Sidebar
{backgroundImage}
{panels}
onUploadNewImage={onUploadNewImage}
onDownload={onDownload}
onDownloadAll={onDownloadAll}
/>
</div>
<style>
.app-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
}
@media (max-width: 1024px) {
.app-content {
grid-template-columns: 1fr;
}
}
</style>
+24
View File
@@ -0,0 +1,24 @@
<script lang="ts">
interface Props {
message: string | null;
}
let { message }: Props = $props();
</script>
{#if message}
<div class="error-message">
{message}
</div>
{/if}
<style>
.error-message {
background: #ffebee;
color: #c62828;
padding: 1rem;
border-radius: 8px;
border: 1px solid #ffcdd2;
font-weight: 500;
}
</style>
+53
View File
@@ -0,0 +1,53 @@
<script lang="ts">
import { uiStore } from "../stores/uiStore";
import ImageManager from "./ImageManager.svelte";
import TextSection from "./TextSection.svelte";
interface Props {
uploadedImage: string | null;
texts: Array<{ id: string; text: string }>;
onImageUpload: (image: string) => void;
onCropComplete: (croppedImage: string) => void;
onCropCancel: () => void;
onTextAdd: (text: string) => void;
onTextUpdate: (id: string, newText: string) => void;
onTextDelete: (id: string) => void;
}
let {
uploadedImage,
texts,
onImageUpload,
onCropComplete,
onCropCancel,
onTextAdd,
onTextUpdate,
onTextDelete
}: Props = $props();
</script>
<div class="main-section">
<ImageManager
currentStep={$uiStore.currentStep}
{uploadedImage}
onImageUpload={onImageUpload}
onCropComplete={onCropComplete}
onCropCancel={onCropCancel}
/>
{#if $uiStore.currentStep === "text"}
<TextSection
{texts}
onTextAdd={onTextAdd}
onTextUpdate={onTextUpdate}
onTextDelete={onTextDelete}
/>
{/if}
</div>
<style>
.main-section {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
</style>
+49
View File
@@ -0,0 +1,49 @@
<script lang="ts">
import { uiStore } from "../stores/uiStore";
import BackgroundPreview from "./BackgroundPreview.svelte";
import PanelsList from "./PanelsList.svelte";
interface Props {
backgroundImage: string | null;
panels: any[];
onUploadNewImage: () => void;
onDownload: (panel: any) => Promise<void>;
onDownloadAll: () => Promise<void>;
}
let {
backgroundImage,
panels,
onUploadNewImage,
onDownload,
onDownloadAll
}: Props = $props();
</script>
<div class="sidebar">
{#if $uiStore.currentStep === "text"}
<BackgroundPreview
{backgroundImage}
onUploadNewImage={onUploadNewImage}
/>
<PanelsList
{panels}
onDownload={onDownload}
onDownloadAll={onDownloadAll}
/>
{/if}
</div>
<style>
.sidebar {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
@media (max-width: 1024px) {
.sidebar {
order: -1;
}
}
</style>
+17 -96
View File
@@ -6,11 +6,7 @@
import { exportService } from "../services/exportService"; import { exportService } from "../services/exportService";
import type { Panel } from "../lib/types/panel"; import type { Panel } from "../lib/types/panel";
import AppHeader from "../components/AppHeader.svelte"; import AppContainer from "../components/AppContainer.svelte";
import ImageManager from "../components/ImageManager.svelte";
import TextSection from "../components/TextSection.svelte";
import BackgroundPreview from "../components/BackgroundPreview.svelte";
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[]>([]);
@@ -71,94 +67,19 @@
} }
</script> </script>
<div class="app-container"> <AppContainer
<AppHeader onUploadNewImage={handleUploadNewImage} /> errorMessage={exportService.getErrorMessage()}
{uploadedImage}
{#if exportService.getErrorMessage()} {texts}
<div class="error-message"> {backgroundImage}
{exportService.getErrorMessage()} {panels}
</div> onUploadNewImage={handleUploadNewImage}
{/if} onImageUpload={handleImageUpload}
onCropComplete={handleCropComplete}
<div class="app-content"> onCropCancel={handleCropCancel}
<div class="main-section"> onTextAdd={handleAddText}
<ImageManager onTextUpdate={handleUpdateText}
currentStep={$uiStore.currentStep} onTextDelete={handleDeleteText}
uploadedImage={uploadedImage} onDownload={handleDownload}
onImageUpload={handleImageUpload} onDownloadAll={handleDownloadAll}
onCropComplete={handleCropComplete} />
onCropCancel={handleCropCancel}
/>
{#if $uiStore.currentStep === "text"}
<TextSection
texts={texts}
onTextAdd={handleAddText}
onTextUpdate={handleUpdateText}
onTextDelete={handleDeleteText}
/>
{/if}
</div>
<div class="sidebar">
{#if $uiStore.currentStep === "text"}
<BackgroundPreview
backgroundImage={backgroundImage}
onUploadNewImage={handleUploadNewImage}
/>
<PanelsList
panels={panels}
onDownload={handleDownload}
onDownloadAll={handleDownloadAll}
/>
{/if}
</div>
</div>
</div>
<style>
.app-container {
display: flex;
flex-direction: column;
gap: 1.5rem;
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
.app-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
}
.main-section {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.sidebar {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.error-message {
background: #ffebee;
color: #c62828;
padding: 1rem;
border-radius: 8px;
border: 1px solid #ffcdd2;
font-weight: 500;
}
@media (max-width: 1024px) {
.app-content {
grid-template-columns: 1fr;
}
.sidebar {
order: -1;
}
}
</style>
-203
View File
@@ -1,203 +0,0 @@
<script lang="ts">
import { onMount } from "svelte";
import { uiStore } from "../stores/uiStore";
import { imageService } from "../services/imageService";
import { panelService } from "../services/panelService";
import { exportService } from "../services/exportService";
import type { Panel } from "../lib/types/panel";
import AppHeader from "../components/AppHeader.svelte";
import ImageManager from "../components/ImageManager.svelte";
import TextSection from "../components/TextSection.svelte";
import BackgroundPreview from "../components/BackgroundPreview.svelte";
import PanelsList from "../components/PanelsList.svelte";
let uploadedImage = $state<string | null>(null);
let panels = $state<Panel[]>([]);
let texts = $state<Array<{ id: string; text: string }>>([]);
let backgroundImage = $state<string | null>(null);
onMount(async () => {
try {
backgroundImage = await imageService.loadDefaultBackground();
} catch (error) {
exportService.setErrorMessage("Не удалось загрузить фоновое изображение по умолчанию");
}
});
function handleImageUpload(image: string) {
uploadedImage = image;
imageService.handleImageUpload(image);
}
function handleUploadNewImage() {
imageService.handleUploadNewImage();
}
function handleCropComplete(croppedImage: string) {
backgroundImage = croppedImage;
uploadedImage = null;
imageService.handleCropComplete(croppedImage);
panels = panelService.updatePanelsBackground(panels, croppedImage);
panels = panelService.updatePanelsFromTexts(texts, panels, croppedImage);
}
function handleCropCancel() {
uploadedImage = null;
imageService.handleCropCancel();
}
function handleAddText(text: string) {
// Проверяем, что текст не пустой и не дублируется
if (!text.trim()) return;
if (texts.some(t => t.text === text)) return;
texts = [...texts, { id: crypto.randomUUID(), text }];
updatePanelsFromTexts();
}
function handleUpdateText(id: string, newText: string) {
// Проверяем, что текст не пустой и не дублируется
if (!newText.trim()) return;
if (texts.some(t => t.id !== id && t.text === newText)) return;
texts = texts.map(t => t.id === id ? { ...t, text: newText } : t);
updatePanelsFromTexts();
}
function handleDeleteText(id: string) {
texts = texts.filter(t => t.id !== id);
updatePanelsFromTexts();
}
function updatePanelsFromTexts() {
if (!backgroundImage) return;
panels = texts.map(textItem => {
const existingPanel = panels.find(p => p.texts[0]?.text === textItem.text);
if (existingPanel) {
return updatePanelText(existingPanel, textItem.text);
}
return createPanelFromText(backgroundImage!, textItem.text);
});
}
async function handleDownload(panel: Panel) {
try {
setLoading(true);
const result = await exportService.exportPanel(panel);
if (!result.success) {
errorMessage = result.error || "Ошибка экспорта панели";
}
} catch (error) {
errorMessage = error instanceof Error ? error.message : "Ошибка экспорта панели";
} finally {
setLoading(false);
}
}
async function handleDownloadAll() {
try {
setLoading(true);
for (const panel of panels) {
await exportService.exportPanel(panel);
}
} catch (error) {
errorMessage = error instanceof Error ? error.message : "Ошибка экспорта панелей";
} finally {
setLoading(false);
}
}
</script>
<div class="app-container">
<AppHeader onUploadNewImage={handleUploadNewImage} />
{#if errorMessage}
<div class="error-message">
{errorMessage}
</div>
{/if}
<div class="app-content">
<div class="main-section">
<ImageManager
currentStep={$uiStore.currentStep}
uploadedImage={uploadedImage}
onImageUpload={handleImageUpload}
onCropComplete={handleCropComplete}
onCropCancel={handleCropCancel}
/>
{#if $uiStore.currentStep === "text"}
<TextSection
texts={texts}
onTextAdd={handleAddText}
onTextUpdate={handleUpdateText}
onTextDelete={handleDeleteText}
/>
{/if}
</div>
<div class="sidebar">
{#if $uiStore.currentStep === "text"}
<BackgroundPreview
backgroundImage={backgroundImage}
onUploadNewImage={handleUploadNewImage}
/>
<PanelsList
panels={panels}
onDownload={handleDownload}
onDownloadAll={handleDownloadAll}
/>
{/if}
</div>
</div>
</div>
<style>
.app-container {
display: flex;
flex-direction: column;
gap: 1.5rem;
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
.app-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
}
.main-section {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.sidebar {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.error-message {
background: #ffebee;
color: #c62828;
padding: 1rem;
border-radius: 8px;
border: 1px solid #ffcdd2;
font-weight: 500;
}
@media (max-width: 1024px) {
.app-content {
grid-template-columns: 1fr;
}
.sidebar {
order: -1;
}
}
</style>