refactor: change null to undefined

This commit is contained in:
2026-02-04 12:50:38 +05:00
parent 2797fea079
commit dfa6c40743
21 changed files with 89 additions and 112 deletions
+8 -4
View File
@@ -40,6 +40,7 @@ interface ImageUploadProps {
- **Error Handling**: Invalid crop areas ✅ - **Error Handling**: Invalid crop areas ✅
**Implementation Details:** **Implementation Details:**
- Integrated cropperjs v2.1.0 with Web Components - Integrated cropperjs v2.1.0 with Web Components
- Used `$toCanvas()` method to get HTMLCanvasElement - Used `$toCanvas()` method to get HTMLCanvasElement
- Implemented proper error handling with user-friendly messages - Implemented proper error handling with user-friendly messages
@@ -147,7 +148,7 @@ interface Panel {
} }
interface PanelManagerProps { interface PanelManagerProps {
currentPanel: Panel | null; currentPanel: Panel | undefined;
onPanelUpdate: (panel: Panel) => void; onPanelUpdate: (panel: Panel) => void;
} }
@@ -255,11 +256,11 @@ interface ImageUploadResult {
```typescript ```typescript
// Core stores // Core stores
export const panelStore = writable<Panel | null>(null); export const panelStore = writable<Panel | undefined>(undefined);
export const uiStore = writable({ export const uiStore = writable({
isLoading: false, isLoading: false,
error: null as string | null, error: string | undefined,
currentStep: "upload" as "upload" | "crop" | "text" | "preview", currentStep: "upload" | "crop" | "text" | "preview",
}); });
``` ```
@@ -359,11 +360,13 @@ _Estimated Duration: 5 weeks_
**Challenge:** cropperjs v2.x uses Web Components API which differs significantly from v1.x **Challenge:** cropperjs v2.x uses Web Components API which differs significantly from v1.x
**Solution:** **Solution:**
1. Used `getCropperCanvas()` to get `<cropper-canvas>` Web Component 1. Used `getCropperCanvas()` to get `<cropper-canvas>` Web Component
2. Called `$toCanvas()` method to convert to HTMLCanvasElement 2. Called `$toCanvas()` method to convert to HTMLCanvasElement
3. Applied `toDataURL()` on the resulting canvas to get base64 image 3. Applied `toDataURL()` on the resulting canvas to get base64 image
**Code Example:** **Code Example:**
```typescript ```typescript
const cropperCanvasElement = cropper.getCropperCanvas(); const cropperCanvasElement = cropper.getCropperCanvas();
const canvas = await cropperCanvasElement.$toCanvas({ const canvas = await cropperCanvasElement.$toCanvas({
@@ -375,6 +378,7 @@ const croppedImage = canvas.toDataURL("image/png");
``` ```
**Key Points:** **Key Points:**
- cropperjs v2.x methods return Web Components, not standard DOM elements - cropperjs v2.x methods return Web Components, not standard DOM elements
- `$toCanvas()` is an async method that returns HTMLCanvasElement - `$toCanvas()` is an async method that returns HTMLCanvasElement
- Proper error handling is essential for user experience - Proper error handling is essential for user experience
+14 -14
View File
@@ -4,10 +4,10 @@
import AppContent from "./AppContent.svelte"; import AppContent from "./AppContent.svelte";
interface Props { interface Props {
errorMessage: string | null; errorMessage: string | undefined;
uploadedImage: string | null; uploadedImage: string | undefined;
texts: Array<{ id: string; text: string }>; texts: Array<{ id: string; text: string }>;
backgroundImage: string | null; backgroundImage: string | undefined;
panels: any[]; panels: any[];
onUploadNewImage: () => void; onUploadNewImage: () => void;
onImageUpload: (image: string) => void; onImageUpload: (image: string) => void;
@@ -34,12 +34,12 @@
onTextUpdate, onTextUpdate,
onTextDelete, onTextDelete,
onDownload, onDownload,
onDownloadAll onDownloadAll,
}: Props = $props(); }: Props = $props();
</script> </script>
<div class="app-container"> <div class="app-container">
<AppHeader onUploadNewImage={onUploadNewImage} /> <AppHeader {onUploadNewImage} />
<ErrorMessage {errorMessage} /> <ErrorMessage {errorMessage} />
@@ -48,15 +48,15 @@
{texts} {texts}
{backgroundImage} {backgroundImage}
{panels} {panels}
onImageUpload={onImageUpload} {onImageUpload}
onCropComplete={onCropComplete} {onCropComplete}
onCropCancel={onCropCancel} {onCropCancel}
onTextAdd={onTextAdd} {onTextAdd}
onTextUpdate={onTextUpdate} {onTextUpdate}
onTextDelete={onTextDelete} {onTextDelete}
onUploadNewImage={onUploadNewImage} {onUploadNewImage}
onDownload={onDownload} {onDownload}
onDownloadAll={onDownloadAll} {onDownloadAll}
/> />
</div> </div>
+10 -16
View File
@@ -3,9 +3,9 @@
import Sidebar from "./Sidebar.svelte"; import Sidebar from "./Sidebar.svelte";
interface Props { interface Props {
uploadedImage: string | null; uploadedImage: string | undefined;
texts: Array<{ id: string; text: string }>; texts: Array<{ id: string; text: string }>;
backgroundImage: string | null; backgroundImage: string | undefined;
panels: any[]; panels: any[];
onImageUpload: (image: string) => void; onImageUpload: (image: string) => void;
onCropComplete: (croppedImage: string) => void; onCropComplete: (croppedImage: string) => void;
@@ -31,7 +31,7 @@
onTextDelete, onTextDelete,
onUploadNewImage, onUploadNewImage,
onDownload, onDownload,
onDownloadAll onDownloadAll,
}: Props = $props(); }: Props = $props();
</script> </script>
@@ -39,20 +39,14 @@
<MainSection <MainSection
{uploadedImage} {uploadedImage}
{texts} {texts}
onImageUpload={onImageUpload} {onImageUpload}
onCropComplete={onCropComplete} {onCropComplete}
onCropCancel={onCropCancel} {onCropCancel}
onTextAdd={onTextAdd} {onTextAdd}
onTextUpdate={onTextUpdate} {onTextUpdate}
onTextDelete={onTextDelete} {onTextDelete}
/>
<Sidebar
{backgroundImage}
{panels}
onUploadNewImage={onUploadNewImage}
onDownload={onDownload}
onDownloadAll={onDownloadAll}
/> />
<Sidebar {backgroundImage} {panels} {onUploadNewImage} {onDownload} {onDownloadAll} />
</div> </div>
<style> <style>
+1 -1
View File
@@ -2,7 +2,7 @@
import { Button } from "../lib/components/ui"; import { Button } from "../lib/components/ui";
interface Props { interface Props {
backgroundImage?: string | null; backgroundImage?: string | undefined;
onUploadNewImage: () => void; onUploadNewImage: () => void;
} }
+4 -4
View File
@@ -1,14 +1,14 @@
<script lang="ts"> <script lang="ts">
interface Props { interface Props {
message: string | null; errorMessage: string | undefined;
} }
let { message }: Props = $props(); let { errorMessage }: Props = $props();
</script> </script>
{#if message} {#if errorMessage}
<div class="error-message"> <div class="error-message">
{message} {errorMessage}
</div> </div>
{/if} {/if}
+3 -3
View File
@@ -15,9 +15,9 @@
let { imageSrc, onCropComplete, onCancel }: Props = $props(); let { imageSrc, onCropComplete, onCancel }: Props = $props();
let imageElement: HTMLImageElement; let imageElement: HTMLImageElement;
let cropper: CropperJS | null = null; let cropper: CropperJS | undefined = undefined;
let isProcessing = $state(false); let isProcessing = $state(false);
let errorMessage = $state<string | null>(null); let errorMessage = $state<string | undefined>(undefined);
let imageService = new ImageService(); let imageService = new ImageService();
@@ -46,7 +46,7 @@
try { try {
isProcessing = true; isProcessing = true;
errorMessage = null; errorMessage = undefined;
setLoading(true); setLoading(true);
console.log("Calling getCropperCanvas"); console.log("Calling getCropperCanvas");
+1 -1
View File
@@ -4,7 +4,7 @@
interface Props { interface Props {
currentStep: string; currentStep: string;
uploadedImage: string | null; uploadedImage: string | undefined;
onImageUpload: (image: string) => void; onImageUpload: (image: string) => void;
onCropComplete: (croppedImage: string) => void; onCropComplete: (croppedImage: string) => void;
onCropCancel: () => void; onCropCancel: () => void;
+8 -8
View File
@@ -15,15 +15,15 @@
let { onImageSelect }: Props = $props(); let { onImageSelect }: Props = $props();
let dropZone = $state<HTMLElement | null>(null); let dropZone = $state<HTMLElement | undefined>(undefined);
let fileInput = $state<HTMLInputElement | null>(null); let fileInput = $state<HTMLInputElement | undefined>(undefined);
let urlInput = $state<HTMLInputElement | null>(null); let urlInput = $state<HTMLInputElement | undefined>(undefined);
let urlForm = $state<HTMLFormElement | null>(null); let urlForm = $state<HTMLFormElement | undefined>(undefined);
let isDragOver = $state(false); let isDragOver = $state(false);
let showUrlInput = $state(false); let showUrlInput = $state(false);
let uploadedImage = $state<string | null>(null); let uploadedImage = $state<string | undefined>(undefined);
let errorMessage = $state<string | null>(null); let errorMessage = $state<string | undefined>(undefined);
const uiError = $derived($uiStore.error?.error); const uiError = $derived($uiStore.error?.error);
@@ -148,8 +148,8 @@
} }
function resetUpload() { function resetUpload() {
uploadedImage = null; uploadedImage = undefined;
errorMessage = null; errorMessage = undefined;
showUrlInput = false; showUrlInput = false;
setCurrentStep("upload"); setCurrentStep("upload");
} }
+4 -15
View File
@@ -4,7 +4,7 @@
import TextSection from "./TextSection.svelte"; import TextSection from "./TextSection.svelte";
interface Props { interface Props {
uploadedImage: string | null; uploadedImage: string | undefined;
texts: Array<{ id: string; text: string }>; texts: Array<{ id: string; text: string }>;
onImageUpload: (image: string) => void; onImageUpload: (image: string) => void;
onCropComplete: (croppedImage: string) => void; onCropComplete: (croppedImage: string) => void;
@@ -22,25 +22,14 @@
onCropCancel, onCropCancel,
onTextAdd, onTextAdd,
onTextUpdate, onTextUpdate,
onTextDelete onTextDelete,
}: Props = $props(); }: Props = $props();
</script> </script>
<div class="main-section"> <div class="main-section">
<ImageManager <ImageManager currentStep={$uiStore.currentStep} {uploadedImage} {onImageUpload} {onCropComplete} {onCropCancel} />
currentStep={$uiStore.currentStep}
{uploadedImage}
onImageUpload={onImageUpload}
onCropComplete={onCropComplete}
onCropCancel={onCropCancel}
/>
{#if $uiStore.currentStep === "text"} {#if $uiStore.currentStep === "text"}
<TextSection <TextSection {texts} {onTextAdd} {onTextUpdate} {onTextDelete} />
{texts}
onTextAdd={onTextAdd}
onTextUpdate={onTextUpdate}
onTextDelete={onTextDelete}
/>
{/if} {/if}
</div> </div>
+5 -5
View File
@@ -12,8 +12,8 @@
let { onPanelSelect, onPanelDelete }: Props = $props(); let { onPanelSelect, onPanelDelete }: Props = $props();
let panels = $state<Panel[]>([]); let panels = $state<Panel[]>([]);
let selectedPanelId = $state<string | null>(null); let selectedPanelId = $state<string | undefined>(undefined);
let errorMessage = $state<string | null>(null); let errorMessage = $state<string | undefined>(undefined);
onMount(() => { onMount(() => {
loadPanels(); loadPanels();
@@ -21,7 +21,7 @@
function loadPanels() { function loadPanels() {
try { try {
errorMessage = null; errorMessage = undefined;
panels = panelStorage.getAllPanels(); panels = panelStorage.getAllPanels();
// Если есть текущая панель в store, отмечаем её как выбранную // Если есть текущая панель в store, отмечаем её как выбранную
@@ -41,7 +41,7 @@
function handlePanelDelete(panelId: string) { function handlePanelDelete(panelId: string) {
try { try {
errorMessage = null; errorMessage = undefined;
const result = panelStorage.deletePanel(panelId); const result = panelStorage.deletePanel(panelId);
if (result.success) { if (result.success) {
@@ -49,7 +49,7 @@
// Если удалили выбранную панель, сбрасываем выбор // Если удалили выбранную панель, сбрасываем выбор
if (selectedPanelId === panelId) { if (selectedPanelId === panelId) {
selectedPanelId = null; selectedPanelId = undefined;
} }
// Уведомляем родительский компонент // Уведомляем родительский компонент
+4 -17
View File
@@ -4,33 +4,20 @@
import PanelsList from "./PanelsList.svelte"; import PanelsList from "./PanelsList.svelte";
interface Props { interface Props {
backgroundImage: string | null; backgroundImage: string | undefined;
panels: any[]; panels: any[];
onUploadNewImage: () => void; onUploadNewImage: () => void;
onDownload: (panel: any) => Promise<void>; onDownload: (panel: any) => Promise<void>;
onDownloadAll: () => Promise<void>; onDownloadAll: () => Promise<void>;
} }
let { let { backgroundImage, panels, onUploadNewImage, onDownload, onDownloadAll }: Props = $props();
backgroundImage,
panels,
onUploadNewImage,
onDownload,
onDownloadAll
}: Props = $props();
</script> </script>
<div class="sidebar"> <div class="sidebar">
{#if $uiStore.currentStep === "text"} {#if $uiStore.currentStep === "text"}
<BackgroundPreview <BackgroundPreview {backgroundImage} {onUploadNewImage} />
{backgroundImage} <PanelsList {panels} {onDownload} {onDownloadAll} />
onUploadNewImage={onUploadNewImage}
/>
<PanelsList
{panels}
onDownload={onDownload}
onDownloadAll={onDownloadAll}
/>
{/if} {/if}
</div> </div>
+3 -3
View File
@@ -11,14 +11,14 @@
let { onTextAdd, onTextUpdate, onTextDelete, texts }: Props = $props(); let { onTextAdd, onTextUpdate, onTextDelete, texts }: Props = $props();
let newText = $state(""); let newText = $state("");
let errorMessage = $state<string | null>(null); let errorMessage = $state<string | undefined>(undefined);
// Общие настройки текста для всех панелей // Общие настройки текста для всех панелей
let commonTextSettings = $state({ let commonTextSettings = $state({
fontSize: 18, fontSize: 18,
fontFamily: "Arial", fontFamily: "Arial",
color: "#ffffff", color: "#ffffff",
textAlign: "center" as "left" | "center" | "right", textAlign: "left" | "center" | "right",
paddingX: 10, paddingX: 10,
verticalOffset: 0, verticalOffset: 0,
}); });
@@ -47,7 +47,7 @@
} }
try { try {
errorMessage = null; errorMessage = undefined;
onTextAdd(newText.trim()); onTextAdd(newText.trim());
newText = ""; newText = "";
} catch (error) { } catch (error) {
+2 -1
View File
@@ -87,7 +87,7 @@ export class ImageService {
} }
// Find image item // Find image item
let imageItem: DataTransferItem | null = null; let imageItem: DataTransferItem | undefined = undefined;
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
if (items[i].type.indexOf("image") !== -1) { if (items[i].type.indexOf("image") !== -1) {
imageItem = items[i]; imageItem = items[i];
@@ -184,6 +184,7 @@ export class ImageService {
private fileToBase64(file: File): Promise<string> { private fileToBase64(file: File): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const reader = new FileReader(); const reader = new FileReader();
// TODO: look for a better way to handle errors non strings
reader.onload = () => resolve(reader.result as string); reader.onload = () => resolve(reader.result as string);
reader.onerror = () => reject(new ImageError("Ошибка чтения файла")); reader.onerror = () => reject(new ImageError("Ошибка чтения файла"));
reader.readAsDataURL(file); reader.readAsDataURL(file);
+1 -1
View File
@@ -42,7 +42,7 @@ export interface ImageCropResult {
export interface UIState { export interface UIState {
isLoading: boolean; isLoading: boolean;
error: string | null; error: string | undefined;
currentStep: "upload" | "crop" | "text" | "preview"; currentStep: "upload" | "crop" | "text" | "preview";
showCropModal: boolean; showCropModal: boolean;
showTextManager: boolean; showTextManager: boolean;
+3 -4
View File
@@ -1,4 +1,3 @@
import type { Panel } from "../types/panel"; import type { Panel } from "../types/panel";
import { ImageError } from "../types/errors"; import { ImageError } from "../types/errors";
import { handleError, logError } from "./errorHandler"; import { handleError, logError } from "./errorHandler";
@@ -72,13 +71,13 @@ export class PanelStorage {
/** /**
* Загружает панель по ID * Загружает панель по ID
*/ */
getPanelById(id: string): Panel | null { getPanelById(id: string): Panel | undefined {
try { try {
const panels = this.getAllPanels(); const panels = this.getAllPanels();
return panels.find((p) => p.id === id) || null; return panels.find((p) => p.id === id) || undefined;
} catch (error) { } catch (error) {
logError(error, "Failed to get panel by id"); logError(error, "Failed to get panel by id");
return null; return undefined;
} }
} }
+4 -4
View File
@@ -8,10 +8,10 @@
import AppContainer from "../components/AppContainer.svelte"; import AppContainer from "../components/AppContainer.svelte";
let uploadedImage = $state<string | null>(null); let uploadedImage = $state<string | undefined>(undefined);
let panels = $state<Panel[]>([]); let panels = $state<Panel[]>([]);
let texts = $state<Array<{ id: string; text: string }>>([]); let texts = $state<Array<{ id: string; text: string }>>([]);
let backgroundImage = $state<string | null>(null); let backgroundImage = $state<string | undefined>(undefined);
onMount(async () => { onMount(async () => {
try { try {
@@ -32,14 +32,14 @@
function handleCropComplete(croppedImage: string) { function handleCropComplete(croppedImage: string) {
backgroundImage = croppedImage; backgroundImage = croppedImage;
uploadedImage = null; uploadedImage = undefined;
imageService.handleCropComplete(croppedImage); imageService.handleCropComplete(croppedImage);
panels = panelService.updatePanelsBackground(panels, croppedImage); panels = panelService.updatePanelsBackground(panels, croppedImage);
panels = panelService.updatePanelsFromTexts(texts, panels, croppedImage); panels = panelService.updatePanelsFromTexts(texts, panels, croppedImage);
} }
function handleCropCancel() { function handleCropCancel() {
uploadedImage = null; uploadedImage = undefined;
imageService.handleCropCancel(); imageService.handleCropCancel();
} }
+3 -3
View File
@@ -4,7 +4,7 @@ import { setLoading } from "../stores/uiStore";
export class ExportService { export class ExportService {
private static instance: ExportService; private static instance: ExportService;
private errorMessage: string | null = null; private errorMessage: string | undefined = undefined;
private constructor() {} private constructor() {}
@@ -15,11 +15,11 @@ export class ExportService {
return ExportService.instance; return ExportService.instance;
} }
getErrorMessage(): string | null { getErrorMessage(): string | undefined {
return this.errorMessage; return this.errorMessage;
} }
setErrorMessage(message: string | null): void { setErrorMessage(message: string | undefined): void {
this.errorMessage = message; this.errorMessage = message;
} }
+1
View File
@@ -19,6 +19,7 @@ export class ImageService {
const blob = await response.blob(); const blob = await response.blob();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const reader = new FileReader(); const reader = new FileReader();
// TODO: look for a better handle non string
reader.onload = () => resolve(reader.result as string); reader.onload = () => resolve(reader.result as string);
reader.onerror = reject; reader.onerror = reject;
reader.readAsDataURL(blob); reader.readAsDataURL(blob);
+3 -3
View File
@@ -2,7 +2,7 @@ import { writable, type Writable } from "svelte/store";
import { type Panel } from "../lib/types/panel"; import { type Panel } from "../lib/types/panel";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
export const panelStore: Writable<Panel | null> = writable(null); export const panelStore: Writable<Panel | undefined> = writable(undefined);
export const createEmptyPanel = (height: number = 100): Panel => { export const createEmptyPanel = (height: number = 100): Panel => {
return { return {
@@ -30,7 +30,7 @@ export const addTextToPanel = (panel: Panel, text: string): Panel => {
fontSize: 18, fontSize: 18,
fontFamily: "Arial", fontFamily: "Arial",
color: "#ffffff", color: "#ffffff",
textAlign: "center" as const, textAlign: "center",
paddingX: 10, paddingX: 10,
verticalOffset: 0, verticalOffset: 0,
}; };
@@ -58,7 +58,7 @@ export const createPanelFromText = (backgroundImage: string, text: string, heigh
fontSize: 18, fontSize: 18,
fontFamily: "Arial", fontFamily: "Arial",
color: "#ffffff", color: "#ffffff",
textAlign: "center" as const, textAlign: "center",
paddingX: 10, paddingX: 10,
verticalOffset: 0, verticalOffset: 0,
}; };
+4 -4
View File
@@ -3,7 +3,7 @@ import { type UIState } from "../lib/types/panel";
export const uiStore: Writable<UIState> = writable({ export const uiStore: Writable<UIState> = writable({
isLoading: false, isLoading: false,
error: null, error: undefined,
currentStep: "text", currentStep: "text",
showCropModal: false, showCropModal: false,
showTextManager: false, showTextManager: false,
@@ -13,12 +13,12 @@ export const setLoading = (loading: boolean): void => {
uiStore.update((state) => ({ ...state, isLoading: loading })); uiStore.update((state) => ({ ...state, isLoading: loading }));
}; };
export const setError = (error: string | null): void => { export const setError = (error: string | undefined): void => {
uiStore.update((state) => ({ ...state, error })); uiStore.update((state) => ({ ...state, error }));
}; };
export const clearError = (): void => { export const clearError = (): void => {
setError(null); setError(undefined);
}; };
export const setCurrentStep = (step: UIState["currentStep"]): void => { export const setCurrentStep = (step: UIState["currentStep"]): void => {
@@ -36,7 +36,7 @@ export const showTextManager = (show: boolean): void => {
export const resetUI = (): void => { export const resetUI = (): void => {
uiStore.set({ uiStore.set({
isLoading: false, isLoading: false,
error: null, error: undefined,
currentStep: "text", currentStep: "text",
showCropModal: false, showCropModal: false,
showTextManager: false, showTextManager: false,
+2
View File
@@ -10,6 +10,8 @@
"skipLibCheck": true, "skipLibCheck": true,
"sourceMap": true, "sourceMap": true,
"strict": true, "strict": true,
"strictNullChecks": true,
"exactOptionalPropertyTypes": true,
"moduleResolution": "bundler" "moduleResolution": "bundler"
} }
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias