refactor: pick out magic numbers to constants

This commit is contained in:
2026-02-05 00:49:47 +05:00
parent dafc1bbfd2
commit a19d94a744
9 changed files with 148 additions and 60 deletions
+8 -7
View File
@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import Button from "$components/ui/Button.svelte"; 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 type { Panel, TextItem } from "$lib/types/panel";
import { setCurrentStep } from "$stores/uiStore"; import { setCurrentStep } from "$stores/uiStore";
import type { Stage as KonvaStage } from "konva/lib/Stage"; import type { Stage as KonvaStage } from "konva/lib/Stage";
@@ -23,7 +24,7 @@
}); });
function handleUploadNewImage() { function handleUploadNewImage() {
setCurrentStep("upload"); setCurrentStep(UI_SETTINGS.STEPS.UPLOAD);
} }
$effect(() => { $effect(() => {
@@ -63,7 +64,7 @@
} }
function getTextPosition(textItem: TextItem) { function getTextPosition(textItem: TextItem) {
const panelWidth = 320; const panelWidth = PANEL_SETTINGS.PANEL_WIDTH;
const paddingX = textItem.paddingX || 20; const paddingX = textItem.paddingX || 20;
const verticalOffset = textItem.verticalOffset || 0; const verticalOffset = textItem.verticalOffset || 0;
const centerY = panel.height / 2 + verticalOffset; const centerY = panel.height / 2 + verticalOffset;
@@ -95,10 +96,10 @@
<div class="preview-sections"> <div class="preview-sections">
<div class="preview-section"> <div class="preview-section">
<div class="canvas-container"> <div class="canvas-container">
<Stage width={320} height={panel.height} bind:this={stageComponent}> <Stage width={PANEL_SETTINGS.PANEL_WIDTH} height={panel.height} bind:this={stageComponent}>
<Layer> <Layer>
{#if backgroundImage} {#if backgroundImage}
<Image image={backgroundImage} width={320} height={panel.height} /> <Image image={backgroundImage} width={PANEL_SETTINGS.PANEL_WIDTH} height={panel.height} />
{/if} {/if}
</Layer> </Layer>
@@ -108,12 +109,12 @@
<Text <Text
text={panel.text.text} text={panel.text.text}
fontSize={panel.text.fontSize || 24} fontSize={panel.text.fontSize || TYPOGRAPHY.FONT_SIZE_DEFAULT}
fill={panel.text.color || "#ffffff"} fill={panel.text.color || TYPOGRAPHY.TEXT_COLOR_DEFAULT}
fontFamily={panel.text.fontFamily || "Arial"} fontFamily={panel.text.fontFamily || "Arial"}
x={textPosition.x} x={textPosition.x}
y={textPosition.y} y={textPosition.y}
align={panel.text.textAlign || "center"} align={panel.text.textAlign || TYPOGRAPHY.TEXT_ALIGN_CENTER}
width={300} width={300}
/> />
{/if} {/if}
+11 -19
View File
@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import Button from "$components/ui/Button.svelte"; import Button from "$components/ui/Button.svelte";
import { TYPOGRAPHY } from "$lib/constants";
import type { TextAlign, TextItem } from "$lib/types/panel"; import type { TextAlign, TextItem } from "$lib/types/panel";
import { textSettingsStore, updateAllTextSettings } from "$stores/panelStore"; import { textSettingsStore, updateAllTextSettings } from "$stores/panelStore";
@@ -17,16 +18,7 @@
let commonTextSettings = $derived($textSettingsStore); let commonTextSettings = $derived($textSettingsStore);
const availableFonts = [ const availableFonts = TYPOGRAPHY.FONT_FAMILIES;
"Arial",
"Verdana",
"Georgia",
"Times New Roman",
"Courier New",
"Impact",
"Comic Sans MS",
"Trebuchet MS",
];
function handleAddText() { function handleAddText() {
if (!newText.trim()) { if (!newText.trim()) {
@@ -34,8 +26,8 @@
return; return;
} }
if (newText.length > 100) { if (newText.length > TYPOGRAPHY.MAX_TEXT_LENGTH) {
errorMessage = "Текст не должен превышать 100 символов"; errorMessage = `Текст не должен превышать ${TYPOGRAPHY.MAX_TEXT_LENGTH} символов`;
return; return;
} }
@@ -75,7 +67,7 @@
bind:value={newText} bind:value={newText}
placeholder="Введите текст для панели (например: links, about me, projects...)" placeholder="Введите текст для панели (например: links, about me, projects...)"
class="text-input" class="text-input"
maxlength="100" maxlength={TYPOGRAPHY.MAX_TEXT_LENGTH}
onkeypress={handleKeyPress} onkeypress={handleKeyPress}
/> />
<Button variant="primary" onclick={handleAddText}>Добавить</Button> <Button variant="primary" onclick={handleAddText}>Добавить</Button>
@@ -98,7 +90,7 @@
value={textItem.text} value={textItem.text}
oninput={(e) => handleUpdateText(textItem.id, e.currentTarget.value)} oninput={(e) => handleUpdateText(textItem.id, e.currentTarget.value)}
class="text-edit-input" class="text-edit-input"
maxlength="100" maxlength={TYPOGRAPHY.MAX_TEXT_LENGTH}
/> />
<Button variant="danger" size="sm" onclick={() => handleDeleteText(textItem.id)} aria-label="Удалить текст"> <Button variant="danger" size="sm" onclick={() => handleDeleteText(textItem.id)} aria-label="Удалить текст">
× ×
@@ -119,8 +111,8 @@
Размер шрифта: Размер шрифта:
<input <input
type="range" type="range"
min="10" min={TYPOGRAPHY.FONT_SIZE_MIN}
max="72" max={TYPOGRAPHY.FONT_SIZE_MAX}
step="1" step="1"
value={commonTextSettings.fontSize} value={commonTextSettings.fontSize}
oninput={(e: Event) => { oninput={(e: Event) => {
@@ -201,7 +193,7 @@
<input <input
type="range" type="range"
min="0" min="0"
max="50" max={TYPOGRAPHY.PADDING_X_MAX}
step="1" step="1"
value={commonTextSettings.paddingX} value={commonTextSettings.paddingX}
oninput={(e) => { oninput={(e) => {
@@ -217,8 +209,8 @@
Смещение от центра: Смещение от центра:
<input <input
type="range" type="range"
min="-50" min={TYPOGRAPHY.VERTICAL_OFFSET_MIN}
max="50" max={TYPOGRAPHY.VERTICAL_OFFSET_MAX}
step="1" step="1"
value={commonTextSettings.verticalOffset} value={commonTextSettings.verticalOffset}
oninput={(e) => { oninput={(e) => {
+12 -7
View File
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { PANEL_SETTINGS, TYPOGRAPHY } from "$lib/constants";
import type { TextItem } from "$lib/types/panel"; import type { TextItem } from "$lib/types/panel";
import { panelStore } from "$stores/panelStore"; import { panelStore } from "$stores/panelStore";
import { Image, Layer, Stage, Text } from "svelte-konva"; import { Image, Layer, Stage, Text } from "svelte-konva";
@@ -28,17 +29,17 @@
} }
function getTextPosition() { function getTextPosition() {
const panelWidth = 320; const panelWidth = PANEL_SETTINGS.PANEL_WIDTH;
const paddingX = textItem.paddingX || 0; const paddingX = textItem.paddingX || 0;
const verticalOffset = textItem.verticalOffset || 0; const verticalOffset = textItem.verticalOffset || 0;
const centerY = height / 2 + verticalOffset; const centerY = height / 2 + verticalOffset;
switch (textItem.textAlign) { switch (textItem.textAlign) {
case "left": case TYPOGRAPHY.TEXT_ALIGN_LEFT:
return { x: paddingX, y: centerY }; return { x: paddingX, y: centerY };
case "right": case TYPOGRAPHY.TEXT_ALIGN_RIGHT:
return { x: panelWidth - paddingX, y: centerY }; return { x: panelWidth - paddingX, y: centerY };
case "center": case TYPOGRAPHY.TEXT_ALIGN_CENTER:
default: default:
return { x: panelWidth / 2, y: centerY }; return { x: panelWidth / 2, y: centerY };
} }
@@ -50,9 +51,9 @@
<div class="text-preview"> <div class="text-preview">
{#if bgImage} {#if bgImage}
<div class="canvas-container"> <div class="canvas-container">
<Stage width={320} {height}> <Stage width={PANEL_SETTINGS.PANEL_WIDTH} {height}>
<Layer> <Layer>
<Image image={bgImage} width={320} {height} /> <Image image={bgImage} width={PANEL_SETTINGS.PANEL_WIDTH} {height} />
<Text <Text
text={textItem.text} text={textItem.text}
fontSize={textItem.fontSize} fontSize={textItem.fontSize}
@@ -62,7 +63,11 @@
y={textPosition.y} y={textPosition.y}
align={textItem.textAlign} align={textItem.textAlign}
width={320 - textItem.paddingX * 2} width={320 - textItem.paddingX * 2}
offsetX={textItem.textAlign === "center" ? 0 : textItem.textAlign === "right" ? 0 : 0} offsetX={textItem.textAlign === TYPOGRAPHY.TEXT_ALIGN_CENTER
? 0
: textItem.textAlign === TYPOGRAPHY.TEXT_ALIGN_RIGHT
? 0
: 0}
/> />
</Layer> </Layer>
</Stage> </Stage>
+85
View File
@@ -0,0 +1,85 @@
// Application Constants
// This file contains magic numbers and strings used in JavaScript/TypeScript logic
// ===== PANEL SETTINGS =====
export const PANEL_SETTINGS = {
// Storage
STORAGE_KEY: "twitch-panels",
MAX_PANELS_COUNT: 50,
// Dimensions
PANEL_WIDTH: 320,
PANEL_HEIGHT_DEFAULT: 100,
PANEL_HEIGHT_MAX: 1000,
// Default values
DEFAULT_BACKGROUND_IMAGE: "/backgrounds/b1.jpg",
} as const;
// ===== TYPOGRAPHY =====
export const TYPOGRAPHY = {
// Font families
FONT_FAMILY_DEFAULT: "Arial",
FONT_FAMILIES: [
"Arial",
"Verdana",
"Georgia",
"Times New Roman",
"Courier New",
"Impact",
"Comic Sans MS",
"Trebuchet MS",
],
// Font sizes for range inputs
FONT_SIZE_MIN: 10,
FONT_SIZE_MAX: 72,
FONT_SIZE_DEFAULT: 18,
// Text alignment
TEXT_ALIGN_LEFT: "left",
TEXT_ALIGN_CENTER: "center",
TEXT_ALIGN_RIGHT: "right",
// Text limits
MAX_TEXT_LENGTH: 100,
// Padding for range inputs
PADDING_X_DEFAULT: 10,
PADDING_X_LARGE: 20,
PADDING_X_MAX: 50,
// Vertical offset for range inputs
VERTICAL_OFFSET_MAX: 50,
VERTICAL_OFFSET_MIN: -50,
// Colors
TEXT_COLOR_DEFAULT: "#ffffff",
TEXT_COLOR_ERROR: "#c62828",
} as const;
// ===== IMAGE SETTINGS =====
export const IMAGE_SETTINGS = {
// File sizes
MAX_FILE_SIZE: 10 * 1024 * 1024, // 10MB
// Supported formats
SUPPORTED_FORMATS: ["image/jpeg", "image/jpg", "image/png", "image/webp", "image/gif"] as const,
} as const;
// ===== UI SETTINGS =====
export const UI_SETTINGS = {
// Steps
STEPS: {
UPLOAD: "upload",
CROP: "crop",
TEXT: "text",
} as const,
} as const;
// ===== ERROR HANDLING =====
export const ERROR_HANDLING = {
// Retry settings
MAX_RETRIES: 3,
RETRY_DELAY_MS: 1000,
} as const;
+4 -3
View File
@@ -1,4 +1,5 @@
import { AppError, type ErrorType } from "../types/errors"; import { ERROR_HANDLING } from "../constants";
import { AppError } from "../types/errors";
export function handleError(error: unknown, defaultMessage: string = "Произошла ошибка"): string { export function handleError(error: unknown, defaultMessage: string = "Произошла ошибка"): string {
if (error instanceof AppError) { if (error instanceof AppError) {
@@ -38,8 +39,8 @@ export function logError(error: unknown, context?: string): void {
export function retryOperation<T>( export function retryOperation<T>(
operation: () => Promise<T>, operation: () => Promise<T>,
maxRetries: number = 3, maxRetries: number = ERROR_HANDLING.MAX_RETRIES,
delayMs: number = 1000, delayMs: number = ERROR_HANDLING.RETRY_DELAY_MS,
): Promise<T> { ): Promise<T> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let attempt = 0; let attempt = 0;
+4 -3
View File
@@ -1,7 +1,8 @@
import { IMAGE_SETTINGS } from "../constants";
import { ImageError } from "../types/errors"; import { ImageError } from "../types/errors";
export const MAX_FILE_SIZE = 10 * 1024 * 1024; export const MAX_FILE_SIZE = IMAGE_SETTINGS.MAX_FILE_SIZE;
export const SUPPORTED_FORMATS = ["image/jpeg", "image/jpg", "image/png", "image/webp", "image/gif"]; export const SUPPORTED_FORMATS = IMAGE_SETTINGS.SUPPORTED_FORMATS;
export type ValidationResult = export type ValidationResult =
| { | {
@@ -23,7 +24,7 @@ export function validateFileSize(file: File): ValidationResult {
} }
export function validateFileType(file: File): ValidationResult { export function validateFileType(file: File): ValidationResult {
if (!SUPPORTED_FORMATS.includes(file.type)) { if (!SUPPORTED_FORMATS.includes(file.type as any)) {
return { return {
isValid: false, isValid: false,
error: `Неподдерживаемый формат. Допустимые форматы: ${SUPPORTED_FORMATS.join(", ")}`, error: `Неподдерживаемый формат. Допустимые форматы: ${SUPPORTED_FORMATS.join(", ")}`,
+4 -3
View File
@@ -1,8 +1,9 @@
import { PANEL_SETTINGS } from "../constants";
import type { Panel } from "../types/panel"; import type { Panel } from "../types/panel";
import { handleError, logError } from "./errorHandler"; import { handleError, logError } from "./errorHandler";
const STORAGE_KEY = "twitch-panels"; const STORAGE_KEY = PANEL_SETTINGS.STORAGE_KEY;
const MAX_PANELS = 50; const MAX_PANELS = PANEL_SETTINGS.MAX_PANELS_COUNT;
export interface StorageResult { export interface StorageResult {
success: boolean; success: boolean;
@@ -109,7 +110,7 @@ export class PanelStorage {
Array.isArray(panel.texts) && Array.isArray(panel.texts) &&
typeof panel.height === "number" && typeof panel.height === "number" &&
panel.height > 0 && panel.height > 0 &&
panel.height <= 1000 && panel.height <= PANEL_SETTINGS.PANEL_HEIGHT_MAX &&
panel.createdAt instanceof Date && panel.createdAt instanceof Date &&
panel.updatedAt instanceof Date panel.updatedAt instanceof Date
); );
+2 -1
View File
@@ -1,3 +1,4 @@
import { PANEL_SETTINGS } from "../lib/constants";
import type { Panel, TextItem } from "../lib/types/panel"; import type { Panel, TextItem } from "../lib/types/panel";
import { createPanelFromText, updatePanelText } from "../stores/panelStore"; import { createPanelFromText, updatePanelText } from "../stores/panelStore";
@@ -54,7 +55,7 @@ export class PanelService {
if (existingPanel) { if (existingPanel) {
return updatePanelText(existingPanel, textItem.text); return updatePanelText(existingPanel, textItem.text);
} }
return createPanelFromText(backgroundImage, textItem.text, 100, textSettings); return createPanelFromText(backgroundImage, textItem.text, PANEL_SETTINGS.PANEL_HEIGHT_DEFAULT, textSettings);
}); });
} }
+18 -17
View File
@@ -1,15 +1,16 @@
import { writable, type Writable } from "svelte/store"; import { writable, type Writable } from "svelte/store";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import { PANEL_SETTINGS, TYPOGRAPHY } from "../lib/constants";
import { type Panel, type TextItem } from "../lib/types/panel"; import { type Panel, type TextItem } from "../lib/types/panel";
export const panelStore: Writable<Panel | undefined> = writable(undefined); export const panelStore: Writable<Panel | undefined> = writable(undefined);
export const textSettingsStore = writable<Partial<TextItem>>({ export const textSettingsStore = writable<Partial<TextItem>>({
fontSize: 18, fontSize: TYPOGRAPHY.FONT_SIZE_DEFAULT,
fontFamily: "Arial", fontFamily: TYPOGRAPHY.FONT_FAMILY_DEFAULT,
color: "#ffffff", color: TYPOGRAPHY.TEXT_COLOR_DEFAULT,
textAlign: "left", textAlign: TYPOGRAPHY.TEXT_ALIGN_LEFT,
paddingX: 10, paddingX: TYPOGRAPHY.PADDING_X_DEFAULT,
verticalOffset: 0, verticalOffset: 0,
}); });
@@ -17,15 +18,15 @@ export const updateAllTextSettings = (settings: Partial<TextItem>) => {
textSettingsStore.update((current) => ({ ...current, ...settings })); textSettingsStore.update((current) => ({ ...current, ...settings }));
}; };
export const createEmptyPanel = (height: number = 100): Panel => { export const createEmptyPanel = (height: number = PANEL_SETTINGS.PANEL_HEIGHT_DEFAULT): Panel => {
const defaultText: TextItem = { const defaultText: TextItem = {
id: uuidv4(), id: uuidv4(),
text: "", text: "",
fontSize: 18, fontSize: TYPOGRAPHY.FONT_SIZE_DEFAULT,
fontFamily: "Arial", fontFamily: TYPOGRAPHY.FONT_FAMILY_DEFAULT,
color: "#ffffff", color: TYPOGRAPHY.TEXT_COLOR_DEFAULT,
textAlign: "center", textAlign: TYPOGRAPHY.TEXT_ALIGN_CENTER,
paddingX: 10, paddingX: TYPOGRAPHY.PADDING_X_DEFAULT,
verticalOffset: 0, verticalOffset: 0,
}; };
@@ -62,17 +63,17 @@ export const updateTextProperties = (panel: Panel, updates: Partial<TextItem>):
export const createPanelFromText = ( export const createPanelFromText = (
backgroundImage: string, backgroundImage: string,
text: string, text: string,
height: number = 100, height: number = PANEL_SETTINGS.PANEL_HEIGHT_DEFAULT,
textSettings?: Partial<TextItem>, textSettings?: Partial<TextItem>,
): Panel => { ): Panel => {
const newText: TextItem = { const newText: TextItem = {
id: uuidv4(), id: uuidv4(),
text, text,
fontSize: textSettings?.fontSize ?? 18, fontSize: textSettings?.fontSize ?? TYPOGRAPHY.FONT_SIZE_DEFAULT,
fontFamily: textSettings?.fontFamily ?? "Arial", fontFamily: textSettings?.fontFamily ?? TYPOGRAPHY.FONT_FAMILY_DEFAULT,
color: textSettings?.color ?? "#ffffff", color: textSettings?.color ?? TYPOGRAPHY.TEXT_COLOR_DEFAULT,
textAlign: textSettings?.textAlign ?? "center", textAlign: textSettings?.textAlign ?? TYPOGRAPHY.TEXT_ALIGN_CENTER,
paddingX: textSettings?.paddingX ?? 10, paddingX: textSettings?.paddingX ?? TYPOGRAPHY.PADDING_X_DEFAULT,
verticalOffset: textSettings?.verticalOffset ?? 0, verticalOffset: textSettings?.verticalOffset ?? 0,
}; };