feat: Initialize frontend with React, Vite, and Tailwind CSS
- Set up main entry point for React application. - Create About, Home, NotFound, Privacy, and Terms pages with SEO support. - Implement API service for file uploads and task management. - Add global styles using Tailwind CSS. - Create utility functions for SEO and text processing. - Configure Vite for development and production builds. - Set up Nginx configuration for serving frontend and backend. - Add scripts for cleanup of expired files and sitemap generation. - Implement deployment script for production environment.
This commit is contained in:
69
frontend/src/utils/seo.ts
Normal file
69
frontend/src/utils/seo.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* SEO utility functions for structured data generation.
|
||||
*/
|
||||
|
||||
export interface ToolSeoData {
|
||||
name: string;
|
||||
description: string;
|
||||
url: string;
|
||||
category?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate WebApplication JSON-LD structured data for a tool page.
|
||||
*/
|
||||
export function generateToolSchema(tool: ToolSeoData): object {
|
||||
return {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebApplication',
|
||||
name: tool.name,
|
||||
url: tool.url,
|
||||
applicationCategory: tool.category || 'UtilitiesApplication',
|
||||
operatingSystem: 'Any',
|
||||
offers: {
|
||||
'@type': 'Offer',
|
||||
price: '0',
|
||||
priceCurrency: 'USD',
|
||||
},
|
||||
description: tool.description,
|
||||
inLanguage: ['en', 'ar'],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate BreadcrumbList JSON-LD.
|
||||
*/
|
||||
export function generateBreadcrumbs(
|
||||
items: { name: string; url: string }[]
|
||||
): object {
|
||||
return {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'BreadcrumbList',
|
||||
itemListElement: items.map((item, index) => ({
|
||||
'@type': 'ListItem',
|
||||
position: index + 1,
|
||||
name: item.name,
|
||||
item: item.url,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate FAQ structured data.
|
||||
*/
|
||||
export function generateFAQ(
|
||||
questions: { question: string; answer: string }[]
|
||||
): object {
|
||||
return {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'FAQPage',
|
||||
mainEntity: questions.map((q) => ({
|
||||
'@type': 'Question',
|
||||
name: q.question,
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: q.answer,
|
||||
},
|
||||
})),
|
||||
};
|
||||
}
|
||||
124
frontend/src/utils/textTools.ts
Normal file
124
frontend/src/utils/textTools.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* Client-side text processing utilities.
|
||||
* These run entirely in the browser — no API calls needed.
|
||||
*/
|
||||
|
||||
export interface TextStats {
|
||||
words: number;
|
||||
characters: number;
|
||||
charactersNoSpaces: number;
|
||||
sentences: number;
|
||||
paragraphs: number;
|
||||
readingTime: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count words, characters, sentences, and paragraphs.
|
||||
* Supports both English and Arabic text.
|
||||
*/
|
||||
export function countText(text: string): TextStats {
|
||||
if (!text.trim()) {
|
||||
return {
|
||||
words: 0,
|
||||
characters: 0,
|
||||
charactersNoSpaces: 0,
|
||||
sentences: 0,
|
||||
paragraphs: 0,
|
||||
readingTime: '0 min',
|
||||
};
|
||||
}
|
||||
|
||||
const characters = text.length;
|
||||
const charactersNoSpaces = text.replace(/\s/g, '').length;
|
||||
|
||||
// Word count — split by whitespace, filter empty
|
||||
const words = text
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter((w) => w.length > 0).length;
|
||||
|
||||
// Sentence count — split by sentence-ending punctuation
|
||||
const sentences = text
|
||||
.split(/[.!?؟。]+/)
|
||||
.filter((s) => s.trim().length > 0).length;
|
||||
|
||||
// Paragraph count — split by double newlines or single newlines
|
||||
const paragraphs = text
|
||||
.split(/\n\s*\n|\n/)
|
||||
.filter((p) => p.trim().length > 0).length;
|
||||
|
||||
// Reading time (avg 200 words/min for English, 150 for Arabic)
|
||||
const avgWPM = 180;
|
||||
const minutes = Math.ceil(words / avgWPM);
|
||||
const readingTime = minutes <= 1 ? '< 1 min' : `${minutes} min`;
|
||||
|
||||
return {
|
||||
words,
|
||||
characters,
|
||||
charactersNoSpaces,
|
||||
sentences,
|
||||
paragraphs,
|
||||
readingTime,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove extra whitespace (multiple spaces, tabs, etc.)
|
||||
*/
|
||||
export function removeExtraSpaces(text: string): string {
|
||||
return text
|
||||
.replace(/[^\S\n]+/g, ' ') // multiple spaces → single space
|
||||
.replace(/\n{3,}/g, '\n\n') // 3+ newlines → 2
|
||||
.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert text case.
|
||||
*/
|
||||
export function convertCase(
|
||||
text: string,
|
||||
type: 'upper' | 'lower' | 'title' | 'sentence'
|
||||
): string {
|
||||
switch (type) {
|
||||
case 'upper':
|
||||
return text.toUpperCase();
|
||||
|
||||
case 'lower':
|
||||
return text.toLowerCase();
|
||||
|
||||
case 'title':
|
||||
return text.replace(
|
||||
/\w\S*/g,
|
||||
(txt) => txt.charAt(0).toUpperCase() + txt.substring(1).toLowerCase()
|
||||
);
|
||||
|
||||
case 'sentence':
|
||||
return text
|
||||
.toLowerCase()
|
||||
.replace(/(^\s*\w|[.!?؟]\s*\w)/g, (match) => match.toUpperCase());
|
||||
|
||||
default:
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove Arabic diacritics (tashkeel) from text.
|
||||
*/
|
||||
export function removeDiacritics(text: string): string {
|
||||
// Arabic diacritics Unicode range: \u064B-\u065F, \u0670
|
||||
return text.replace(/[\u064B-\u065F\u0670]/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Format file size in human-readable form.
|
||||
*/
|
||||
export function formatFileSize(bytes: number): string {
|
||||
if (bytes === 0) return '0 B';
|
||||
|
||||
const units = ['B', 'KB', 'MB', 'GB'];
|
||||
const k = 1024;
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${units[i]}`;
|
||||
}
|
||||
Reference in New Issue
Block a user