fix: remove old components

This commit is contained in:
2026-02-05 14:58:19 +05:00
parent 7ebbc85e3a
commit f1f37aec9e
17 changed files with 0 additions and 1983 deletions
@@ -1,24 +0,0 @@
<script lang="ts">
interface Props {
errorMessage: string | undefined;
}
let { errorMessage }: Props = $props();
</script>
{#if errorMessage}
<div class="error-message">
{errorMessage}
</div>
{/if}
<style>
.error-message {
background: #ffebee;
color: #c62828;
padding: 1rem;
border-radius: 8px;
border: 1px solid #ffcdd2;
font-weight: 500;
}
</style>
@@ -1,53 +0,0 @@
<script lang="ts">
import Button from "$components/ui/Button.svelte";
interface Props {
backgroundImage?: string | undefined;
onUploadNewImage: () => void;
}
let { backgroundImage, onUploadNewImage }: Props = $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>
-215
View File
@@ -1,215 +0,0 @@
<script lang="ts">
import Button from "$components/ui/Button.svelte";
import { PANEL_SETTINGS } from "$lib/constants";
import type { ImageCropResult } from "$lib/types/panel";
import { setLoading } from "$stores/uiStore";
import CropperJS from "cropperjs";
import { onMount } from "svelte";
interface Props {
imageSrc: string;
onCropComplete: (croppedImage: string) => void;
onCancel: () => void;
}
let { imageSrc, onCropComplete, onCancel }: Props = $props();
let imageElement: HTMLImageElement;
let cropper: CropperJS | undefined = undefined;
let isProcessing = $state(false);
let errorMessage = $state<string | undefined>(undefined);
onMount(() => {
initializeCropper();
return () => {
if (cropper) {
cropper.destroy();
}
};
});
function initializeCropper() {
if (!imageElement || !CropperJS) {
return;
}
cropper = new CropperJS(imageElement);
}
async function handleCrop() {
if (!cropper) {
return;
}
try {
isProcessing = true;
errorMessage = undefined;
setLoading(true);
const cropperCanvasElement = cropper.getCropperCanvas();
if (!cropperCanvasElement) {
throw new Error("Не удалось получить обрезанное изображение");
}
const canvas = await cropperCanvasElement.$toCanvas({
width: PANEL_SETTINGS.PANEL_WIDTH,
});
if (!canvas) {
throw new Error("Не удалось получить canvas");
}
const croppedImage = canvas.toDataURL("image/png");
const cropResult: ImageCropResult = {
success: true,
croppedImage,
};
onCropComplete(croppedImage);
} catch (error) {
errorMessage = error instanceof Error ? error.message : "Ошибка обрезки изображения";
} finally {
isProcessing = false;
setLoading(false);
}
}
function handleCancel() {
if (cropper) {
cropper.destroy();
}
onCancel();
}
</script>
<div class="cropper-container">
<div class="cropper-wrapper">
<img bind:this={imageElement} src={imageSrc} alt="" class="cropper-image" />
</div>
{#if errorMessage}
<div class="error-message">
<strong>Ошибка:</strong>
{errorMessage}
</div>
{/if}
<div class="cropper-actions">
<Button variant="secondary" onclick={handleCancel} disabled={isProcessing}>Отмена</Button>
<Button variant="primary" onclick={handleCrop} disabled={isProcessing} loading={isProcessing}>Применить</Button>
</div>
<div class="cropper-info">
<p>💡 Изображение будет обрезано до ширины 320px</p>
</div>
</div>
<style>
.cropper-container {
display: flex;
flex-direction: column;
gap: 1rem;
width: 100%;
max-width: 800px;
margin: 0 auto;
}
.cropper-wrapper {
position: relative;
width: 100%;
max-height: 600px;
overflow: hidden;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.cropper-image {
display: block;
max-width: 100%;
height: auto;
}
.cropper-actions {
display: flex;
gap: 1rem;
justify-content: center;
padding: 1rem;
background: #f9f9f9;
border-radius: 8px;
}
.cropper-info {
text-align: center;
color: #666;
font-size: 0.9rem;
padding: 0.5rem;
}
.error-message {
background: #ffebee;
color: #c62828;
padding: 0.75rem;
border-radius: 4px;
border: 1px solid #ffcdd2;
}
:global(.cropper-view-box),
:global(.cropper-face) {
border-radius: 0;
}
:global(.cropper-line) {
background-color: rgba(0, 123, 255, 0.5);
}
:global(.cropper-point) {
background-color: #007bff;
}
:global(.cropper-container) {
direction: ltr;
font-size: 0;
line-height: 0;
position: relative;
touch-action: none;
user-select: none;
}
:global(.cropper-container img) {
display: block;
height: 100%;
image-orientation: 0deg;
max-height: none !important;
max-width: none !important;
min-height: 0 !important;
min-width: 0 !important;
width: 100%;
}
:global(.cropper-wrap-box, .cropper-canvas, .cropper-drag-box, .cropper-crop-box, .cropper-modal) {
bottom: 0;
left: 0;
position: absolute;
right: 0;
top: 0;
}
:global(.cropper-wrap-box, .cropper-canvas) {
overflow: hidden;
}
:global(.cropper-drag-box) {
background-color: #fff;
opacity: 0;
}
:global(.cropper-modal) {
background-color: rgba(0, 0, 0, 0.5);
}
:global(.cropper-crop-box) {
border-color: rgba(0, 123, 255, 0.5);
}
</style>
-20
View File
@@ -1,20 +0,0 @@
<script lang="ts">
import ImageUpload from "./ImageUpload.svelte";
import ImageCropper from "./ImageCropper.svelte";
interface Props {
currentStep: string;
uploadedImage: string | undefined;
onImageUpload: (image: string) => void;
onCropComplete: (croppedImage: string) => void;
onCropCancel: () => void;
}
let { currentStep, uploadedImage, onImageUpload, onCropComplete, onCropCancel }: Props = $props();
</script>
{#if currentStep === "upload"}
<ImageUpload onImageSelect={onImageUpload} />
{:else if currentStep === "crop" && uploadedImage}
<ImageCropper imageSrc={uploadedImage} {onCropComplete} onCancel={onCropCancel} />
{/if}
-401
View File
@@ -1,401 +0,0 @@
<script lang="ts">
import Button from "$components/ui/Button.svelte";
import type { ImageUploadResult } from "$lib/types/panel";
import { handleError } from "$lib/utils/errorHandler";
import { clearError, setCurrentStep, setLoading, uiStore } from "$stores/uiStore";
import { onMount } from "svelte";
import { imageService } from "../../services/imageService";
interface Props {
onImageSelect: (image: string) => void;
}
let { onImageSelect }: Props = $props();
let dropZone = $state<HTMLElement | undefined>(undefined);
let fileInput = $state<HTMLInputElement | undefined>(undefined);
let urlInput = $state<HTMLInputElement | undefined>(undefined);
let urlForm = $state<HTMLFormElement | undefined>(undefined);
let isDragOver = $state(false);
let showUrlInput = $state(false);
let uploadedImage = $state<string | undefined>(undefined);
let errorMessage = $state<string | undefined>(undefined);
const uiError = $derived($uiStore.error);
onMount(() => {
setupPasteHandler();
setupDragAndDrop();
});
function setupPasteHandler() {
document.addEventListener("paste", handlePaste);
return () => document.removeEventListener("paste", handlePaste);
}
function setupDragAndDrop() {
if (!dropZone) return;
dropZone!.addEventListener("dragover", handleDragOver);
dropZone!.addEventListener("dragleave", handleDragLeave);
dropZone!.addEventListener("drop", handleDrop);
return () => {
dropZone!.removeEventListener("dragover", handleDragOver);
dropZone!.removeEventListener("dragleave", handleDragLeave);
dropZone!.removeEventListener("drop", handleDrop);
};
}
function handleDragOver(e: DragEvent) {
e.preventDefault();
isDragOver = true;
}
function handleDragLeave(e: DragEvent) {
e.preventDefault();
isDragOver = false;
}
async function handleDrop(e: DragEvent) {
e.preventDefault();
isDragOver = false;
const files = e.dataTransfer?.files;
if (!files || files.length === 0) return;
const file = files[0];
await handleFileUpload(file);
}
async function handleFileUpload(file: File) {
try {
setLoading(true);
clearError();
const result: ImageUploadResult = await imageService.handleFileUpload(file);
if (result.success && result.image) {
uploadedImage = result.image;
onImageSelect(result.image);
setCurrentStep("crop");
} else {
errorMessage = result.error || "Ошибка загрузки файла";
}
} catch (error) {
errorMessage = handleError(error);
} finally {
setLoading(false);
}
}
async function handlePaste(e: ClipboardEvent) {
e.preventDefault();
try {
setLoading(true);
clearError();
const result: ImageUploadResult = await imageService.handlePasteUpload(e);
if (result.success && result.image) {
uploadedImage = result.image;
onImageSelect(result.image);
setCurrentStep("crop");
} else {
errorMessage = result.error || "Ошибка вставки изображения";
}
} catch (error) {
errorMessage = handleError(error);
} finally {
setLoading(false);
}
}
async function handleUrlSubmit(e: Event) {
e.preventDefault();
if (!urlInput?.value.trim()) return;
try {
setLoading(true);
clearError();
const result: ImageUploadResult = await imageService.handleUrlUpload(urlInput.value.trim());
if (result.success && result.image) {
uploadedImage = result.image;
onImageSelect(result.image);
setCurrentStep("crop");
showUrlInput = false;
} else {
errorMessage = result.error || "Ошибка загрузки изображения";
}
} catch (error) {
errorMessage = handleError(error);
} finally {
setLoading(false);
}
}
function triggerFileInput() {
fileInput?.click();
}
function resetUpload() {
uploadedImage = undefined;
errorMessage = undefined;
showUrlInput = false;
setCurrentStep("upload");
}
</script>
<div class="image-upload-container">
{#if uploadedImage}
<div class="uploaded-image-preview">
<img src={uploadedImage} alt="Uploaded preview" />
<div class="preview-actions">
<Button variant="secondary" onclick={resetUpload}>Загрузить другое</Button>
</div>
</div>
{:else}
<div
bind:this={dropZone}
class="drop-zone {isDragOver ? 'drag-over' : ''} {uiError ? 'error' : ''}"
role="button"
tabindex="0"
onclick={triggerFileInput}
onkeydown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
triggerFileInput();
}
}}
>
<div class="drop-zone-content">
<svg class="upload-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
<h3>Загрузите изображение</h3>
<p>Перетащите файл сюда, нажмите для выбора, или вставьте через Ctrl+V</p>
<div class="upload-methods">
<Button
variant="primary"
onclick={(e) => {
e.stopPropagation();
triggerFileInput();
}}>Выбрать файл</Button
>
<Button variant="secondary" onclick={() => (showUrlInput = !showUrlInput)}>
{showUrlInput ? "Скрыть" : "По URL"}
</Button>
</div>
{#if showUrlInput}
<form bind:this={urlForm} onsubmit={handleUrlSubmit} class="url-input-form">
<input
bind:this={urlInput}
type="url"
placeholder="https://example.com/image.jpg"
required
class="url-input"
/>
<Button type="submit" variant="primary" disabled={$uiStore.isLoading} loading={$uiStore.isLoading}
>Загрузить</Button
>
</form>
{/if}
<div class="upload-tips">
<p>• Поддерживаемые форматы: JPG, PNG, WebP, GIF</p>
<p>• Максимальный размер: 10MB</p>
<p>• Или вставьте изображение через Ctrl+V</p>
</div>
</div>
</div>
<input
bind:this={fileInput}
type="file"
accept="image/jpeg,image/jpg,image/png,image/webp,image/gif"
style="display: none;"
onchange={(e) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (file) handleFileUpload(file);
}}
/>
{/if}
{#if errorMessage}
<div class="error-message">
<strong>Ошибка:</strong>
{errorMessage}
</div>
{/if}
{#if $uiStore.isLoading}
<div class="loading-overlay">
<div class="loading-spinner"></div>
<p>Загрузка...</p>
</div>
{/if}
</div>
<style>
.image-upload-container {
position: relative;
width: 100%;
max-width: 400px;
margin: 0 auto;
}
.drop-zone {
border: 2px dashed #ccc;
border-radius: 8px;
padding: 2rem;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
background: #f9f9f9;
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
}
.drop-zone:hover {
border-color: #007bff;
background: #f0f8ff;
}
.drop-zone.drag-over {
border-color: #007bff;
background: #e3f2fd;
transform: scale(1.02);
}
.drop-zone.error {
border-color: #dc3545;
background: #ffebee;
}
.drop-zone-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
.upload-icon {
width: 48px;
height: 48px;
color: #666;
}
.drop-zone h3 {
margin: 0;
color: #333;
font-size: 1.2rem;
}
.drop-zone p {
margin: 0;
color: #666;
font-size: 0.9rem;
}
.upload-methods {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
justify-content: center;
}
.url-input-form {
display: flex;
gap: 0.5rem;
margin-top: 1rem;
width: 100%;
}
.url-input {
flex: 1;
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 0.9rem;
}
.upload-tips {
font-size: 0.8rem;
color: #666;
margin-top: 1rem;
}
.upload-tips p {
margin: 0.25rem 0;
}
.uploaded-image-preview {
text-align: center;
}
.uploaded-image-preview img {
max-width: 100%;
max-height: 300px;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.preview-actions {
margin-top: 1rem;
}
.error-message {
background: #ffebee;
color: #c62828;
padding: 0.75rem;
border-radius: 4px;
margin-top: 1rem;
border: 1px solid #ffcdd2;
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.9);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: 8px;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #007bff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 1rem;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
-69
View File
@@ -1,69 +0,0 @@
<script lang="ts">
import ErrorMessage from "$components/feedback/ErrorMessage.svelte";
import AppContent from "$components/layout/AppContent.svelte";
interface Props {
errorMessage: string | undefined;
uploadedImage: string | undefined;
texts: Array<{ id: string; text: string }>;
backgroundImage: string | undefined;
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, konvaStage: 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">
<ErrorMessage {errorMessage} />
<AppContent
{uploadedImage}
{texts}
{backgroundImage}
{panels}
{onImageUpload}
{onCropComplete}
{onCropCancel}
{onTextAdd}
{onTextUpdate}
{onTextDelete}
{onUploadNewImage}
{onDownload}
{onDownloadAll}
/>
</div>
<style>
.app-container {
display: flex;
flex-direction: column;
gap: 1.5rem;
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
</style>
-64
View File
@@ -1,64 +0,0 @@
<script lang="ts">
import MainSection from "$components/layout/MainSection.svelte";
import Sidebar from "$components/layout/Sidebar.svelte";
interface Props {
uploadedImage: string | undefined;
texts: Array<{ id: string; text: string }>;
backgroundImage: string | undefined;
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, konvaStage: 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}
{onCropComplete}
{onCropCancel}
{onTextAdd}
{onTextUpdate}
{onTextDelete}
/>
<Sidebar {backgroundImage} {panels} {onUploadNewImage} {onDownload} {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>
-42
View File
@@ -1,42 +0,0 @@
<script lang="ts">
import ImageManager from "$components/image/ImageManager.svelte";
import TextSection from "$components/text/TextSection.svelte";
import { uiStore } from "$stores/uiStore";
interface Props {
uploadedImage: string | undefined;
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} {onCropComplete} {onCropCancel} />
{#if $uiStore.currentStep === "text"}
<TextSection {texts} {onTextAdd} {onTextUpdate} {onTextDelete} />
{/if}
</div>
<style>
.main-section {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
</style>
-36
View File
@@ -1,36 +0,0 @@
<script lang="ts">
import BackgroundPreview from "$components/image/BackgroundPreview.svelte";
import PanelsList from "$components/panel/PanelsList.svelte";
import { uiStore } from "$stores/uiStore";
interface Props {
backgroundImage: string | undefined;
panels: any[];
onUploadNewImage: () => void;
onDownload: (panel: any, konvaStage: 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} />
<PanelsList {panels} {onDownload} {onDownloadAll} />
{/if}
</div>
<style>
.sidebar {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
@media (max-width: 1024px) {
.sidebar {
order: -1;
}
}
</style>
-173
View File
@@ -1,173 +0,0 @@
<script lang="ts">
import Button from "$components/ui/Button.svelte";
import { PANEL_SETTINGS, TYPOGRAPHY, UI_SETTINGS } from "$lib/constants";
import type { Panel, TextItem } from "$lib/types/panel";
import { setCurrentStep } from "$stores/uiStore";
import type { Stage as KonvaStage } from "konva/lib/Stage";
import { Image, Layer, Stage, Text } from "svelte-konva";
interface Props {
panel: Panel;
onDownload?: (panel: Panel, konvaStage: KonvaStage) => void;
}
let { panel, onDownload }: Props = $props();
let backgroundImage: HTMLImageElement | undefined = $state(undefined);
let konvaStage: KonvaStage | null = $state(null);
let stageComponent: any = $state(null);
$effect(() => {
if (stageComponent) {
konvaStage = stageComponent.node;
}
});
function handleUploadNewImage() {
setCurrentStep(UI_SETTINGS.STEPS.UPLOAD);
}
$effect(() => {
if (panel?.backgroundImage && panel.backgroundImage !== backgroundImage?.src) {
loadImage(panel.backgroundImage);
}
});
function loadImage(src: string) {
const img = document.createElement("img");
img.onload = () => {
backgroundImage = img;
};
img.onerror = (error) => {
console.error("[PanelPreview] Failed to load background image");
};
img.src = src;
}
function handleDownload() {
let stageToUse = konvaStage;
if (!stageToUse && stageComponent) {
stageToUse = stageComponent.node || stageComponent.stage || stageComponent._stage || stageComponent;
if (stageToUse && typeof stageToUse.toBlob === "function") {
} else {
if (typeof stageComponent.toBlob === "function") {
stageToUse = stageComponent;
}
}
}
if (onDownload && stageToUse) {
onDownload(panel, stageToUse);
}
}
function getTextPosition(textItem: TextItem) {
const panelWidth = PANEL_SETTINGS.PANEL_WIDTH;
const paddingX = textItem.paddingX;
const verticalOffset = textItem.verticalOffset;
const centerY = panel.height / 2 + verticalOffset;
const textWidth = panelWidth - paddingX * 2;
let position;
switch (textItem.textAlign) {
case "left":
position = { x: paddingX, y: centerY };
break;
case "right":
position = { x: panelWidth - textWidth - paddingX, y: centerY };
break;
case "center":
default:
position = { x: (panelWidth - textWidth) / 2, y: centerY };
break;
}
return position;
}
</script>
<div class="panel-preview">
<div class="preview-header">
<div class="panel-title">{panel.text?.text || "Без названия"}</div>
<Button variant="primary" size="sm" onclick={handleDownload}>Скачать</Button>
</div>
<div class="preview-sections">
<div class="preview-section">
<div class="canvas-container">
<Stage width={PANEL_SETTINGS.PANEL_WIDTH} height={panel.height} bind:this={stageComponent}>
<Layer>
{#if backgroundImage}
<Image image={backgroundImage} width={PANEL_SETTINGS.PANEL_WIDTH} height={panel.height} />
{/if}
</Layer>
<Layer>
{#if panel.text}
{@const textPosition = getTextPosition(panel.text)}
<Text
text={panel.text.text}
fontSize={panel.text.fontSize || TYPOGRAPHY.FONT_SIZE_DEFAULT}
fill={panel.text.color || TYPOGRAPHY.TEXT_COLOR_DEFAULT}
fontFamily={panel.text.fontFamily || "Arial"}
x={textPosition.x}
y={textPosition.y}
align={panel.text.textAlign || TYPOGRAPHY.TEXT_ALIGN_CENTER}
width={PANEL_SETTINGS.PANEL_WIDTH - textPosition.x * 2}
offsetX={0}
/>
{/if}
</Layer>
</Stage>
</div>
</div>
</div>
</div>
<style>
.panel-preview {
display: flex;
flex-direction: column;
gap: 1rem;
width: 100%;
}
.preview-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 0.5rem;
padding: 1rem;
margin-bottom: 1rem;
background: #f8f9fa;
border-radius: 8px;
}
.panel-title {
font-weight: 600;
color: #333;
font-size: 1.1rem;
}
.preview-sections {
display: flex;
flex-direction: column;
gap: 1rem;
}
.preview-section {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.canvas-container {
display: flex;
justify-content: center;
padding: 1rem;
background: #f5f5f5;
border-radius: 8px;
}
</style>
-66
View File
@@ -1,66 +0,0 @@
<script lang="ts">
import type { Panel } from "$lib/types/panel";
import type { Stage as KonvaStage } from "konva/lib/Stage";
import Button from "../ui/Button.svelte";
import PanelPreview from "./PanelPreview.svelte";
interface Props {
panels: Panel[];
onDownload: (panel: Panel, konvaStage: KonvaStage) => void;
onDownloadAll: () => void;
}
let { panels, onDownload, onDownloadAll }: Props = $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} onDownload={(panel, konvaStage) => onDownload(panel, konvaStage)} />
</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>
-10
View File
@@ -1,10 +0,0 @@
<script lang="ts">
import IconPlus from "$components/ui/Icons/IconPlus.svelte";
</script>
<div class="input-group">
<input type="text" id="panelTextInput" placeholder="Введите текст..." class="text-input" />
<button id="addTextBtn" class="btn btn-primary">
<IconPlus />
</button>
</div>
-414
View File
@@ -1,414 +0,0 @@
<script lang="ts">
import Button from "$components/ui/Button.svelte";
import { TYPOGRAPHY } from "$lib/constants";
import type { TextAlign, TextItem } from "$lib/types/panel";
import { textSettingsStore, updateAllTextSettings } from "$stores/panelStore";
interface Props {
onTextAdd: (text: string, settings?: Partial<TextItem>) => void;
onTextUpdate: (id: string, text: string) => void;
onTextDelete: (id: string) => void;
texts: Array<{ id: string; text: string }>;
}
let { onTextAdd, onTextUpdate, onTextDelete, texts }: Props = $props();
let newText = $state("");
let errorMessage = $state<string | undefined>(undefined);
let commonTextSettings = $derived($textSettingsStore);
const availableFonts = TYPOGRAPHY.FONT_FAMILIES;
function handleAddText() {
if (!newText.trim()) {
errorMessage = "Введите текст";
return;
}
if (newText.length > TYPOGRAPHY.MAX_TEXT_LENGTH) {
errorMessage = `Текст не должен превышать ${TYPOGRAPHY.MAX_TEXT_LENGTH} символов`;
return;
}
try {
errorMessage = undefined;
onTextAdd(newText.trim(), commonTextSettings);
newText = "";
} catch (error) {
errorMessage = error instanceof Error ? error.message : "Ошибка добавления текста";
}
}
function handleKeyPress(e: KeyboardEvent) {
if (e.key === "Enter") {
handleAddText();
}
}
function handleUpdateText(id: string, newText: string) {
if (onTextUpdate && newText.trim()) {
onTextUpdate(id, newText.trim());
}
}
function handleDeleteText(id: string) {
if (onTextDelete) {
onTextDelete(id);
}
}
</script>
<div class="text-manager">
<div class="add-text-section">
<div class="input-group">
<input
type="text"
bind:value={newText}
placeholder="Введите текст для панели (например: links, about me, projects...)"
class="text-input"
maxlength={TYPOGRAPHY.MAX_TEXT_LENGTH}
onkeypress={handleKeyPress}
/>
<Button variant="primary" onclick={handleAddText}>Добавить</Button>
</div>
{#if errorMessage}
<div class="error-message">
{errorMessage}
</div>
{/if}
</div>
{#if texts.length > 0}
<div class="texts-list">
<h3>Созданные тексты ({texts.length})</h3>
<div class="texts-container">
{#each texts as textItem (textItem.id)}
<div class="text-item">
<input
type="text"
value={textItem.text}
oninput={(e) => handleUpdateText(textItem.id, e.currentTarget.value)}
class="text-edit-input"
maxlength={TYPOGRAPHY.MAX_TEXT_LENGTH}
/>
<Button variant="danger" size="sm" onclick={() => handleDeleteText(textItem.id)} aria-label="Удалить текст">
×
</Button>
</div>
{/each}
</div>
</div>
{/if}
<div class="common-settings-section">
<h3>Общие настройки текста</h3>
<p class="settings-note">Настройки применятся ко всем создаваемым панелям</p>
<div class="settings-grid">
<div class="control-group">
<label>
Размер шрифта:
<input
type="range"
min={TYPOGRAPHY.FONT_SIZE_MIN}
max={TYPOGRAPHY.FONT_SIZE_MAX}
step="1"
value={commonTextSettings.fontSize}
oninput={(e: Event) => {
updateAllTextSettings({ fontSize: parseInt((e.target as HTMLInputElement).value) });
}}
/>
<span class="value-display">{commonTextSettings.fontSize}px</span>
</label>
</div>
<div class="control-group">
<label>
Шрифт:
<select
value={commonTextSettings.fontFamily}
onchange={(e) => {
updateAllTextSettings({ fontFamily: (e.target as HTMLSelectElement).value });
}}
>
{#each availableFonts as font}
<option value={font}>{font}</option>
{/each}
</select>
</label>
</div>
<div class="control-group">
<label>
Цвет:
<input
type="color"
value={commonTextSettings.color}
oninput={(e) => {
updateAllTextSettings({ color: (e.target as HTMLInputElement).value });
}}
/>
</label>
</div>
<div class="control-group">
<label>
Выравнивание:
<div class="alignment-buttons">
<button
class="align-btn {commonTextSettings.textAlign === 'left' ? 'active' : ''}"
onclick={() => {
updateAllTextSettings({ textAlign: "left" as TextAlign });
}}
aria-label="Выровнять по левому краю"
>
</button>
<button
class="align-btn {commonTextSettings.textAlign === 'center' ? 'active' : ''}"
onclick={() => {
updateAllTextSettings({ textAlign: "center" as TextAlign });
}}
aria-label="Выровнять по центру"
>
</button>
<button
class="align-btn {commonTextSettings.textAlign === 'right' ? 'active' : ''}"
onclick={() => {
updateAllTextSettings({ textAlign: "right" as TextAlign });
}}
aria-label="Выровнять по правому краю"
>
</button>
</div>
</label>
</div>
<div class="control-group">
<label>
Боковые отступы:
<input
type="range"
min="0"
max={TYPOGRAPHY.PADDING_X_MAX}
step="1"
value={commonTextSettings.paddingX}
oninput={(e) => {
updateAllTextSettings({ paddingX: parseInt((e.target as HTMLInputElement).value) });
}}
/>
<span class="value-display">{commonTextSettings.paddingX}px</span>
</label>
</div>
<div class="control-group">
<label>
Смещение от центра:
<input
type="range"
min={TYPOGRAPHY.VERTICAL_OFFSET_MIN}
max={TYPOGRAPHY.VERTICAL_OFFSET_MAX}
step="1"
value={commonTextSettings.verticalOffset}
oninput={(e) => {
updateAllTextSettings({ verticalOffset: parseInt((e.target as HTMLInputElement).value) });
}}
/>
<span class="value-display"
>{(commonTextSettings.verticalOffset ?? 0) > 0 ? "+" : ""}{commonTextSettings.verticalOffset ?? 0}px</span
>
</label>
</div>
</div>
</div>
</div>
<style>
.text-manager {
display: flex;
flex-direction: column;
gap: 1rem;
width: 100%;
}
.common-settings-section {
background: #f8f9fa;
border: 2px solid #e9ecef;
border-radius: 8px;
padding: 1rem;
margin-bottom: 1rem;
}
.common-settings-section h3 {
margin: 0 0 0.75rem 0;
color: #333;
font-size: 1rem;
font-weight: 500;
}
.settings-note {
margin: 0 0 1rem 0;
color: #666;
font-size: 0.875rem;
font-style: italic;
}
.settings-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
}
.alignment-buttons {
display: flex;
gap: 0.25rem;
}
.align-btn {
padding: 0.5rem 0.75rem;
border: 1px solid #ddd;
background: white;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
font-size: 1rem;
}
.align-btn:hover {
background: #f0f0f0;
border-color: #007bff;
}
.align-btn.active {
background: #007bff;
color: white;
border-color: #007bff;
}
.add-text-section {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-bottom: 1rem;
}
.texts-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-bottom: 1.5rem;
}
.texts-list h3 {
margin: 0;
color: #333;
font-size: 1rem;
font-weight: 600;
}
.texts-container {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.text-item {
display: flex;
gap: 0.5rem;
align-items: center;
}
.text-edit-input {
flex: 1;
padding: 0.5rem 0.75rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 0.95rem;
}
.text-edit-input:focus {
outline: none;
border-color: #007bff;
}
.input-group {
display: flex;
gap: 0.5rem;
}
.text-input {
flex: 1;
padding: 0.6rem 0.75rem;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 0.95rem;
}
.text-input:focus {
outline: none;
border-color: #007bff;
}
.error-message {
background: #ffebee;
color: #c62828;
padding: 0.6rem 0.75rem;
border-radius: 6px;
border: 1px solid #ffcdd2;
font-size: 0.9rem;
}
.text-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 0.75rem;
border: 2px solid #e9ecef;
border-radius: 6px;
background: white;
transition: all 0.2s ease;
}
.text-item:hover {
border-color: #007bff;
background: #f0f8ff;
}
.control-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.control-group label {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.9rem;
color: #666;
}
.control-group input[type="range"] {
flex: 1;
cursor: pointer;
}
.control-group input[type="color"] {
width: 40px;
height: 40px;
border: none;
border-radius: 4px;
cursor: pointer;
padding: 0;
}
.value-display {
min-width: 40px;
text-align: right;
font-weight: 500;
color: #333;
}
</style>
-33
View File
@@ -1,33 +0,0 @@
<script lang="ts">
import type { TextItem } from "$lib/types/panel";
import TextManager from "./TextManager.svelte";
interface Props {
texts: Array<{ id: string; text: string }>;
onTextAdd: (text: string, settings?: Partial<TextItem>) => void;
onTextUpdate: (id: string, text: string) => void;
onTextDelete: (id: string) => void;
}
let { texts, onTextAdd, onTextUpdate, onTextDelete }: Props = $props();
</script>
<div class="text-section">
<h2>Добавьте тексты для панелей</h2>
<TextManager {onTextAdd} {onTextUpdate} {onTextDelete} {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>
-142
View File
@@ -1,142 +0,0 @@
<script lang="ts">
type ButtonVariant = "primary" | "secondary" | "danger";
type ButtonSize = "sm" | "md" | "lg";
type ButtonType = "button" | "submit" | "reset";
interface Props {
variant?: ButtonVariant;
size?: ButtonSize;
disabled?: boolean;
type?: ButtonType;
fullWidth?: boolean;
loading?: boolean;
class?: string;
children?: any;
onclick?: (event: MouseEvent) => void;
[key: string]: any;
}
let {
variant = "primary",
size = "md",
disabled = false,
type = "button",
fullWidth = false,
loading = false,
class: className = "",
children,
...restProps
}: Props = $props();
</script>
<button
{type}
{disabled}
class:btn-primary={variant === "primary"}
class:btn-secondary={variant === "secondary"}
class:btn-danger={variant === "danger"}
class:btn-sm={size === "sm"}
class:btn-md={size === "md"}
class:btn-lg={size === "lg"}
class:btn-full={fullWidth}
class:btn-loading={loading}
class={className}
{...restProps}
>
{#if loading}
<span class="btn-spinner"></span>
{/if}
{@render children?.()}
</button>
<style>
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
white-space: nowrap;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Sizes */
.btn-sm {
padding: 0.5rem 1rem;
font-size: 0.875rem;
}
.btn-md {
padding: 0.75rem 1.5rem;
font-size: 1rem;
}
.btn-lg {
padding: 1rem 2rem;
font-size: 1.125rem;
}
.btn-full {
width: 100%;
}
/* Variants */
.btn-primary {
background: #007bff;
color: white;
}
.btn-primary:hover:not(:disabled) {
background: #0056b3;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover:not(:disabled) {
background: #545b62;
}
.btn-danger {
background: #dc3545;
color: white;
}
.btn-danger:hover:not(:disabled) {
background: #c82333;
}
/* Loading state */
.btn-loading {
position: relative;
pointer-events: none;
}
.btn-spinner {
display: inline-block;
width: 1rem;
height: 1rem;
border: 2px solid currentColor;
border-right-color: transparent;
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>
-103
View File
@@ -1,103 +0,0 @@
<script lang="ts">
type IconButtonVariant = "primary" | "secondary" | "danger";
type IconButtonSize = "sm" | "md" | "lg";
interface Props {
variant?: IconButtonVariant;
size?: IconButtonSize;
disabled?: boolean;
ariaLabel?: string;
class?: string;
children?: any;
onclick?: (event: MouseEvent) => void;
[key: string]: any;
}
let {
variant = "secondary",
size = "md",
disabled = false,
ariaLabel = "",
class: className = "",
children,
...restProps
}: Props = $props();
</script>
<button
{disabled}
aria-label={ariaLabel}
class:icon-btn-primary={variant === "primary"}
class:icon-btn-secondary={variant === "secondary"}
class:icon-btn-danger={variant === "danger"}
class:icon-btn-sm={size === "sm"}
class:icon-btn-md={size === "md"}
class:icon-btn-lg={size === "lg"}
class={className}
{...restProps}
>
{@render children?.()}
</button>
<style>
.icon-btn {
display: inline-flex;
align-items: center;
justify-content: center;
border: none;
cursor: pointer;
transition: all 0.2s ease;
background: none;
padding: 0.25rem 0.5rem;
font-size: 1.2rem;
}
.icon-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Sizes */
.icon-btn-sm {
padding: 0.125rem 0.25rem;
font-size: 1rem;
}
.icon-btn-md {
padding: 0.25rem 0.5rem;
font-size: 1.2rem;
}
.icon-btn-lg {
padding: 0.375rem 0.75rem;
font-size: 1.5rem;
}
/* Variants */
.icon-btn-primary {
color: #007bff;
}
.icon-btn-primary:hover:not(:disabled) {
color: #0056b3;
transform: scale(1.1);
}
.icon-btn-secondary {
color: #666;
}
.icon-btn-secondary:hover:not(:disabled) {
color: #007bff;
transform: scale(1.1);
}
.icon-btn-danger {
color: #dc3545;
}
.icon-btn-danger:hover:not(:disabled) {
color: #c82333;
transform: scale(1.1);
}
</style>
-118
View File
@@ -1,107 +1,6 @@
<script lang="ts">
import type { Panel, TextItem } from "$lib/types/panel";
import type { Stage as KonvaStage } from "konva/lib/Stage";
import { onMount } from "svelte";
import { exportService } from "../services/exportService";
import { imageService } from "../services/imageService";
import { panelService } from "../services/panelService";
import { textSettingsStore } from "../stores/panelStore";
import PanelBar from "$components/layout/PanelBar.svelte";
import TextBar from "$components/layout/TextBar.svelte";
let uploadedImage = $state<string | undefined>(undefined);
let panels = $state<Panel[]>([]);
let texts = $state<Array<{ id: string; text: string }>>([]);
let backgroundImage = $state<string | undefined>(undefined);
let textSettings = $derived($textSettingsStore);
let previousSettings = $state<string>("");
$effect(() => {
const currentSettings = textSettings;
const settingsString = JSON.stringify(currentSettings);
if (settingsString !== previousSettings) {
const updatedPanels = panels.map((panel) => ({
...panel,
text: { ...panel.text, ...currentSettings },
}));
Promise.resolve().then(() => {
if (panels.length > 0) {
panels = updatedPanels;
}
});
previousSettings = settingsString;
}
});
onMount(async () => {
const defaultTexts = [
{ id: crypto.randomUUID(), text: "About" },
{ id: crypto.randomUUID(), text: "Links" },
];
texts = defaultTexts;
try {
const loadedBackground = await imageService.loadDefaultBackground();
backgroundImage = loadedBackground;
} catch (error) {
console.error("Failed to load default background");
exportService.setErrorMessage("Не удалось загрузить фоновое изображение по умолчанию");
}
const initialPanels = panelService.updatePanelsFromTexts(texts, [], backgroundImage || "");
panels = initialPanels;
});
function handleImageUpload(image: string) {
uploadedImage = image;
imageService.handleImageUpload(image);
}
function handleUploadNewImage() {
imageService.handleUploadNewImage();
}
function handleCropComplete(croppedImage: string) {
backgroundImage = croppedImage;
uploadedImage = undefined;
imageService.handleCropComplete(croppedImage);
panels = panelService.updatePanelsBackground(panels, croppedImage);
panels = panelService.updatePanelsFromTexts(texts, panels, croppedImage);
}
function handleCropCancel() {
uploadedImage = undefined;
imageService.handleCropCancel();
}
function handleAddText(text: string, settings?: Partial<TextItem>) {
texts = panelService.addText(texts, text);
panels = panelService.updatePanelsFromTexts(texts, panels, backgroundImage!, settings);
}
function handleUpdateText(id: string, newText: string) {
texts = panelService.updateText(texts, id, newText);
panels = panelService.updatePanelsFromTexts(texts, panels, backgroundImage!);
}
function handleDeleteText(id: string) {
texts = panelService.deleteText(texts, id);
panels = panelService.updatePanelsFromTexts(texts, panels, backgroundImage!);
}
async function handleDownload(panel: Panel, konvaStage: KonvaStage) {
await exportService.handleDownload(panel, konvaStage);
}
async function handleDownloadAll() {
await exportService.handleDownloadAll(panels);
}
</script>
<div class="main-grid">
@@ -109,23 +8,6 @@
<PanelBar />
</div>
<!-- <AppContainer
errorMessage={exportService.getErrorMessage() || undefined}
{uploadedImage}
{texts}
{backgroundImage}
{panels}
onUploadNewImage={handleUploadNewImage}
onImageUpload={handleImageUpload}
onCropComplete={handleCropComplete}
onCropCancel={handleCropCancel}
onTextAdd={handleAddText}
onTextUpdate={handleUpdateText}
onTextDelete={handleDeleteText}
onDownload={handleDownload}
onDownloadAll={handleDownloadAll}
/> -->
<style>
.main-grid {
display: grid;