This commit is contained in:
Adrián Borrageiros Mourelos 2025-05-13 23:01:28 +02:00
parent 0ebef51f5c
commit ddd0eba390
27 changed files with 2532 additions and 160 deletions

View File

@ -1 +1,2 @@
JWT_SECRET=146acd46a3932b75db1cf64b21753ee1b933dd73e2fdff3eab8f000f6a7493e7
MONGODB_URI=mongodb+srv://adrianborrageirosmourelos:FOcz19ZnmULvFZeY@cluster0.gf3secu.mongodb.net/draw_dev?retryWrites=true&w=majority&appName=Cluster0

View File

@ -16,6 +16,7 @@
"i18next": "^25.1.2",
"i18next-resources-to-backend": "^1.2.1",
"jsonwebtoken": "^9.0.2",
"jwt-decode": "^4.0.0",
"mongoose": "^8.14.2",
"next": "15.3.2",
"react": "^19.0.0",

View File

@ -1,7 +1,8 @@
{
"header": {
"login": "Log in",
"register": "Sign up"
"register": "Sign up",
"logout": "Log out"
},
"home": {
"title": "Draw what you imagine",
@ -16,7 +17,8 @@
"login": {
"title": "Log in",
"subtitle": "Access your account to start drawing",
"email": "Email address",
"emailOrUsername": "Email or Username",
"emailOrUsernamePlaceholder": "you@example.com or your_username",
"password": "Password",
"forgotPassword": "Forgot your password?",
"rememberMe": "Remember me",
@ -55,5 +57,32 @@
"light": "Light theme",
"dark": "Dark theme",
"select": "Select theme"
},
"dashboard": {
"title": "My Drawings",
"loading": "Loading dashboard...",
"newDrawingButton": "Create New Drawing",
"newDrawingPlaceholder": "Enter drawing title...",
"noDrawings": "You don't have any drawings yet. Create one!",
"errors": {
"createFailed": "Failed to create new drawing.",
"titleRequired": "Title is required."
}
},
"drawingCard": {
"lastUpdatedPrefix": "Last updated:",
"editTitleAria": "Edit title",
"deleteAria": "Delete drawing",
"confirmDelete": {
"title": "Confirm Deletion",
"message": "Are you sure you want to delete \"{{title}}\"? This action cannot be undone.",
"confirmButton": "Delete",
"cancelButton": "Cancel"
},
"errors": {
"titleRequired": "Title is required.",
"renameFailed": "Failed to rename drawing.",
"deleteFailed": "Failed to delete drawing."
}
}
}

View File

@ -1,7 +1,8 @@
{
"header": {
"login": "Iniciar sesión",
"register": "Registrarse"
"register": "Registrarse",
"logout": "Cerrar sesión"
},
"home": {
"title": "Dibuja lo que imagines",
@ -16,7 +17,8 @@
"login": {
"title": "Iniciar sesión",
"subtitle": "Accede a tu cuenta para comenzar a dibujar",
"email": "Correo electrónico",
"emailOrUsername": "Email o Nombre de usuario",
"emailOrUsernamePlaceholder": "tu@ejemplo.com o tu_usuario",
"password": "Contraseña",
"forgotPassword": "¿Olvidaste tu contraseña?",
"rememberMe": "Recordarme",
@ -55,5 +57,32 @@
"light": "Tema claro",
"dark": "Tema oscuro",
"select": "Seleccionar tema"
},
"dashboard": {
"title": "Mis Dibujos",
"loading": "Cargando dashboard...",
"newDrawingButton": "Crear Nuevo Dibujo",
"newDrawingPlaceholder": "Introduce el título del dibujo...",
"noDrawings": "Aún no tienes dibujos. ¡Crea uno!",
"errors": {
"createFailed": "Error al crear nuevo dibujo.",
"titleRequired": "El título es obligatorio."
}
},
"drawingCard": {
"lastUpdatedPrefix": "Última act.:",
"editTitleAria": "Editar título",
"deleteAria": "Eliminar dibujo",
"confirmDelete": {
"title": "Confirmar Eliminación",
"message": "¿Estás seguro de que quieres eliminar \"{{title}}\"? Esta acción no se puede deshacer.",
"confirmButton": "Eliminar",
"cancelButton": "Cancelar"
},
"errors": {
"titleRequired": "El título es obligatorio.",
"renameFailed": "Error al renombrar el dibujo.",
"deleteFailed": "Error al eliminar el dibujo."
}
}
}

View File

@ -18,9 +18,9 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ token, user }, { status: 200 });
} catch (error) {
console.error("Error en el login:", error);
console.error("Error in login:", error);
return NextResponse.json(
{ error: "Error interno del servidor" },
{ error: "Internal server error" },
{ status: 500 }
);
}

View File

@ -13,9 +13,9 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ token, user }, { status: 201 });
} catch (error) {
console.error("Error en el registro:", error);
console.error("Error in register:", error);
return NextResponse.json(
{ error: "Error interno del servidor" },
{ error: "Internal server error" },
{ status: 500 }
);
}

View File

@ -0,0 +1,142 @@
import { NextRequest, NextResponse } from "next/server";
import {
getDrawingById,
updateDrawing,
deleteDrawing,
} from "@/controllers/drawingsController";
import { validateToken } from "@/controllers/authController";
interface RouteParams {
params: {
id: string;
};
}
async function getUserIdFromRequest(
request: NextRequest
): Promise<string | null> {
const authHeader = request.headers.get("Authorization");
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return null;
}
const token = authHeader.substring(7);
const { user, error } = await validateToken(token);
if (error || !user) {
return null;
}
return user.id;
}
export async function GET(request: NextRequest, { params }: RouteParams) {
const userId = await getUserIdFromRequest(request);
if (!userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const drawingId = params.id;
if (!drawingId) {
return NextResponse.json(
{ error: "Drawing ID is required" },
{ status: 400 }
);
}
try {
const result = await getDrawingById(drawingId, userId);
if (result.error) {
return NextResponse.json(
{ error: result.error },
{ status: result.status }
);
}
return NextResponse.json(result.data, { status: result.status });
} catch (error) {
console.error(`Error in GET /api/drawings/${drawingId}:`, error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
export async function PUT(request: NextRequest, { params }: RouteParams) {
const userId = await getUserIdFromRequest(request);
if (!userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const drawingId = params.id;
if (!drawingId) {
return NextResponse.json(
{ error: "Drawing ID is required" },
{ status: 400 }
);
}
try {
const body = await request.json();
const { title, drawingData, shared_with } = body;
if (
title === undefined &&
drawingData === undefined &&
shared_with === undefined
) {
return NextResponse.json(
{ error: "No update data provided" },
{ status: 400 }
);
}
const result = await updateDrawing(drawingId, userId, {
title,
data: drawingData,
shared_with,
});
if (result.error) {
return NextResponse.json(
{ error: result.error },
{ status: result.status }
);
}
return NextResponse.json(result.data, { status: result.status });
} catch (error) {
console.error(`Error in PUT /api/drawings/${drawingId}:`, error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
export async function DELETE(request: NextRequest, { params }: RouteParams) {
const userId = await getUserIdFromRequest(request);
if (!userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const drawingId = params.id;
if (!drawingId) {
return NextResponse.json(
{ error: "Drawing ID is required" },
{ status: 400 }
);
}
try {
const result = await deleteDrawing(drawingId, userId);
if (result.error) {
return NextResponse.json(
{ error: result.error },
{ status: result.status }
);
}
return NextResponse.json(result.data, { status: result.status });
} catch (error) {
console.error(`Error in DELETE /api/drawings/${drawingId}:`, error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}

View File

@ -0,0 +1,67 @@
import { NextRequest, NextResponse } from "next/server";
import {
createDrawing,
getUserDrawings,
} from "@/controllers/drawingsController";
import { validateToken } from "@/controllers/authController";
async function getUserIdFromRequest(
request: NextRequest
): Promise<string | null> {
const authHeader = request.headers.get("Authorization");
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return null;
}
const token = authHeader.substring(7); // Remove "Bearer "
const { user, error } = await validateToken(token);
if (error || !user) {
return null;
}
return user.id;
}
export async function POST(request: NextRequest) {
const userId = await getUserIdFromRequest(request);
if (!userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
try {
const body = await request.json();
const { title, drawingData } = body;
if (!title || drawingData === undefined) {
return NextResponse.json(
{ error: "Missing title or drawingData" },
{ status: 400 }
);
}
const result = await createDrawing({ title, drawingData, userId });
return NextResponse.json(result.data, { status: result.status });
} catch (error) {
console.error("Error in POST /api/drawings:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
export async function GET(request: NextRequest) {
const userId = await getUserIdFromRequest(request);
if (!userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
try {
const result = await getUserDrawings(userId);
return NextResponse.json(result.data, { status: result.status });
} catch (error) {
console.error("Error in GET /api/drawings:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}

View File

@ -83,18 +83,18 @@ export default function Login() {
<form onSubmit={handleSubmit} className="space-y-5">
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
{t('auth.login.email')}
{t('auth.login.emailOrUsername')}
</label>
<input
id="email"
name="email"
type="email"
autoComplete="email"
type="text"
autoComplete="username email"
required
value={formData.email}
onChange={handleChange}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-1 focus:ring-gray-400 focus:border-gray-400 dark:focus:ring-gray-500 dark:focus:border-gray-500 text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
placeholder="tu@ejemplo.com"
placeholder={t('auth.login.emailOrUsernamePlaceholder')}
/>
</div>

View File

@ -0,0 +1,257 @@
.page {
height: 100%;
padding: 2rem;
max-width: 1200px;
margin: 0 auto;
font-family: sans-serif;
color: #333;
background-color: #f9f9f9;
transition: background-color 0.3s ease, color 0.3s ease;
}
:global(.dark) .page {
background-color: #121212;
color: #e0e0e0;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 1px solid #e0e0e0;
}
:global(.dark) .header {
border-bottom: 1px solid #333;
}
.headerLeft {
display: flex;
align-items: center;
gap: 1.5rem;
}
.headerSwitchers {
display: flex;
align-items: center;
gap: 0.75rem;
}
.headerRight {
display: flex;
align-items: center;
gap: 0.75rem;
}
.header h1 {
font-size: 2rem;
font-weight: 600;
color: #333;
}
:global(.dark) .header h1 {
color: #f0f0f0;
}
.logoutButton {
display: flex;
align-items: center;
gap: 0.5rem;
background-color: transparent;
border: 1px solid var(--border-color, #e0e0e0);
color: var(--text-secondary, #555);
padding: 0.5rem 1rem;
border-radius: 6px;
font-size: 0.9rem;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease;
}
:global(.dark) .logoutButton {
--border-color: #444;
--text-secondary: #bbb;
}
.logoutButton:hover {
background-color: var(--hover-bg, #f0f0f0);
border-color: var(--border-color-hover, #ccc);
color: var(--text-primary, #111);
}
:global(.dark) .logoutButton:hover {
--hover-bg: #333;
--border-color-hover: #555;
--text-primary: #fff;
}
.createButton {
background-color: #0070f3;
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 6px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s ease-in-out, transform 0.1s ease;
}
.createButton:hover {
background-color: #005bb5;
transform: translateY(-2px);
}
.createButton:active {
transform: translateY(0);
}
.errorText {
color: #ff4d4f;
background-color: #fff1f0;
border: 1px solid #ffccc7;
padding: 1rem;
border-radius: 6px;
margin-bottom: 1.5rem;
text-align: center;
}
:global(.dark) .errorText {
color: #ff7875;
background-color: rgba(255, 77, 79, 0.2);
border: 1px solid rgba(255, 77, 79, 0.3);
}
.drawingsGrid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1.5rem;
}
.drawingCardPlaceholder {
border: 1px solid #ccc;
padding: 1rem;
border-radius: 8px;
background-color: #f9f9f9;
}
:global(.dark) .drawingCardPlaceholder {
border: 1px solid #333;
background-color: #1e1e1e;
}
.drawingCardPlaceholder h2 {
margin-top: 0;
margin-bottom: 0.5rem;
}
.drawingCardPlaceholder p {
font-size: 0.9rem;
color: #555;
margin-bottom: 1rem;
}
:global(.dark) .drawingCardPlaceholder p {
color: #aaa;
}
.drawingCardLink {
display: inline-block;
padding: 0.5rem 1rem;
background-color: #e9e9e9;
color: #333;
text-decoration: none;
border-radius: 4px;
transition: background-color 0.2s ease;
}
:global(.dark) .drawingCardLink {
background-color: #333;
color: #e0e0e0;
}
.drawingCardLink:hover {
background-color: #dcdcdc;
}
:global(.dark) .drawingCardLink:hover {
background-color: #444;
}
.page p {
text-align: center;
font-size: 1.1rem;
color: #555;
padding: 2rem 0;
}
:global(.dark) .page p {
color: #bbb;
}
.inlineCreateForm {
display: flex;
align-items: center;
gap: 0.5rem;
}
.inlineInput {
padding: 0.6rem 0.8rem;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 0.9rem;
flex-grow: 1;
background-color: #fff;
color: #333;
}
:global(.dark) .inlineInput {
background-color: #2a2a2a;
border-color: #444;
color: #e0e0e0;
}
.inlineInput:focus {
outline: none;
border-color: #0070f3;
box-shadow: 0 0 0 2px rgba(0, 112, 243, 0.2);
}
:global(.dark) .inlineInput:focus {
border-color: #0070f3;
box-shadow: 0 0 0 2px rgba(0, 112, 243, 0.3);
}
.actionButton {
padding: 0.6rem 1rem;
border: none;
border-radius: 4px;
font-size: 0.9rem;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out, transform 0.1s ease;
}
.actionButton:active {
transform: translateY(1px);
}
.confirmButton {
background-color: #28a745;
color: white;
}
.confirmButton:hover {
background-color: #218838;
}
.cancelButton {
background-color: #6c757d;
color: white;
}
.cancelButton:hover {
background-color: #5a6268;
}

213
src/app/dashboard/page.tsx Normal file
View File

@ -0,0 +1,213 @@
"use client";
import { useEffect, useState, useRef } from "react";
import { useRouter } from "next/navigation";
import styles from "./page.module.css";
import { useI18n } from '@/lib/i18n/useI18n';
import AuthGuard from '@/components/AuthGuard';
import { getAllDrawings, createDrawing, Drawing, logout } from '@/lib/api';
import DrawingCard from '@/components/DrawingCard';
import ThemeSwitcher from "@/components/ThemeSwitcher";
import LanguageSwitcher from "@/components/LanguageSwitcher";
import Icon from "@/components/Icon";
export default function Dashboard() {
const { t, isLoading: i18nIsLoading } = useI18n();
const router = useRouter();
const [drawings, setDrawings] = useState<Drawing[]>([]);
const [pageLoading, setPageLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [isCreating, setIsCreating] = useState(false);
const [newDrawingTitle, setNewDrawingTitle] = useState("");
const newTitleInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
const fetchDrawings = async () => {
try {
setPageLoading(true);
setError(null);
const response = await getAllDrawings();
if (response.data) {
setDrawings(response.data);
} else if (response.error) {
setError(response.error);
}
} catch (err) {
setError("Failed to fetch drawings.");
console.error(err);
} finally {
setPageLoading(false);
}
};
if (!i18nIsLoading) {
fetchDrawings();
}
}, [i18nIsLoading]);
useEffect(() => {
if (isCreating && newTitleInputRef.current) {
newTitleInputRef.current.focus();
}
const handleClickOutside = (event: MouseEvent) => {
if (isCreating && newTitleInputRef.current && !newTitleInputRef.current.contains(event.target as Node)) {
handleCancelCreate();
}
};
if (isCreating) {
document.addEventListener('mousedown', handleClickOutside);
} else {
document.removeEventListener('mousedown', handleClickOutside);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [isCreating]);
const handleStartCreate = () => {
setIsCreating(true);
};
const handleCancelCreate = () => {
setIsCreating(false);
setNewDrawingTitle("");
setError(null);
};
const handleConfirmCreate = async () => {
if (!newDrawingTitle.trim()) {
setError(t('dashboard.errors.titleRequired') || "Title is required.");
return;
}
try {
setError(null);
const newDrawingData = {
title: newDrawingTitle.trim(),
drawingData: { elements: [], appState: {} }
};
const response = await createDrawing(newDrawingData);
if (response.data) {
router.push(`/editor?id=${response.data._id}`);
setIsCreating(false);
setNewDrawingTitle("");
// fetchDrawings();
} else if (response.error) {
setError(response.error);
}
} catch (err) {
const errorText = i18nIsLoading ? "Failed to create new drawing." : (t('dashboard.errors.createFailed') || "Failed to create new drawing.");
setError(errorText);
console.error(err);
}
};
const handleNewTitleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
handleConfirmCreate();
}
if (event.key === 'Escape') {
handleCancelCreate();
}
};
const handleDrawingRenamed = (updatedDrawing: Drawing) => {
setDrawings(prevDrawings =>
prevDrawings.map(d => d._id === updatedDrawing._id ? updatedDrawing : d)
);
};
const handleDrawingDeleted = (drawingId: string) => {
setDrawings(prevDrawings =>
prevDrawings.filter(d => d._id !== drawingId)
);
};
const handleLogout = async () => {
try {
await logout();
router.push('/auth/login');
} catch (error) {
console.error("Failed to logout:", error);
setError(t('auth.errors.logoutError') || "Logout failed. Please try again.");
}
};
if (i18nIsLoading) {
return (
<AuthGuard>
<div className={styles.page}>
<p>Loading translations...</p>
</div>
</AuthGuard>
);
}
if (pageLoading) {
return (
<AuthGuard>
<div className={styles.page}>
<p>{t('dashboard.loading') || "Loading dashboard..."}</p>
</div>
</AuthGuard>
);
}
return (
<AuthGuard>
<div className={styles.page}>
<header className={styles.header}>
<div className={styles.headerLeft}>
<h1>{t('dashboard.title') || "My Drawings"}</h1>
<div className={styles.headerSwitchers}>
<LanguageSwitcher />
<ThemeSwitcher />
</div>
</div>
<div className={styles.headerRight}>
{!isCreating ? (
<button onClick={handleStartCreate} className={styles.createButton}>
{t('dashboard.newDrawingButton') || "Create New Drawing"}
</button>
) : (
<div className={styles.inlineCreateForm}>
<input
ref={newTitleInputRef}
type="text"
value={newDrawingTitle}
onChange={(e) => setNewDrawingTitle(e.target.value)}
onKeyDown={handleNewTitleKeyDown}
placeholder={t('dashboard.newDrawingPlaceholder') || "Enter drawing title..."}
className={styles.inlineInput}
/>
</div>
)}
<button onClick={handleLogout} className={styles.logoutButton} title={t('header.logout') || 'Log out'}>
<Icon name="log-out" size={20} />
<span className={styles.logoutButtonText}>{t('header.logout') || 'Log out'}</span>
</button>
</div>
</header>
{error && <p className={styles.errorText}>{error}</p>}
{drawings.length === 0 && !error && !isCreating && (
<p>{t('dashboard.noDrawings') || "You don\'t have any drawings yet. Create one!"}</p>
)}
<div className={styles.drawingsGrid}>
{drawings.map((drawing) => (
<DrawingCard
key={drawing._id}
drawing={drawing}
onDrawingRenamed={handleDrawingRenamed}
onDrawingDeleted={handleDrawingDeleted}
/>
))}
</div>
</div>
</AuthGuard>
);
}

View File

@ -1,33 +1,79 @@
"use client";
import styles from "./page.module.css";
import dynamic from "next/dynamic";
import { useRef } from "react";
import { useRef, useState, useEffect, useCallback } from "react";
import "@excalidraw/excalidraw/index.css";
import { MainMenu } from "@excalidraw/excalidraw";
import { useTheme } from "@/lib/theme-context";
import { useLanguage } from "@/lib/i18n/language-context";
import { useI18n } from '@/lib/i18n/useI18n';
import Icon from '@/components/Icon';
import AuthGuard from '@/components/AuthGuard';
const ExcalidrawComponent = dynamic(
() => import("@excalidraw/excalidraw").then((mod) => mod.Excalidraw),
{ ssr: false }
);
export default function Home() {
export default function Editor() {
const { t, isLoading } = useI18n();
const excalidrawWrapperRef = useRef<HTMLDivElement>(null);
const { theme, setTheme } = useTheme();
const { locale, setLocale } = useLanguage();
const [isClient, setIsClient] = useState(false);
useEffect(() => {
setIsClient(true);
}, []);
const handleThemeChange = useCallback(() => {
setTheme(theme === 'light' ? 'dark' : 'light');
}, [theme, setTheme]);
const handleLanguageChange = useCallback(() => {
const newLocale = locale === 'es' ? 'en' : 'es';
setLocale(newLocale);
}, [locale, setLocale]);
const getMenuItemText = useCallback((key: string, defaultText: string) => {
if (isLoading || !t) return defaultText;
return t(key);
}, [t, isLoading]);
return (
<AuthGuard>
<div className={styles.page}>
<div ref={excalidrawWrapperRef} className={styles.excalidrawContainer}>
<ExcalidrawComponent>
<MainMenu>
<MainMenu.DefaultItems.LoadScene />
<MainMenu.DefaultItems.Export />
<MainMenu.DefaultItems.ClearCanvas />
<MainMenu.Separator />
<MainMenu.DefaultItems.ToggleTheme />
<MainMenu.Separator />
<MainMenu.DefaultItems.ChangeCanvasBackground />
</MainMenu>
</ExcalidrawComponent>
{isClient && (
<ExcalidrawComponent
theme={theme}
langCode={locale === 'es' ? 'es-ES' : 'en-US'}
>
<MainMenu>
<MainMenu.DefaultItems.LoadScene />
<MainMenu.DefaultItems.Export />
<MainMenu.DefaultItems.ClearCanvas />
<MainMenu.Separator />
<MainMenu.Item onSelect={handleThemeChange}>
{theme === 'light'
? <><Icon name='moon' /> {getMenuItemText('theme.dark', 'Dark mode')}</>
: <><Icon name='sun' /> {getMenuItemText('theme.light', 'Light mode')}</>}
</MainMenu.Item>
<MainMenu.Item onSelect={handleLanguageChange}>
{locale === 'es'
? <><Icon name='flag-en' viewBox="0 0 60 30" /> {getMenuItemText('language.en', 'English')}</>
: <><Icon name='flag-es' viewBox="0 0 300 200" /> {getMenuItemText('language.es', 'Spanish')}</>}
</MainMenu.Item>
<MainMenu.Separator />
<MainMenu.DefaultItems.ChangeCanvasBackground />
</MainMenu>
</ExcalidrawComponent>
)}
</div>
</div>
</AuthGuard>
);
}

View File

@ -0,0 +1,51 @@
'use client';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { jwtDecode } from 'jwt-decode';
interface AuthGuardProps {
children: React.ReactNode;
}
interface DecodedToken {
exp: number;
}
const AuthGuard: React.FC<AuthGuardProps> = ({ children }) => {
const router = useRouter();
const [isVerified, setIsVerified] = useState(false);
useEffect(() => {
const token = localStorage.getItem('token');
if (!token) {
router.replace('/auth/login');
return;
}
try {
const decodedToken = jwtDecode<DecodedToken>(token);
const currentTime = Date.now() / 1000;
if (decodedToken.exp < currentTime) {
localStorage.removeItem('token');
router.replace('/auth/login');
} else {
setIsVerified(true);
}
} catch (error) {
console.error('Invalid token:', error);
localStorage.removeItem('token');
router.replace('/auth/login');
}
}, [router]);
if (!isVerified) {
return <div>Loading...</div>;
}
return <>{children}</>;
};
export default AuthGuard;

View File

@ -0,0 +1,288 @@
.cardContainer {
border: 1px solid #e0e0e0;
border-radius: 8px;
background-color: #ffffff;
padding: 1rem;
transition: box-shadow 0.2s ease-in-out, transform 0.2s ease-in-out, background-color 0.3s ease, border-color 0.3s ease;
display: flex;
flex-direction: column;
}
:global(.dark) .cardContainer {
background-color: #1e1e1e;
border-color: #333;
}
.cardContainer:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
:global(.dark) .cardContainer:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.titleContainer {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
}
.titleLink {
text-decoration: none;
color: inherit;
flex-grow: 1;
margin-right: 0.5rem;
}
.cardTitle {
font-size: 1.2rem;
font-weight: 600;
color: #333;
margin: 0;
word-break: break-word;
}
:global(.dark) .cardTitle {
color: #e0e0e0;
}
.editButton {
background: none;
border: none;
padding: 0.25rem;
cursor: pointer;
color: #555;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
}
.editButton:hover {
color: #000;
background-color: #f0f0f0;
}
.actionButtonsContainer {
display: flex;
gap: 0.25rem;
}
.iconButton {
background: none;
border: none;
padding: 0.25rem;
cursor: pointer;
color: #555;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: background-color 0.2s ease, color 0.2s ease;
}
:global(.dark) .iconButton {
color: #aaa;
}
.iconButton:hover {
color: #000;
background-color: #f0f0f0;
}
:global(.dark) .iconButton:hover {
color: #fff;
background-color: #333;
}
.deleteButton:hover {
color: #d9534f;
}
:global(.dark) .deleteButton:hover {
color: #ff7875;
}
.inlineEditForm {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.inlineInput {
padding: 0.5rem 0.7rem;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 1rem;
flex-grow: 1;
background-color: #fff;
color: #333;
}
:global(.dark) .inlineInput {
background-color: #2a2a2a;
border-color: #444;
color: #e0e0e0;
}
.inlineInput:focus {
outline: none;
border-color: #0070f3;
box-shadow: 0 0 0 2px rgba(0, 112, 243, 0.2);
}
:global(.dark) .inlineInput:focus {
border-color: #0070f3;
box-shadow: 0 0 0 2px rgba(0, 112, 243, 0.3);
}
.formActionButton {
padding: 0.5rem 0.9rem;
border: none;
border-radius: 4px;
font-size: 0.85rem;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out, transform 0.1s ease;
}
.formActionButton:active {
transform: translateY(1px);
}
.confirmButton {
background-color: #28a745;
color: white;
}
.confirmButton:hover {
background-color: #218838;
}
.cancelButton {
background-color: #6c757d;
color: white;
}
.cancelButton:hover {
background-color: #5a6268;
}
.cardLinkUnderTitle {
text-decoration: none;
color: inherit;
}
.cardContent {
margin-top: auto;
}
.cardDate {
font-size: 0.8rem;
color: #777;
margin-top: 0.5rem;
}
:global(.dark) .cardDate {
color: #aaa;
}
.renameErrorText {
color: #d9534f;
font-size: 0.8rem;
margin-bottom: 0.5rem;
}
:global(.dark) .renameErrorText {
color: #ff7875;
}
.thumbnailPlaceholder {
width: 100%;
height: 150px;
background-color: #f0f0f0;
display: flex;
align-items: center;
justify-content: center;
color: #999;
border-radius: 4px;
margin-bottom: 12px;
}
:global(.dark) .thumbnailPlaceholder {
background-color: #2a2a2a;
color: #aaa;
}
.cardActions {
margin-top: 16px;
display: flex;
justify-content: flex-end;
}
.modalErrorText {
color: #d9534f;
font-size: 0.9rem;
margin: 0.5rem 0;
padding: 0.5rem;
background-color: rgba(217, 83, 79, 0.1);
border-radius: 4px;
}
:global(.dark) .modalErrorText {
color: #ff7875;
background-color: rgba(255, 120, 117, 0.15);
}
.modalFooter {
display: flex;
justify-content: flex-end;
gap: 0.75rem;
margin-top: 1rem;
}
.modalButtonCancel {
padding: 0.6rem 1.2rem;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
background-color: #f8f9fa;
color: #333;
border: 1px solid #ced4da;
transition: background-color 0.2s ease, border-color 0.2s ease;
}
:global(.dark) .modalButtonCancel {
background-color: #333;
color: #e0e0e0;
border-color: #555;
}
.modalButtonCancel:hover {
background-color: #e2e6ea;
border-color: #dae0e5;
}
:global(.dark) .modalButtonCancel:hover {
background-color: #444;
border-color: #666;
}
.modalButtonConfirm {
padding: 0.6rem 1.2rem;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
background-color: #d9534f;
color: white;
border: 1px solid #d9534f;
transition: background-color 0.2s ease, border-color 0.2s ease;
}
.modalButtonConfirm:hover {
background-color: #c9302c;
border-color: #ac2925;
}

View File

@ -0,0 +1,220 @@
'use client';
import { useState, useEffect, useRef } from 'react';
import Link from 'next/link';
import {
Drawing,
updateDrawing as apiUpdateDrawing,
deleteDrawing as apiDeleteDrawing,
} from '@/lib/api';
import styles from './DrawingCard.module.css';
import { useI18n } from '@/lib/i18n/useI18n';
import Icon from '@/components/Icon';
import Modal from '@/components/Modal';
interface DrawingCardProps {
drawing: Drawing;
onDrawingRenamed: (updatedDrawing: Drawing) => void;
onDrawingDeleted: (drawingId: string) => void;
}
export default function DrawingCard({
drawing,
onDrawingRenamed,
onDrawingDeleted,
}: DrawingCardProps) {
const { t, isLoading: i18nCardIsLoading } = useI18n();
const [isEditing, setIsEditing] = useState(false);
const [editingTitle, setEditingTitle] = useState(drawing.title);
const editTitleInputRef = useRef<HTMLInputElement>(null);
const [renameError, setRenameError] = useState<string | null>(null);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [deleteError, setDeleteError] = useState<string | null>(null);
useEffect(() => {
if (isEditing && editTitleInputRef.current) {
editTitleInputRef.current.focus();
editTitleInputRef.current.select();
}
const handleClickOutside = (event: MouseEvent) => {
if (isEditing && editTitleInputRef.current && !editTitleInputRef.current.contains(event.target as Node)) {
handleCancelEdit();
}
};
if (isEditing) {
document.addEventListener('mousedown', handleClickOutside);
} else {
document.removeEventListener('mousedown', handleClickOutside);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [isEditing]);
const lastUpdatedText = i18nCardIsLoading
? "Last updated:"
: t('drawingCard.lastUpdatedPrefix') || "Last updated:";
const editTitleAriaLabel = i18nCardIsLoading
? "Edit title"
: t('drawingCard.editTitleAria') || "Edit title";
const deleteButtonAriaLabel = i18nCardIsLoading
? "Delete drawing"
: t('drawingCard.deleteAria') || "Delete drawing";
const handleStartEdit = () => {
setEditingTitle(drawing.title);
setIsEditing(true);
setRenameError(null);
};
const handleCancelEdit = () => {
setIsEditing(false);
setEditingTitle(drawing.title);
setRenameError(null);
};
const handleConfirmEdit = async () => {
if (!editingTitle.trim()) {
setRenameError(t('drawingCard.errors.titleRequired') || "Title is required.");
return;
}
if (editingTitle.trim() === drawing.title) {
setIsEditing(false);
setRenameError(null);
return;
}
try {
setRenameError(null);
const response = await apiUpdateDrawing(drawing._id, { title: editingTitle.trim() });
if (response.data) {
onDrawingRenamed(response.data);
setIsEditing(false);
} else {
setRenameError(response.error || t('drawingCard.errors.renameFailed') || "Failed to rename drawing.");
}
} catch (err) {
setRenameError(t('drawingCard.errors.renameFailed') || "Failed to rename drawing.");
console.error("Rename error:", err);
}
};
const handleEditTitleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
handleConfirmEdit();
}
if (event.key === 'Escape') {
handleCancelEdit();
}
};
const handleOpenDeleteModal = () => {
setShowDeleteModal(true);
setDeleteError(null);
};
const handleCloseDeleteModal = () => {
setShowDeleteModal(false);
};
const handleConfirmDelete = async () => {
try {
setDeleteError(null);
const response = await apiDeleteDrawing(drawing._id);
if (response.data) {
onDrawingDeleted(drawing._id);
setShowDeleteModal(false);
} else {
setDeleteError(response.error || t('drawingCard.errors.deleteFailed') || "Failed to delete drawing.");
}
} catch (err) {
setDeleteError(t('drawingCard.errors.deleteFailed') || "Failed to delete drawing.");
console.error("Delete error:", err);
}
};
const modalTitle = i18nCardIsLoading
? "Confirm Deletion"
: t('drawingCard.confirmDelete.title') || "Confirm Deletion";
let modalMessage = `Are you sure you want to delete "${drawing.title}"? This action cannot be undone.`;
if (!i18nCardIsLoading) {
const messageTemplate = t('drawingCard.confirmDelete.message');
if (messageTemplate) {
modalMessage = messageTemplate.replace('{{title}}', drawing.title);
} else {
modalMessage = `Are you sure you want to delete "${drawing.title}"? This action cannot be undone.`;
}
}
const modalConfirmButtonText = i18nCardIsLoading
? "Delete"
: t('drawingCard.confirmDelete.confirmButton') || "Delete";
const modalCancelButtonText = i18nCardIsLoading
? "Cancel"
: t('drawingCard.confirmDelete.cancelButton') || "Cancel";
return (
<>
<div className={styles.cardContainer}>
{renameError && <p className={styles.renameErrorText}>{renameError}</p>}
{!isEditing ? (
<div className={styles.titleContainer}>
<Link href={`/editor?id=${drawing._id}`} className={styles.titleLink}>
<h3 className={styles.cardTitle}>{drawing.title}</h3>
</Link>
<div className={styles.actionButtonsContainer}>
<button onClick={handleStartEdit} className={styles.iconButton} aria-label={editTitleAriaLabel}>
<Icon name="edit-2" size={18} />
</button>
<button onClick={handleOpenDeleteModal} className={`${styles.iconButton} ${styles.deleteButton}`} aria-label={deleteButtonAriaLabel}>
<Icon name="trash-2" size={18} />
</button>
</div>
</div>
) : (
<div className={styles.inlineEditForm}>
<input
ref={editTitleInputRef}
type="text"
value={editingTitle}
onChange={(e) => setEditingTitle(e.target.value)}
onKeyDown={handleEditTitleKeyDown}
className={styles.inlineInput}
/>
</div>
)}
<Link href={`/editor?id=${drawing._id}`} className={styles.cardLinkUnderTitle}>
<div className={styles.cardContent}>
<p className={styles.cardDate}>
{`${lastUpdatedText} ${new Date(drawing.updatedAt).toLocaleDateString()}`}
</p>
</div>
</Link>
</div>
<Modal
isOpen={showDeleteModal}
onClose={handleCloseDeleteModal}
title={modalTitle}
>
<p>{modalMessage}</p>
{deleteError && <p className={styles.modalErrorText}>{deleteError}</p>}
<div className={styles.modalFooter}>
<button onClick={handleCloseDeleteModal} className={styles.modalButtonCancel}>
{modalCancelButtonText}
</button>
<button onClick={handleConfirmDelete} className={styles.modalButtonConfirm}>
{modalConfirmButtonText}
</button>
</div>
</Modal>
</>
);
}

59
src/components/Icon.tsx Normal file
View File

@ -0,0 +1,59 @@
import React from 'react';
import { icons } from '@/lib/icons';
interface IconProps extends React.SVGProps<SVGSVGElement> {
name?: string;
size?: number | string;
strokeWidth?: number | string;
color?: string;
className?: string;
}
const Icon: React.FC<IconProps> = ({
name = '',
size = 24,
strokeWidth = 2,
color = 'currentColor',
className = '',
...restProps
}) => {
const iconHtml = name && icons[name] ? icons[name] : null;
if (iconHtml) {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke={color}
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeLinejoin="round"
className={`feather ${className}`.trim()}
dangerouslySetInnerHTML={{ __html: iconHtml }}
{...restProps}
/>
);
}
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke={color}
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeLinejoin="round"
className={`feather feather-help-circle ${className}`.trim()}
{...restProps}
>
<circle cx="12" cy="12" r="10"></circle>
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path>
<line x1="12" y1="17" x2="12.01" y2="17"></line>
</svg>
);
};
export default Icon;

View File

@ -2,63 +2,28 @@
import { useLanguage } from '@/lib/i18n/language-context';
import { locales } from '@/lib/i18n/i18n-config';
import { useState, useRef, useEffect } from 'react';
import { useI18n } from '@/lib/i18n/useI18n';
export default function LanguageSwitcher() {
const { locale, setLocale } = useLanguage();
const { t, isLoading } = useI18n();
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
const { isLoading } = useI18n();
if (isLoading) return null;
return (
<div className="relative" ref={dropdownRef}>
<button
onClick={() => setIsOpen(!isOpen)}
className="flex items-center gap-1.5 text-sm text-gray-600 hover:text-gray-900"
>
<span className="uppercase">{locale}</span>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
const toggleLanguage = () => {
const currentIndex = locales.indexOf(locale);
const nextIndex = (currentIndex + 1) % locales.length;
setLocale(locales[nextIndex]);
};
{isOpen && (
<div className="absolute right-0 mt-2 w-40 bg-white rounded-md shadow-lg py-1 z-10">
<div className="px-3 py-2 text-xs font-medium text-gray-500">
{t('language.select')}
</div>
{locales.map((lang) => (
<button
key={lang}
onClick={() => {
setLocale(lang);
setIsOpen(false);
}}
className={`block w-full text-left px-4 py-2 text-sm hover:bg-gray-100 ${
locale === lang ? 'text-blue-600 font-medium' : 'text-gray-700'
}`}
>
{t(`language.${lang}`)}
</button>
))}
</div>
)}
return (
<div className="relative">
<button
onClick={toggleLanguage}
className="flex items-center gap-1.5 text-sm text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white"
>
<span className="uppercase border border-gray-400 px-2 py-1 rounded">{locale}</span>
</button>
</div>
);
}

View File

@ -0,0 +1,154 @@
.overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
backdrop-filter: blur(3px);
}
:global(.dark) .overlay {
background-color: rgba(0, 0, 0, 0.7);
}
.modal {
background-color: white;
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
width: 90%;
max-width: 500px;
display: flex;
flex-direction: column;
transition: background-color 0.3s ease, color 0.3s ease, box-shadow 0.3s ease;
}
:global(.dark) .modal {
background-color: #1e1e1e;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.5);
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid #e9ecef;
}
:global(.dark) .header {
border-bottom: 1px solid #333;
}
.title {
font-size: 1.25rem;
font-weight: 600;
color: #333;
margin: 0;
}
:global(.dark) .title {
color: #f0f0f0;
}
.closeButton {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
padding: 0.25rem;
line-height: 1;
color: #666;
border-radius: 4px;
transition: color 0.2s ease, background-color 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
:global(.dark) .closeButton {
color: #aaa;
}
.closeButton:hover {
color: #000;
background-color: #f0f0f0;
}
:global(.dark) .closeButton:hover {
color: #fff;
background-color: #333;
}
.content {
margin-bottom: 1.5rem;
font-size: 1rem;
color: #555;
line-height: 1.6;
}
:global(.dark) .content {
color: #bbb;
}
.footer {
display: flex;
justify-content: flex-end;
gap: 0.75rem;
padding-top: 1rem;
border-top: 1px solid #e9ecef;
}
:global(.dark) .footer {
border-top: 1px solid #333;
}
.footer button {
padding: 0.6rem 1.2rem;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s ease, border-color 0.2s ease, transform 0.1s ease;
}
.footer button:active {
transform: translateY(1px);
}
.footer .confirm {
background-color: #d9534f;
color: white;
border: 1px solid #d9534f;
}
.footer .confirm:hover {
background-color: #c9302c;
border-color: #ac2925;
}
.footer .cancel {
background-color: #f8f9fa;
color: #333;
border: 1px solid #ced4da;
}
:global(.dark) .footer .cancel {
background-color: #333;
color: #e0e0e0;
border-color: #555;
}
.footer .cancel:hover {
background-color: #e2e6ea;
border-color: #dae0e5;
}
:global(.dark) .footer .cancel:hover {
background-color: #444;
border-color: #666;
}

60
src/components/Modal.tsx Normal file
View File

@ -0,0 +1,60 @@
'use client';
import React, { useEffect } from 'react';
import styles from './Modal.module.css';
import Icon from './Icon';
interface ModalProps {
isOpen: boolean;
onClose: () => void;
title: string;
children: React.ReactNode;
footer?: React.ReactNode;
}
export default function Modal({
isOpen,
onClose,
title,
children,
footer,
}: ModalProps) {
useEffect(() => {
const handleEscapeKey = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
onClose();
}
};
if (isOpen) {
document.addEventListener('keydown', handleEscapeKey);
} else {
document.removeEventListener('keydown', handleEscapeKey);
}
return () => {
document.removeEventListener('keydown', handleEscapeKey);
};
}, [isOpen, onClose]);
if (!isOpen) return null;
return (
<div className={styles.overlay} onClick={onClose} role="dialog" aria-modal="true" aria-labelledby="modal-title">
<div className={styles.modal} onClick={(e) => e.stopPropagation()}>
<div className={styles.header}>
<h2 id="modal-title" className={styles.title}>{title}</h2>
<button
onClick={onClose}
className={styles.closeButton}
aria-label="Close modal"
>
<Icon name="x" size={20} />
</button>
</div>
<div className={styles.content}>{children}</div>
{footer && <div className={styles.footer}>{footer}</div>}
</div>
</div>
);
}

View File

@ -1,84 +1,28 @@
'use client';
import { useTheme } from '@/lib/theme-context';
import { useState, useRef, useEffect } from 'react';
import { useI18n } from '@/lib/i18n/useI18n';
import Icon from '@/components/Icon';
export default function ThemeSwitcher() {
const { theme, toggleTheme } = useTheme();
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const { t, isLoading } = useI18n();
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
if (isLoading) return null;
return (
<div className="relative" ref={dropdownRef}>
<div className="relative">
<button
onClick={() => setIsOpen(!isOpen)}
onClick={toggleTheme}
className="flex items-center gap-1.5 text-sm text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white"
aria-label="Cambiar tema"
aria-label={t ? t('theme.toggle') : 'Change theme'}
>
{theme === 'light' ? (
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" clipRule="evenodd" />
</svg>
<Icon name='sun' />
) : (
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
</svg>
<Icon name='moon' />
)}
</button>
{isOpen && (
<div className="absolute right-0 mt-2 w-48 bg-white dark:bg-gray-800 rounded-md shadow-lg py-1 z-10">
<button
className={`block w-full text-left px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 ${
theme === 'light' ? 'text-blue-600 font-medium' : 'text-gray-700 dark:text-gray-300'
}`}
onClick={() => {
if (theme === 'dark') toggleTheme();
setIsOpen(false);
}}
>
<div className="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" clipRule="evenodd" />
</svg>
{t ? t('theme.light') : 'Tema claro'}
</div>
</button>
<button
className={`block w-full text-left px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 ${
theme === 'dark' ? 'text-blue-600 font-medium' : 'text-gray-700 dark:text-gray-300'
}`}
onClick={() => {
if (theme === 'light') toggleTheme();
setIsOpen(false);
}}
>
<div className="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor">
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
</svg>
{t ? t('theme.dark') : 'Tema oscuro'}
</div>
</button>
</div>
)}
</div>
);
}

View File

@ -3,7 +3,7 @@ import jwt from "jsonwebtoken";
import User from "../models/User";
import dbConnect from "@/lib/db/connection";
const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key";
const JWT_SECRET = process.env.JWT_SECRET || "";
export async function register(data: {
username: string;
@ -40,7 +40,7 @@ export async function register(data: {
},
};
} catch (error) {
console.error("Error en el registro:", error);
console.error("Error in register:", error);
return { error: "Internal server error", status: 500 };
}
}
@ -52,9 +52,11 @@ export async function loginUser(data: {
}) {
try {
await dbConnect();
const { email, password, rememberMe } = data;
const { email: identifier, password, rememberMe } = data;
const user = await User.findOne({ email });
const user = await User.findOne({
$or: [{ email: identifier }, { username: identifier }],
});
if (!user) {
return { error: "Invalid credentials", status: 401 };
}
@ -81,7 +83,38 @@ export async function loginUser(data: {
},
};
} catch (error) {
console.error("Error en el login:", error);
console.error("Error in login:", error);
return { error: "Internal server error", status: 500 };
}
}
export async function validateToken(token: string): Promise<{
user?: { id: string; username: string; email: string };
error?: string;
status: number;
}> {
try {
await dbConnect();
const decoded = jwt.verify(token, JWT_SECRET) as { userId: string };
const user = await User.findById(decoded.userId).select("-password");
if (!user) {
return { error: "Invalid token - user not found", status: 401 };
}
return {
user: {
id: user._id.toString(),
username: user.username,
email: user.email,
},
status: 200,
};
} catch (error) {
console.error("Error in validateToken:", error);
if (error instanceof jwt.JsonWebTokenError) {
return { error: "Invalid token", status: 401 };
}
return { error: "Internal server error", status: 500 };
}
}

View File

@ -0,0 +1,130 @@
import Drawing, { IDrawing } from "@/models/Drawing";
import dbConnect from "@/lib/db/connection";
import User from "@/models/User";
export async function createDrawing(data: {
title: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
drawingData: any;
userId: string;
}) {
try {
await dbConnect();
const { title, drawingData, userId } = data;
// Verificar que el usuario exista (opcional, pero buena práctica)
const owner = await User.findById(userId);
if (!owner) {
return { error: "Owner not found", status: 404 };
}
const newDrawing = await Drawing.create({
owner_id: userId,
title,
data: drawingData,
shared_with: [], // Inicialmente no compartido
});
return { data: newDrawing, status: 201 };
} catch (error) {
console.error("Error in createDrawing:", error);
return { error: "Internal server error", status: 500 };
}
}
export async function getUserDrawings(userId: string) {
try {
await dbConnect();
const drawings = await Drawing.find({ owner_id: userId }).sort({
updatedAt: -1,
});
return { data: drawings, status: 200 };
} catch (error) {
console.error("Error in getUserDrawings:", error);
return { error: "Internal server error", status: 500 };
}
}
export async function getDrawingById(drawingId: string, userId: string) {
try {
await dbConnect();
const drawing = await Drawing.findById(drawingId);
if (!drawing) {
return { error: "Drawing not found", status: 404 };
}
const isOwner = drawing.owner_id.toString() === userId;
const isSharedWithUser = drawing.shared_with
.map((id: string) => id.toString())
.includes(userId);
if (!isOwner && !isSharedWithUser) {
return { error: "Forbidden", status: 403 };
}
return { data: drawing, status: 200 };
} catch (error) {
console.error("Error in getDrawingById:", error);
return { error: "Internal server error", status: 500 };
}
}
export async function updateDrawing(
drawingId: string,
userId: string,
updateData: Partial<Pick<IDrawing, "title" | "data" | "shared_with">>
) {
try {
await dbConnect();
const drawing = await Drawing.findById(drawingId);
if (!drawing) {
return { error: "Drawing not found", status: 404 };
}
if (drawing.owner_id.toString() !== userId) {
return { error: "Forbidden", status: 403 };
}
if (updateData.title !== undefined) {
drawing.title = updateData.title;
}
if (updateData.data !== undefined) {
drawing.data = updateData.data;
}
if (updateData.shared_with !== undefined) {
drawing.shared_with = updateData.shared_with;
}
drawing.updatedAt = new Date();
const updatedDrawing = await drawing.save();
return { data: updatedDrawing, status: 200 };
} catch (error) {
console.error("Error in updateDrawing:", error);
return { error: "Internal server error", status: 500 };
}
}
export async function deleteDrawing(drawingId: string, userId: string) {
try {
await dbConnect();
const drawing = await Drawing.findById(drawingId);
if (!drawing) {
return { error: "Drawing not found", status: 404 };
}
if (drawing.owner_id.toString() !== userId) {
return { error: "Forbidden", status: 403 };
}
await Drawing.findByIdAndDelete(drawingId);
return { data: { message: "Drawing deleted successfully" }, status: 200 };
} catch (error) {
console.error("Error in deleteDrawing:", error);
return { error: "Internal server error", status: 500 };
}
}

View File

@ -1,3 +1,5 @@
import { jwtDecode } from "jwt-decode";
interface LoginRequest {
email: string;
password: string;
@ -27,12 +29,56 @@ interface AuthResponse {
user: UserProfile;
}
interface DecodedToken {
exp: number;
}
async function fetchApi<T, R>(
endpoint: string,
method: string = "GET",
body?: T,
headers?: Record<string, string>
headers?: Record<string, string>,
requiresAuth: boolean = false
): Promise<ApiResponse<R>> {
if (requiresAuth) {
const token = localStorage.getItem("token");
if (!token) {
logout();
return {
error: "Authentication required. No token found.",
status: 401,
};
}
try {
const decodedToken = jwtDecode<DecodedToken>(token);
const currentTime = Date.now() / 1000;
if (decodedToken.exp < currentTime) {
logout();
return {
error: "Authentication required. Token expired.",
status: 401,
};
}
headers = {
...headers,
Authorization: `Bearer ${token}`,
};
} catch (error) {
console.error("Token validation error:", error);
localStorage.removeItem("token");
if (typeof window !== "undefined") {
window.location.href = "/auth/login";
}
return {
error: "Authentication required. Invalid token.",
status: 401,
};
}
}
try {
const requestHeaders = {
"Content-Type": "application/json",
@ -48,9 +94,18 @@ async function fetchApi<T, R>(
const response = await fetch(`/api${endpoint}`, config);
const data = await response.json();
if (!response.ok) {
if (response.status === 401 && typeof window !== "undefined") {
logout();
}
return {
error: data.error || "Error en la petición",
status: response.status,
};
}
return {
data: response.ok ? data : undefined,
error: !response.ok ? data.error || "Error en la petición" : undefined,
data: data,
status: response.status,
};
} catch (error) {
@ -82,21 +137,91 @@ export async function register(
);
}
export async function getProfile(): Promise<ApiResponse<UserProfile>> {
const token = localStorage.getItem("token");
if (!token) {
return {
error: "No hay token de autenticación",
status: 401,
};
}
return fetchApi<undefined, UserProfile>("/user/profile", "GET", undefined, {
Authorization: `Bearer ${token}`,
});
}
export async function logout(): Promise<void> {
localStorage.removeItem("token");
if (typeof window !== "undefined") {
window.location.href = "/auth/login";
}
}
// --- Drawing API Functions ---
export interface Drawing {
_id: string;
owner_id: string;
title: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data: any;
shared_with: string[];
createdAt: string;
updatedAt: string;
}
interface CreateDrawingRequest {
title: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
drawingData: any;
}
interface UpdateDrawingRequest {
title?: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
drawingData?: any;
shared_with?: string[];
}
export async function createDrawing(
drawingData: CreateDrawingRequest
): Promise<ApiResponse<Drawing>> {
return fetchApi<CreateDrawingRequest, Drawing>(
"/drawings",
"POST",
drawingData,
{},
true
);
}
export async function getAllDrawings(): Promise<ApiResponse<Drawing[]>> {
return fetchApi<undefined, Drawing[]>(
"/drawings",
"GET",
undefined,
{},
true
);
}
export async function getOneDrawing(id: string): Promise<ApiResponse<Drawing>> {
return fetchApi<undefined, Drawing>(
`/drawings/${id}`,
"GET",
undefined,
{},
true
);
}
export async function updateDrawing(
id: string,
updateData: UpdateDrawingRequest
): Promise<ApiResponse<Drawing>> {
return fetchApi<UpdateDrawingRequest, Drawing>(
`/drawings/${id}`,
"PUT",
updateData,
{},
true
);
}
export async function deleteDrawing(
id: string
): Promise<ApiResponse<{ message: string }>> {
return fetchApi<undefined, { message: string }>(
`/drawings/${id}`,
"DELETE",
undefined,
{},
true
);
}

View File

@ -30,7 +30,6 @@ export function LanguageProvider({ children }: { children: ReactNode }) {
if (locales.includes(newLocale)) {
setLocale(newLocale);
localStorage.setItem('locale', newLocale);
window.location.reload();
}
};

516
src/lib/icons.ts Normal file
View File

@ -0,0 +1,516 @@
export const icons: Record<string, string> = {
activity: '<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline>',
airplay:
'<path d="M5 17H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2h-1"></path><polygon points="12 15 17 21 7 21 12 15"></polygon>',
"alert-circle":
'<circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line>',
"alert-octagon":
'<polygon points="7.86 2 16.14 2 22 7.86 22 16.14 16.14 22 7.86 22 2 16.14 2 7.86 7.86 2"></polygon><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line>',
"alert-triangle":
'<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line>',
"align-center":
'<line x1="18" y1="10" x2="6" y2="10"></line><line x1="21" y1="6" x2="3" y2="6"></line><line x1="21" y1="14" x2="3" y2="14"></line><line x1="18" y1="18" x2="6" y2="18"></line>',
"align-justify":
'<line x1="21" y1="10" x2="3" y2="10"></line><line x1="21" y1="6" x2="3" y2="6"></line><line x1="21" y1="14" x2="3" y2="14"></line><line x1="21" y1="18" x2="3" y2="18"></line>',
"align-left":
'<line x1="17" y1="10" x2="3" y2="10"></line><line x1="21" y1="6" x2="3" y2="6"></line><line x1="21" y1="14" x2="3" y2="14"></line><line x1="17" y1="18" x2="3" y2="18"></line>',
"align-right":
'<line x1="21" y1="10" x2="7" y2="10"></line><line x1="21" y1="6" x2="3" y2="6"></line><line x1="21" y1="14" x2="3" y2="14"></line><line x1="21" y1="18" x2="7" y2="18"></line>',
anchor:
'<circle cx="12" cy="5" r="3"></circle><line x1="12" y1="22" x2="12" y2="8"></line><path d="M5 12H2a10 10 0 0 0 20 0h-3"></path>',
aperture:
'<circle cx="12" cy="12" r="10"></circle><line x1="14.31" y1="8" x2="20.05" y2="17.94"></line><line x1="9.69" y1="8" x2="21.17" y2="8"></line><line x1="7.38" y1="12" x2="13.12" y2="2.06"></line><line x1="9.69" y1="16" x2="3.95" y2="6.06"></line><line x1="14.31" y1="16" x2="2.83" y2="16"></line><line x1="16.62" y1="12" x2="10.88" y2="21.94"></line>',
archive:
'<polyline points="21 8 21 21 3 21 3 8"></polyline><rect x="1" y="3" width="22" height="5"></rect><line x1="10" y1="12" x2="14" y2="12"></line>',
"arrow-down":
'<line x1="12" y1="5" x2="12" y2="19"></line><polyline points="19 12 12 19 5 12"></polyline>',
"arrow-down-circle":
'<circle cx="12" cy="12" r="10"></circle><polyline points="8 12 12 16 16 12"></polyline><line x1="12" y1="8" x2="12" y2="16"></line>',
"arrow-down-left":
'<line x1="17" y1="7" x2="7" y2="17"></line><polyline points="17 17 7 17 7 7"></polyline>',
"arrow-down-right":
'<line x1="7" y1="7" x2="17" y2="17"></line><polyline points="17 7 17 17 7 17"></polyline>',
"arrow-left":
'<line x1="19" y1="12" x2="5" y2="12"></line><polyline points="12 19 5 12 12 5"></polyline>',
"arrow-left-circle":
'<circle cx="12" cy="12" r="10"></circle><polyline points="12 8 8 12 12 16"></polyline><line x1="16" y1="12" x2="8" y2="12"></line>',
"arrow-right":
'<line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline>',
"arrow-right-circle":
'<circle cx="12" cy="12" r="10"></circle><polyline points="12 16 16 12 12 8"></polyline><line x1="8" y1="12" x2="16" y2="12"></line>',
"arrow-up":
'<line x1="12" y1="19" x2="12" y2="5"></line><polyline points="5 12 12 5 19 12"></polyline>',
"arrow-up-circle":
'<circle cx="12" cy="12" r="10"></circle><polyline points="16 12 12 8 8 12"></polyline><line x1="12" y1="16" x2="12" y2="8"></line>',
"arrow-up-left":
'<line x1="17" y1="17" x2="7" y2="7"></line><polyline points="7 17 7 7 17 7"></polyline>',
"arrow-up-right":
'<line x1="7" y1="17" x2="17" y2="7"></line><polyline points="7 7 17 7 17 17"></polyline>',
"at-sign":
'<circle cx="12" cy="12" r="4"></circle><path d="M16 8v5a3 3 0 0 0 6 0v-1a10 10 0 1 0-3.92 7.94"></path>',
award:
'<circle cx="12" cy="8" r="7"></circle><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline>',
"bar-chart":
'<line x1="12" y1="20" x2="12" y2="10"></line><line x1="18" y1="20" x2="18" y2="4"></line><line x1="6" y1="20" x2="6" y2="16"></line>',
"bar-chart-2":
'<line x1="18" y1="20" x2="18" y2="10"></line><line x1="12" y1="20" x2="12" y2="4"></line><line x1="6" y1="20" x2="6" y2="14"></line>',
battery:
'<rect x="1" y="6" width="18" height="12" rx="2" ry="2"></rect><line x1="23" y1="13" x2="23" y2="11"></line>',
"battery-charging":
'<path d="M5 18H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h3.19M15 6h2a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-3.19"></path><line x1="23" y1="13" x2="23" y2="11"></line><polyline points="11 6 7 12 13 12 9 18"></polyline>',
bell: '<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path><path d="M13.73 21a2 2 0 0 1-3.46 0"></path>',
"bell-off":
'<path d="M13.73 21a2 2 0 0 1-3.46 0"></path><path d="M18.63 13A17.89 17.89 0 0 1 18 8"></path><path d="M6.26 6.26A5.86 5.86 0 0 0 6 8c0 7-3 9-3 9h14"></path><path d="M18 8a6 6 0 0 0-9.33-5"></path><line x1="1" y1="1" x2="23" y2="23"></line>',
bluetooth:
'<polyline points="6.5 6.5 17.5 17.5 12 23 12 1 17.5 6.5 6.5 17.5"></polyline>',
bold: '<path d="M6 4h8a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"></path><path d="M6 12h9a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"></path>',
book: '<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path>',
"book-open":
'<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path>',
bookmark:
'<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"></path>',
box: '<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path><polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline><line x1="12" y1="22.08" x2="12" y2="12"></line>',
briefcase:
'<rect x="2" y="7" width="20" height="14" rx="2" ry="2"></rect><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"></path>',
calendar:
'<rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line>',
camera:
'<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"></path><circle cx="12" cy="13" r="4"></circle>',
"camera-off":
'<line x1="1" y1="1" x2="23" y2="23"></line><path d="M21 21H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h3m3-3h6l2 3h4a2 2 0 0 1 2 2v9.34m-7.72-2.06a4 4 0 1 1-5.56-5.56"></path>',
cast: '<path d="M2 16.1A5 5 0 0 1 5.9 20M2 12.05A9 9 0 0 1 9.95 20M2 8V6a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-6"></path><line x1="2" y1="20" x2="2.01" y2="20"></line>',
check: '<polyline points="20 6 9 17 4 12"></polyline>',
"check-circle":
'<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22 4 12 14.01 9 11.01"></polyline>',
"check-square":
'<polyline points="9 11 12 14 22 4"></polyline><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path>',
"chevron-down": '<polyline points="6 9 12 15 18 9"></polyline>',
"chevron-left": '<polyline points="15 18 9 12 15 6"></polyline>',
"chevron-right": '<polyline points="9 18 15 12 9 6"></polyline>',
"chevron-up": '<polyline points="18 15 12 9 6 15"></polyline>',
"chevrons-down":
'<polyline points="7 13 12 18 17 13"></polyline><polyline points="7 6 12 11 17 6"></polyline>',
"chevrons-left":
'<polyline points="11 17 6 12 11 7"></polyline><polyline points="18 17 13 12 18 7"></polyline>',
"chevrons-right":
'<polyline points="13 17 18 12 13 7"></polyline><polyline points="6 17 11 12 6 7"></polyline>',
"chevrons-up":
'<polyline points="17 11 12 6 7 11"></polyline><polyline points="17 18 12 13 7 18"></polyline>',
chrome:
'<circle cx="12" cy="12" r="10"></circle><circle cx="12" cy="12" r="4"></circle><line x1="21.17" y1="8" x2="12" y2="8"></line><line x1="3.95" y1="6.06" x2="8.54" y2="14"></line><line x1="10.88" y1="21.94" x2="15.46" y2="14"></line>',
circle: '<circle cx="12" cy="12" r="10"></circle>',
clipboard:
'<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path><rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect>',
clock:
'<circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline>',
cloud: '<path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"></path>',
"cloud-drizzle":
'<line x1="8" y1="19" x2="8" y2="21"></line><line x1="8" y1="13" x2="8" y2="15"></line><line x1="16" y1="19" x2="16" y2="21"></line><line x1="16" y1="13" x2="16" y2="15"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="12" y1="15" x2="12" y2="17"></line><path d="M20 16.58A5 5 0 0 0 18 7h-1.26A8 8 0 1 0 4 15.25"></path>',
"cloud-lightning":
'<path d="M19 16.9A5 5 0 0 0 18 7h-1.26a8 8 0 1 0-11.62 9"></path><polyline points="13 11 9 17 15 17 11 23"></polyline>',
"cloud-off":
'<path d="M22.61 16.95A5 5 0 0 0 18 10h-1.26a8 8 0 0 0-7.05-6M5 5a8 8 0 0 0 4 15h9a5 5 0 0 0 1.7-.3"></path><line x1="1" y1="1" x2="23" y2="23"></line>',
"cloud-rain":
'<line x1="16" y1="13" x2="16" y2="21"></line><line x1="8" y1="13" x2="8" y2="21"></line><line x1="12" y1="15" x2="12" y2="23"></line><path d="M20 16.58A5 5 0 0 0 18 7h-1.26A8 8 0 1 0 4 15.25"></path>',
"cloud-snow":
'<path d="M20 17.58A5 5 0 0 0 18 8h-1.26A8 8 0 1 0 4 16.25"></path><line x1="8" y1="16" x2="8.01" y2="16"></line><line x1="8" y1="20" x2="8.01" y2="20"></line><line x1="12" y1="18" x2="12.01" y2="18"></line><line x1="12" y1="22" x2="12.01" y2="22"></line><line x1="16" y1="16" x2="16.01" y2="16"></line><line x1="16" y1="20" x2="16.01" y2="20"></line>',
code: '<polyline points="16 18 22 12 16 6"></polyline><polyline points="8 6 2 12 8 18"></polyline>',
codepen:
'<polygon points="12 2 22 8.5 22 15.5 12 22 2 15.5 2 8.5 12 2"></polygon><line x1="12" y1="22" x2="12" y2="15.5"></line><polyline points="22 8.5 12 15.5 2 8.5"></polyline><polyline points="2 15.5 12 8.5 22 15.5"></polyline><line x1="12" y1="2" x2="12" y2="8.5"></line>',
codesandbox:
'<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path><polyline points="7.5 4.21 12 6.81 16.5 4.21"></polyline><polyline points="7.5 19.79 7.5 14.6 3 12"></polyline><polyline points="21 12 16.5 14.6 16.5 19.79"></polyline><polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline><line x1="12" y1="22.08" x2="12" y2="12"></line>',
coffee:
'<path d="M18 8h1a4 4 0 0 1 0 8h-1"></path><path d="M2 8h16v9a4 4 0 0 1-4 4H6a4 4 0 0 1-4-4V8z"></path><line x1="6" y1="1" x2="6" y2="4"></line><line x1="10" y1="1" x2="10" y2="4"></line><line x1="14" y1="1" x2="14" y2="4"></line>',
columns:
'<path d="M12 3h7a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-7m0-18H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h7m0-18v18"></path>',
command:
'<path d="M18 3a3 3 0 0 0-3 3v12a3 3 0 0 0 3 3 3 3 0 0 0 3-3 3 3 0 0 0-3-3H6a3 3 0 0 0-3 3 3 3 0 0 0 3 3 3 3 0 0 0 3-3V6a3 3 0 0 0-3-3 3 3 0 0 0-3 3 3 3 0 0 0 3 3h12a3 3 0 0 0 3-3 3 3 0 0 0-3-3z"></path>',
compass:
'<circle cx="12" cy="12" r="10"></circle><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"></polygon>',
copy: '<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>',
"corner-down-left":
'<polyline points="9 10 4 15 9 20"></polyline><path d="M20 4v7a4 4 0 0 1-4 4H4"></path>',
"corner-down-right":
'<polyline points="15 10 20 15 15 20"></polyline><path d="M4 4v7a4 4 0 0 0 4 4h12"></path>',
"corner-left-down":
'<polyline points="14 15 9 20 4 15"></polyline><path d="M20 4h-7a4 4 0 0 0-4 4v12"></path>',
"corner-left-up":
'<polyline points="14 9 9 4 4 9"></polyline><path d="M20 20h-7a4 4 0 0 1-4-4V4"></path>',
"corner-right-down":
'<polyline points="10 15 15 20 20 15"></polyline><path d="M4 4h7a4 4 0 0 1 4 4v12"></path>',
"corner-right-up":
'<polyline points="10 9 15 4 20 9"></polyline><path d="M4 20h7a4 4 0 0 0 4-4V4"></path>',
"corner-up-left":
'<polyline points="9 14 4 9 9 4"></polyline><path d="M20 20v-7a4 4 0 0 0-4-4H4"></path>',
"corner-up-right":
'<polyline points="15 14 20 9 15 4"></polyline><path d="M4 20v-7a4 4 0 0 1 4-4h12"></path>',
cpu: '<rect x="4" y="4" width="16" height="16" rx="2" ry="2"></rect><rect x="9" y="9" width="6" height="6"></rect><line x1="9" y1="1" x2="9" y2="4"></line><line x1="15" y1="1" x2="15" y2="4"></line><line x1="9" y1="20" x2="9" y2="23"></line><line x1="15" y1="20" x2="15" y2="23"></line><line x1="20" y1="9" x2="23" y2="9"></line><line x1="20" y1="14" x2="23" y2="14"></line><line x1="1" y1="9" x2="4" y2="9"></line><line x1="1" y1="14" x2="4" y2="14"></line>',
"credit-card":
'<rect x="1" y="4" width="22" height="16" rx="2" ry="2"></rect><line x1="1" y1="10" x2="23" y2="10"></line>',
crop: '<path d="M6.13 1L6 16a2 2 0 0 0 2 2h15"></path><path d="M1 6.13L16 6a2 2 0 0 1 2 2v15"></path>',
crosshair:
'<circle cx="12" cy="12" r="10"></circle><line x1="22" y1="12" x2="18" y2="12"></line><line x1="6" y1="12" x2="2" y2="12"></line><line x1="12" y1="6" x2="12" y2="2"></line><line x1="12" y1="22" x2="12" y2="18"></line>',
database:
'<ellipse cx="12" cy="5" rx="9" ry="3"></ellipse><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"></path><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"></path>',
delete:
'<path d="M21 4H8l-7 8 7 8h13a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2z"></path><line x1="18" y1="9" x2="12" y2="15"></line><line x1="12" y1="9" x2="18" y2="15"></line>',
disc: '<circle cx="12" cy="12" r="10"></circle><circle cx="12" cy="12" r="3"></circle>',
divide:
'<circle cx="12" cy="6" r="2"></circle><line x1="5" y1="12" x2="19" y2="12"></line><circle cx="12" cy="18" r="2"></circle>',
"divide-circle":
'<line x1="8" y1="12" x2="16" y2="12"></line><line x1="12" y1="16" x2="12" y2="16"></line><line x1="12" y1="8" x2="12" y2="8"></line><circle cx="12" cy="12" r="10"></circle>',
"divide-square":
'<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="8" y1="12" x2="16" y2="12"></line><line x1="12" y1="16" x2="12" y2="16"></line><line x1="12" y1="8" x2="12" y2="8"></line>',
"dollar-sign":
'<line x1="12" y1="1" x2="12" y2="23"></line><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"></path>',
download:
'<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>',
"download-cloud":
'<polyline points="8 17 12 21 16 17"></polyline><line x1="12" y1="12" x2="12" y2="21"></line><path d="M20.88 18.09A5 5 0 0 0 18 9h-1.26A8 8 0 1 0 3 16.29"></path>',
dribbble:
'<circle cx="12" cy="12" r="10"></circle><path d="M8.56 2.75c4.37 6.03 6.02 9.42 8.03 17.72m2.54-15.38c-3.72 4.35-8.94 5.66-16.88 5.85m19.5 1.9c-3.5-.93-6.63-.82-8.94 0-2.58.92-5.01 2.86-7.44 6.32"></path>',
droplet: '<path d="M12 2.69l5.66 5.66a8 8 0 1 1-11.31 0z"></path>',
edit: '<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>',
"edit-2":
'<path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"></path>',
"edit-3":
'<path d="M12 20h9"></path><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"></path>',
"external-link":
'<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line>',
eye: '<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle>',
"eye-off":
'<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path><line x1="1" y1="1" x2="23" y2="23"></line>',
facebook:
'<path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"></path>',
"fast-forward":
'<polygon points="13 19 22 12 13 5 13 19"></polygon><polygon points="2 19 11 12 2 5 2 19"></polygon>',
feather:
'<path d="M20.24 12.24a6 6 0 0 0-8.49-8.49L5 10.5V19h8.5z"></path><line x1="16" y1="8" x2="2" y2="22"></line><line x1="17.5" y1="15" x2="9" y2="15"></line>',
figma:
'<path d="M5 5.5A3.5 3.5 0 0 1 8.5 2H12v7H8.5A3.5 3.5 0 0 1 5 5.5z"></path><path d="M12 2h3.5a3.5 3.5 0 1 1 0 7H12V2z"></path><path d="M12 12.5a3.5 3.5 0 1 1 7 0 3.5 3.5 0 1 1-7 0z"></path><path d="M5 19.5A3.5 3.5 0 0 1 8.5 16H12v3.5a3.5 3.5 0 1 1-7 0z"></path><path d="M5 12.5A3.5 3.5 0 0 1 8.5 9H12v7H8.5A3.5 3.5 0 0 1 5 12.5z"></path>',
file: '<path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline>',
"file-minus":
'<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="9" y1="15" x2="15" y2="15"></line>',
"file-plus":
'<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="12" y1="18" x2="12" y2="12"></line><line x1="9" y1="15" x2="15" y2="15"></line>',
"file-text":
'<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline>',
film: '<rect x="2" y="2" width="20" height="20" rx="2.18" ry="2.18"></rect><line x1="7" y1="2" x2="7" y2="22"></line><line x1="17" y1="2" x2="17" y2="22"></line><line x1="2" y1="12" x2="22" y2="12"></line><line x1="2" y1="7" x2="7" y2="7"></line><line x1="2" y1="17" x2="7" y2="17"></line><line x1="17" y1="17" x2="22" y2="17"></line><line x1="17" y1="7" x2="22" y2="7"></line>',
filter:
'<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"></polygon>',
flag: '<path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"></path><line x1="4" y1="22" x2="4" y2="15"></line>',
folder:
'<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>',
"folder-minus":
'<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path><line x1="9" y1="14" x2="15" y2="14"></line>',
"folder-plus":
'<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path><line x1="12" y1="11" x2="12" y2="17"></line><line x1="9" y1="14" x2="15" y2="14"></line>',
framer: '<path d="M5 16V9h14V2H5l14 14h-7m-7 0l7 7v-7m-7 0h7"></path>',
frown:
'<circle cx="12" cy="12" r="10"></circle><path d="M16 16s-1.5-2-4-2-4 2-4 2"></path><line x1="9" y1="9" x2="9.01" y2="9"></line><line x1="15" y1="9" x2="15.01" y2="9"></line>',
gift: '<polyline points="20 12 20 22 4 22 4 12"></polyline><rect x="2" y="7" width="20" height="5"></rect><line x1="12" y1="22" x2="12" y2="7"></line><path d="M12 7H7.5a2.5 2.5 0 0 1 0-5C11 2 12 7 12 7z"></path><path d="M12 7h4.5a2.5 2.5 0 0 0 0-5C13 2 12 7 12 7z"></path>',
"git-branch":
'<line x1="6" y1="3" x2="6" y2="15"></line><circle cx="18" cy="6" r="3"></circle><circle cx="6" cy="18" r="3"></circle><path d="M18 9a9 9 0 0 1-9 9"></path>',
"git-commit":
'<circle cx="12" cy="12" r="4"></circle><line x1="1.05" y1="12" x2="7" y2="12"></line><line x1="17.01" y1="12" x2="22.96" y2="12"></line>',
"git-merge":
'<circle cx="18" cy="18" r="3"></circle><circle cx="6" cy="6" r="3"></circle><path d="M6 21V9a9 9 0 0 0 9 9"></path>',
"git-pull-request":
'<circle cx="18" cy="18" r="3"></circle><circle cx="6" cy="6" r="3"></circle><path d="M13 6h3a2 2 0 0 1 2 2v7"></path><line x1="6" y1="9" x2="6" y2="21"></line>',
github:
'<path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path>',
gitlab:
'<path d="M22.65 14.39L12 22.13 1.35 14.39a.84.84 0 0 1-.3-.94l1.22-3.78 2.44-7.51A.42.42 0 0 1 4.82 2a.43.43 0 0 1 .58 0 .42.42 0 0 1 .11.18l2.44 7.49h8.1l2.44-7.51A.42.42 0 0 1 18.6 2a.43.43 0 0 1 .58 0 .42.42 0 0 1 .11.18l2.44 7.51L23 13.45a.84.84 0 0 1-.35.94z"></path>',
globe:
'<circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path>',
grid: '<rect x="3" y="3" width="7" height="7"></rect><rect x="14" y="3" width="7" height="7"></rect><rect x="14" y="14" width="7" height="7"></rect><rect x="3" y="14" width="7" height="7"></rect>',
"hard-drive":
'<line x1="22" y1="12" x2="2" y2="12"></line><path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"></path><line x1="6" y1="16" x2="6.01" y2="16"></line><line x1="10" y1="16" x2="10.01" y2="16"></line>',
hash: '<line x1="4" y1="9" x2="20" y2="9"></line><line x1="4" y1="15" x2="20" y2="15"></line><line x1="10" y1="3" x2="8" y2="21"></line><line x1="16" y1="3" x2="14" y2="21"></line>',
headphones:
'<path d="M3 18v-6a9 9 0 0 1 18 0v6"></path><path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z"></path>',
heart:
'<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path>',
"help-circle":
'<circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line>',
hexagon:
'<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path>',
home: '<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline>',
image:
'<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><polyline points="21 15 16 10 5 21"></polyline>',
inbox:
'<polyline points="22 12 16 12 14 15 10 15 8 12 2 12"></polyline><path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"></path>',
info: '<circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line>',
instagram:
'<rect x="2" y="2" width="20" height="20" rx="5" ry="5"></rect><path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"></path><line x1="17.5" y1="6.5" x2="17.51" y2="6.5"></line>',
italic:
'<line x1="19" y1="4" x2="10" y2="4"></line><line x1="14" y1="20" x2="5" y2="20"></line><line x1="15" y1="4" x2="9" y2="20"></line>',
key: '<path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"></path>',
layers:
'<polygon points="12 2 2 7 12 12 22 7 12 2"></polygon><polyline points="2 17 12 22 22 17"></polyline><polyline points="2 12 12 17 22 12"></polyline>',
layout:
'<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="3" y1="9" x2="21" y2="9"></line><line x1="9" y1="21" x2="9" y2="9"></line>',
"life-buoy":
'<circle cx="12" cy="12" r="10"></circle><circle cx="12" cy="12" r="4"></circle><line x1="4.93" y1="4.93" x2="9.17" y2="9.17"></line><line x1="14.83" y1="14.83" x2="19.07" y2="19.07"></line><line x1="14.83" y1="9.17" x2="19.07" y2="4.93"></line><line x1="14.83" y1="9.17" x2="18.36" y2="5.64"></line><line x1="4.93" y1="19.07" x2="9.17" y2="14.83"></line>',
link: '<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>',
"link-2":
'<path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path><line x1="8" y1="12" x2="16" y2="12"></line>',
linkedin:
'<path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"></path><rect x="2" y="9" width="4" height="12"></rect><circle cx="4" cy="4" r="2"></circle>',
list: '<line x1="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"></line><line x1="8" y1="18" x2="21" y2="18"></line><line x1="3" y1="6" x2="3.01" y2="6"></line><line x1="3" y1="12" x2="3.01" y2="12"></line><line x1="3" y1="18" x2="3.01" y2="18"></line>',
loader:
'<line x1="12" y1="2" x2="12" y2="6"></line><line x1="12" y1="18" x2="12" y2="22"></line><line x1="4.93" y1="4.93" x2="7.76" y2="7.76"></line><line x1="16.24" y1="16.24" x2="19.07" y2="19.07"></line><line x1="2" y1="12" x2="6" y2="12"></line><line x1="18" y1="12" x2="22" y2="12"></line><line x1="4.93" y1="19.07" x2="7.76" y2="16.24"></line><line x1="16.24" y1="7.76" x2="19.07" y2="4.93"></line>',
lock: '<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path>',
"log-in":
'<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"></path><polyline points="10 17 15 12 10 7"></polyline><line x1="15" y1="12" x2="3" y2="12"></line>',
"log-out":
'<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path><polyline points="16 17 21 12 16 7"></polyline><line x1="21" y1="12" x2="9" y2="12"></line>',
mail: '<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline>',
map: '<polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"></polygon><line x1="8" y1="2" x2="8" y2="18"></line><line x1="16" y1="6" x2="16" y2="22"></line>',
"map-pin":
'<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle>',
maximize:
'<path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"></path>',
"maximize-2":
'<polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" y1="3" x2="14" y2="10"></line><line x1="3" y1="21" x2="10" y2="14"></line>',
meh: '<circle cx="12" cy="12" r="10"></circle><line x1="8" y1="15" x2="16" y2="15"></line><line x1="9" y1="9" x2="9.01" y2="9"></line><line x1="15" y1="9" x2="15.01" y2="9"></line>',
menu: '<line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="18" x2="21" y2="18"></line>',
"message-circle":
'<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path>',
"message-square":
'<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>',
mic: '<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path><path d="M19 10v2a7 7 0 0 1-14 0v-2"></path><line x1="12" y1="19" x2="12" y2="23"></line><line x1="8" y1="23" x2="16" y2="23"></line>',
"mic-off":
'<line x1="1" y1="1" x2="23" y2="23"></line><path d="M9 9v3a3 3 0 0 0 5.12 2.12M15 9.34V4a3 3 0 0 0-5.94-.6"></path><path d="M17 16.95A7 7 0 0 1 5 12v-2m14 0v2a7 7 0 0 1-.11 1.23"></path><line x1="12" y1="19" x2="12" y2="23"></line><line x1="8" y1="23" x2="16" y2="23"></line>',
minimize:
'<path d="M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3"></path>',
"minimize-2":
'<polyline points="4 14 10 14 10 20"></polyline><polyline points="20 10 14 10 14 4"></polyline><line x1="14" y1="10" x2="21" y2="3"></line><line x1="3" y1="21" x2="10" y2="14"></line>',
minus: '<line x1="5" y1="12" x2="19" y2="12"></line>',
"minus-circle":
'<circle cx="12" cy="12" r="10"></circle><line x1="8" y1="12" x2="16" y2="12"></line>',
"minus-square":
'<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="8" y1="12" x2="16" y2="12"></line>',
monitor:
'<rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect><line x1="8" y1="21" x2="16" y2="21"></line><line x1="12" y1="17" x2="12" y2="21"></line>',
moon: '<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>',
"more-horizontal":
'<circle cx="12" cy="12" r="1"></circle><circle cx="19" cy="12" r="1"></circle><circle cx="5" cy="12" r="1"></circle>',
"more-vertical":
'<circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="19" r="1"></circle>',
"mouse-pointer":
'<path d="M3 3l7.07 16.97 2.51-7.39 7.39-2.51L3 3z"></path><path d="M13 13l6 6"></path>',
move: '<polyline points="5 9 2 12 5 15"></polyline><polyline points="9 5 12 2 15 5"></polyline><polyline points="15 19 12 22 9 19"></polyline><polyline points="19 9 22 12 19 15"></polyline><line x1="2" y1="12" x2="22" y2="12"></line><line x1="12" y1="2" x2="12" y2="22"></line>',
music:
'<path d="M9 18V5l12-2v13"></path><circle cx="6" cy="18" r="3"></circle><circle cx="18" cy="16" r="3"></circle>',
navigation: '<polygon points="3 11 22 2 13 21 11 13 3 11"></polygon>',
"navigation-2": '<polygon points="12 2 19 21 12 17 5 21 12 2"></polygon>',
octagon:
'<polygon points="7.86 2 16.14 2 22 7.86 22 16.14 16.14 22 7.86 22 2 16.14 2 7.86 7.86 2"></polygon>',
package:
'<line x1="16.5" y1="9.4" x2="7.5" y2="4.21"></line><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path><polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline><line x1="12" y1="22.08" x2="12" y2="12"></line>',
paperclip:
'<path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"></path>',
pause:
'<rect x="6" y="4" width="4" height="16"></rect><rect x="14" y="4" width="4" height="16"></rect>',
"pause-circle":
'<circle cx="12" cy="12" r="10"></circle><line x1="10" y1="15" x2="10" y2="9"></line><line x1="14" y1="15" x2="14" y2="9"></line>',
"pen-tool":
'<path d="M12 19l7-7 3 3-7 7-3-3z"></path><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"></path><path d="M2 2l7.586 7.586"></path><circle cx="11" cy="11" r="2"></circle>',
percent:
'<line x1="19" y1="5" x2="5" y2="19"></line><circle cx="6.5" cy="6.5" r="2.5"></circle><circle cx="17.5" cy="17.5" r="2.5"></circle>',
phone:
'<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path>',
"phone-call":
'<path d="M15.05 5A5 5 0 0 1 19 8.95M15.05 1A9 9 0 0 1 23 8.94m-1 7.98v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path>',
"phone-forwarded":
'<polyline points="19 1 23 5 19 9"></polyline><line x1="15" y1="5" x2="23" y2="5"></line><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path>',
"phone-incoming":
'<polyline points="16 2 16 8 22 8"></polyline><line x1="23" y1="1" x2="16" y2="8"></line><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path>',
"phone-missed":
'<line x1="23" y1="1" x2="17" y2="7"></line><line x1="17" y1="1" x2="23" y2="7"></line><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path>',
"phone-off":
'<path d="M10.68 13.31a16 16 0 0 0 3.41 2.6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7 2 2 0 0 1 1.72 2v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.42 19.42 0 0 1-3.33-2.67m-2.67-3.34a19.79 19.79 0 0 1-3.07-8.63A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91"></path><line x1="23" y1="1" x2="1" y2="23"></line>',
"phone-outgoing":
'<polyline points="23 7 23 1 17 1"></polyline><line x1="16" y1="8" x2="23" y2="1"></line><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path>',
"pie-chart":
'<path d="M21.21 15.89A10 10 0 1 1 8 2.83"></path><path d="M22 12A10 10 0 0 0 12 2v10z"></path>',
play: '<polygon points="5 3 19 12 5 21 5 3"></polygon>',
"play-circle":
'<circle cx="12" cy="12" r="10"></circle><polygon points="10 8 16 12 10 16 10 8"></polygon>',
plus: '<line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line>',
"plus-circle":
'<circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="16"></line><line x1="8" y1="12" x2="16" y2="12"></line>',
"plus-square":
'<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="12" y1="8" x2="12" y2="16"></line><line x1="8" y1="12" x2="16" y2="12"></line>',
pocket:
'<path d="M4 3h16a2 2 0 0 1 2 2v6a10 10 0 0 1-10 10A10 10 0 0 1 2 11V5a2 2 0 0 1 2-2z"></path><polyline points="8 10 12 14 16 10"></polyline>',
power:
'<path d="M18.36 6.64a9 9 0 1 1-12.73 0"></path><line x1="12" y1="2" x2="12" y2="12"></line>',
printer:
'<polyline points="6 9 6 2 18 2 18 9"></polyline><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path><rect x="6" y="14" width="12" height="8"></rect>',
radio:
'<circle cx="12" cy="12" r="2"></circle><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49m11.31-2.82a10 10 0 0 1 0 14.14m-14.14 0a10 10 0 0 1 0-14.14"></path>',
"refresh-ccw":
'<polyline points="1 4 1 10 7 10"></polyline><polyline points="23 20 23 14 17 14"></polyline><path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"></path>',
"refresh-cw":
'<polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path>',
repeat:
'<polyline points="17 1 21 5 17 9"></polyline><path d="M3 11V9a4 4 0 0 1 4-4h14"></path><polyline points="7 23 3 19 7 15"></polyline><path d="M21 13v2a4 4 0 0 1-4 4H3"></path>',
rewind:
'<polygon points="11 19 2 12 11 5 11 19"></polygon><polygon points="22 19 13 12 22 5 22 19"></polygon>',
"rotate-ccw":
'<polyline points="1 4 1 10 7 10"></polyline><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"></path>',
"rotate-cw":
'<polyline points="23 4 23 10 17 10"></polyline><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>',
rss: '<path d="M4 11a9 9 0 0 1 9 9"></path><path d="M4 4a16 16 0 0 1 16 16"></path><circle cx="5" cy="19" r="1"></circle>',
save: '<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path><polyline points="17 21 17 13 7 13 7 21"></polyline><polyline points="7 3 7 8 15 8"></polyline>',
scissors:
'<circle cx="6" cy="6" r="3"></circle><circle cx="6" cy="18" r="3"></circle><line x1="20" y1="4" x2="8.12" y2="15.88"></line><line x1="14.47" y1="14.48" x2="20" y2="20"></line><line x1="8.12" y1="8.12" x2="12" y2="12"></line>',
search:
'<circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line>',
send: '<line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>',
server:
'<rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect><rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect><line x1="6" y1="6" x2="6.01" y2="6"></line><line x1="6" y1="18" x2="6.01" y2="18"></line>',
settings:
'<circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>',
share:
'<path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"></path><polyline points="16 6 12 2 8 6"></polyline><line x1="12" y1="2" x2="12" y2="15"></line>',
"share-2":
'<circle cx="18" cy="5" r="3"></circle><circle cx="6" cy="12" r="3"></circle><circle cx="18" cy="19" r="3"></circle><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"></line><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"></line>',
shield: '<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path>',
"shield-off":
'<path d="M19.69 14a6.9 6.9 0 0 0 .31-2V5l-8-3-3.16 1.18"></path><path d="M4.73 4.73L4 5v7c0 6 8 10 8 10a20.29 20.29 0 0 0 5.62-4.38"></path><line x1="1" y1="1" x2="23" y2="23"></line>',
"shopping-bag":
'<path d="M6 2L3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"></path><line x1="3" y1="6" x2="21" y2="6"></line><path d="M16 10a4 4 0 0 1-8 0"></path>',
"shopping-cart":
'<circle cx="9" cy="21" r="1"></circle><circle cx="20" cy="21" r="1"></circle><path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"></path>',
shuffle:
'<polyline points="16 3 21 3 21 8"></polyline><line x1="4" y1="20" x2="21" y2="3"></line><polyline points="21 16 21 21 16 21"></polyline><line x1="15" y1="15" x2="21" y2="21"></line><line x1="4" y1="4" x2="9" y2="9"></line>',
sidebar:
'<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="9" y1="3" x2="9" y2="21"></line>',
"skip-back":
'<polygon points="19 20 9 12 19 4 19 20"></polygon><line x1="5" y1="19" x2="5" y2="5"></line>',
"skip-forward":
'<polygon points="5 4 15 12 5 20 5 4"></polygon><line x1="19" y1="5" x2="19" y2="19"></line>',
slack:
'<path d="M14.5 10c-.83 0-1.5-.67-1.5-1.5v-5c0-.83.67-1.5 1.5-1.5s1.5.67 1.5 1.5v5c0 .83-.67 1.5-1.5 1.5z"></path><path d="M20.5 10H19V8.5c0-.83.67-1.5 1.5-1.5s1.5.67 1.5 1.5-.67 1.5-1.5 1.5z"></path><path d="M9.5 14c.83 0 1.5.67 1.5 1.5v5c0 .83-.67 1.5-1.5 1.5S8 21.33 8 20.5v-5c0-.83.67-1.5 1.5-1.5z"></path><path d="M3.5 14H5v1.5c0 .83-.67 1.5-1.5 1.5S2 16.33 2 15.5 2.67 14 3.5 14z"></path><path d="M14 14.5c0-.83.67-1.5 1.5-1.5h5c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5h-5c-.83 0-1.5-.67-1.5-1.5z"></path><path d="M15.5 19H14v1.5c0 .83.67 1.5 1.5 1.5s1.5-.67 1.5-1.5-.67-1.5-1.5-1.5z"></path><path d="M10 9.5C10 8.67 9.33 8 8.5 8h-5C2.67 8 2 8.67 2 9.5S2.67 11 3.5 11h5c.83 0 1.5-.67 1.5-1.5z"></path><path d="M8.5 5H10V3.5C10 2.67 9.33 2 8.5 2S7 2.67 7 3.5 7.67 5 8.5 5z"></path>',
slash:
'<circle cx="12" cy="12" r="10"></circle><line x1="4.93" y1="4.93" x2="19.07" y2="19.07"></line>',
sliders:
'<line x1="4" y1="21" x2="4" y2="14"></line><line x1="4" y1="10" x2="4" y2="3"></line><line x1="12" y1="21" x2="12" y2="12"></line><line x1="12" y1="8" x2="12" y2="3"></line><line x1="20" y1="21" x2="20" y2="16"></line><line x1="20" y1="12" x2="20" y2="3"></line><line x1="1" y1="14" x2="7" y2="14"></line><line x1="9" y1="8" x2="15" y2="8"></line><line x1="17" y1="16" x2="23" y2="16"></line>',
smartphone:
'<rect x="5" y="2" width="14" height="20" rx="2" ry="2"></rect><line x1="12" y1="18" x2="12.01" y2="18"></line>',
smile:
'<circle cx="12" cy="12" r="10"></circle><path d="M8 14s1.5 2 4 2 4-2 4-2"></path><line x1="9" y1="9" x2="9.01" y2="9"></line><line x1="15" y1="9" x2="15.01" y2="9"></line>',
speaker:
'<rect x="4" y="2" width="16" height="20" rx="2" ry="2"></rect><circle cx="12" cy="14" r="4"></circle><line x1="12" y1="6" x2="12.01" y2="6"></line>',
square: '<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>',
star: '<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>',
"stop-circle":
'<circle cx="12" cy="12" r="10"></circle><rect x="9" y="9" width="6" height="6"></rect>',
sun: '<circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>',
sunrise:
'<path d="M17 18a5 5 0 0 0-10 0"></path><line x1="12" y1="2" x2="12" y2="9"></line><line x1="4.22" y1="10.22" x2="5.64" y2="11.64"></line><line x1="1" y1="18" x2="3" y2="18"></line><line x1="21" y1="18" x2="23" y2="18"></line><line x1="18.36" y1="11.64" x2="19.78" y2="10.22"></line><line x1="23" y1="22" x2="1" y2="22"></line><polyline points="8 6 12 2 16 6"></polyline>',
sunset:
'<path d="M17 18a5 5 0 0 0-10 0"></path><line x1="12" y1="9" x2="12" y2="2"></line><line x1="4.22" y1="10.22" x2="5.64" y2="11.64"></line><line x1="1" y1="18" x2="3" y2="18"></line><line x1="21" y1="18" x2="23" y2="18"></line><line x1="18.36" y1="11.64" x2="19.78" y2="10.22"></line><line x1="23" y1="22" x2="1" y2="22"></line><polyline points="16 5 12 9 8 5"></polyline>',
table:
'<path d="M9 3H5a2 2 0 0 0-2 2v4m6-6h10a2 2 0 0 1 2 2v4M9 3v18m0 0h10a2 2 0 0 0 2-2V9M9 21H5a2 2 0 0 1-2-2V9m0 0h18"></path>',
tablet:
'<rect x="4" y="2" width="16" height="20" rx="2" ry="2"></rect><line x1="12" y1="18" x2="12.01" y2="18"></line>',
tag: '<path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line>',
target:
'<circle cx="12" cy="12" r="10"></circle><circle cx="12" cy="12" r="6"></circle><circle cx="12" cy="12" r="2"></circle>',
terminal:
'<polyline points="4 17 10 11 4 5"></polyline><line x1="12" y1="19" x2="20" y2="19"></line>',
thermometer:
'<path d="M14 14.76V3.5a2.5 2.5 0 0 0-5 0v11.26a4.5 4.5 0 1 0 5 0z"></path>',
"thumbs-down":
'<path d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"></path>',
"thumbs-up":
'<path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"></path>',
"toggle-left":
'<rect x="1" y="5" width="22" height="14" rx="7" ry="7"></rect><circle cx="8" cy="12" r="3"></circle>',
"toggle-right":
'<rect x="1" y="5" width="22" height="14" rx="7" ry="7"></rect><circle cx="16" cy="12" r="3"></circle>',
tool: '<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path>',
trash:
'<polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>',
"trash-2":
'<polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line>',
trello:
'<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><rect x="7" y="7" width="3" height="9"></rect><rect x="14" y="7" width="3" height="5"></rect>',
"trending-down":
'<polyline points="23 18 13.5 8.5 8.5 13.5 1 6"></polyline><polyline points="17 18 23 18 23 12"></polyline>',
"trending-up":
'<polyline points="23 6 13.5 15.5 8.5 10.5 1 18"></polyline><polyline points="17 6 23 6 23 12"></polyline>',
triangle:
'<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>',
truck:
'<rect x="1" y="3" width="15" height="13"></rect><polygon points="16 8 20 8 23 11 23 16 16 16 16 8"></polygon><circle cx="5.5" cy="18.5" r="2.5"></circle><circle cx="18.5" cy="18.5" r="2.5"></circle>',
tv: '<rect x="2" y="7" width="20" height="15" rx="2" ry="2"></rect><polyline points="17 2 12 7 7 2"></polyline>',
twitch: '<path d="M21 2H3v16h5v4l4-4h5l4-4V2zm-10 9V7m5 4V7"></path>',
twitter:
'<path d="M23 3a10.9 10.9 0 0 1-3.14 1.53 4.48 4.48 0 0 0-7.86 3v1A10.66 10.66 0 0 1 3 4s-4 9 5 13a11.64 11.64 0 0 1-7 2c9 5 20 0 20-11.5a4.5 4.5 0 0 0-.08-.83A7.72 7.72 0 0 0 23 3z"></path>',
type: '<polyline points="4 7 4 4 20 4 20 7"></polyline><line x1="9" y1="20" x2="15" y2="20"></line><line x1="12" y1="4" x2="12" y2="20"></line>',
umbrella:
'<path d="M23 12a11.05 11.05 0 0 0-22 0zm-5 7a3 3 0 0 1-6 0v-7"></path>',
underline:
'<path d="M6 3v7a6 6 0 0 0 6 6 6 6 0 0 0 6-6V3"></path><line x1="4" y1="21" x2="20" y2="21"></line>',
unlock:
'<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 9.9-1"></path>',
upload:
'<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="17 8 12 3 7 8"></polyline><line x1="12" y1="3" x2="12" y2="15"></line>',
"upload-cloud":
'<polyline points="16 16 12 12 8 16"></polyline><line x1="12" y1="12" x2="12" y2="21"></line><path d="M20.39 18.39A5 5 0 0 0 18 9h-1.26A8 8 0 1 0 3 16.3"></path><polyline points="16 16 12 12 8 16"></polyline>',
user: '<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle>',
"user-check":
'<path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><polyline points="17 11 19 13 23 9"></polyline>',
"user-minus":
'<path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="23" y1="11" x2="17" y2="11"></line>',
"user-plus":
'<path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="20" y1="8" x2="20" y2="14"></line><line x1="23" y1="11" x2="17" y2="11"></line>',
"user-x":
'<path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="18" y1="8" x2="23" y2="13"></line><line x1="23" y1="8" x2="18" y2="13"></line>',
users:
'<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path>',
video:
'<polygon points="23 7 16 12 23 17 23 7"></polygon><rect x="1" y="5" width="15" height="14" rx="2" ry="2"></rect>',
"video-off":
'<path d="M16 16v1a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h2m5.66 0H14a2 2 0 0 1 2 2v3.34l1 1L23 7v10"></path><line x1="1" y1="1" x2="23" y2="23"></line>',
voicemail:
'<circle cx="5.5" cy="11.5" r="4.5"></circle><circle cx="18.5" cy="11.5" r="4.5"></circle><line x1="5.5" y1="16" x2="18.5" y2="16"></line>',
volume: '<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon>',
"volume-1":
'<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon><path d="M15.54 8.46a5 5 0 0 1 0 7.07"></path>',
"volume-2":
'<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon><path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"></path>',
"volume-x":
'<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon><line x1="23" y1="9" x2="17" y2="15"></line><line x1="17" y1="9" x2="23" y2="15"></line>',
watch:
'<circle cx="12" cy="12" r="7"></circle><polyline points="12 9 12 12 13.5 13.5"></polyline><path d="M16.51 17.35l-.35 3.83a2 2 0 0 1-2 1.82H9.83a2 2 0 0 1-2-1.82l-.35-3.83m.01-10.7l.35-3.83A2 2 0 0 1 9.83 1h4.35a2 2 0 0 1 2 1.82l.35 3.83"></path>',
wifi: '<path d="M5 12.55a11 11 0 0 1 14.08 0"></path><path d="M1.42 9a16 16 0 0 1 21.16 0"></path><path d="M8.53 16.11a6 6 0 0 1 6.95 0"></path><line x1="12" y1="20" x2="12.01" y2="20"></line>',
"wifi-off":
'<line x1="1" y1="1" x2="23" y2="23"></line><path d="M16.72 11.06A10.94 10.94 0 0 1 19 12.55"></path><path d="M5 12.55a10.94 10.94 0 0 1 5.17-2.39"></path><path d="M10.71 5.05A16 16 0 0 1 22.58 9"></path><path d="M1.42 9a15.91 15.91 0 0 1 4.7-2.88"></path><path d="M8.53 16.11a6 6 0 0 1 6.95 0"></path><line x1="12" y1="20" x2="12.01" y2="20"></line>',
wind: '<path d="M9.59 4.59A2 2 0 1 1 11 8H2m10.59 11.41A2 2 0 1 0 14 16H2m15.73-8.27A2.5 2.5 0 1 1 19.5 12H2"></path>',
x: '<line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line>',
"x-circle":
'<circle cx="12" cy="12" r="10"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line>',
"x-octagon":
'<polygon points="7.86 2 16.14 2 22 7.86 22 16.14 16.14 22 7.86 22 2 16.14 2 7.86 7.86 2"></polygon><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line>',
"x-square":
'<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="9" y1="9" x2="15" y2="15"></line><line x1="15" y1="9" x2="9" y2="15"></line>',
youtube:
'<path d="M22.54 6.42a2.78 2.78 0 0 0-1.94-2C18.88 4 12 4 12 4s-6.88 0-8.6.46a2.78 2.78 0 0 0-1.94 2A29 29 0 0 0 1 11.75a29 29 0 0 0 .46 5.33A2.78 2.78 0 0 0 3.4 19c1.72.46 8.6.46 8.6.46s6.88 0 8.6-.46a2.78 2.78 0 0 0 1.94-2 29 29 0 0 0 .46-5.25 29 29 0 0 0-.46-5.33z"></path><polygon points="9.75 15.02 15.5 11.75 9.75 8.48 9.75 15.02"></polygon>',
zap: '<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon>',
"zap-off":
'<polyline points="12.41 6.75 13 2 10.57 4.92"></polyline><polyline points="18.57 12.91 21 10 15.66 10"></polyline><polyline points="8 8 3 14 12 14 11 22 16 16"></polyline><line x1="1" y1="1" x2="23" y2="23"></line>',
"zoom-in":
'<circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line>',
"zoom-out":
'<circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="8" y1="11" x2="14" y2="11"></line>',
"flag-es":
'<rect x="0" y="0" width="300" height="200" rx="20" ry="20" fill="#AA151B"/><rect x="0" y="50" width="300" height="100" rx="20" ry="20" fill="#F1BF00"/>',
"flag-en":
'<clipPath id="t"><path d="M0,0 v30 h60 v-30 z" /></clipPath><g clip-path="url(#t)"><path d="M0,0 v30 h60 v-30 z" fill="#012169" /><path d="M0,0 L60,30 M60,0 L0,30" stroke="#fff" stroke-width="6" /><path d="M0,0 L60,30 M60,0 L0,30" stroke="#C8102E" stroke-width="4" /><path d="M30,0 v30 M0,15 h60" stroke="#fff" stroke-width="10" /><path d="M30,0 v30 M0,15 h60" stroke="#C8102E" stroke-width="6" /></g>',
};

38
src/models/Drawing.ts Normal file
View File

@ -0,0 +1,38 @@
import mongoose, { Document, Schema } from "mongoose";
export interface IDrawing extends Document {
id: string;
owner_id: string;
title: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data: any;
shared_with: string[];
createdAt: Date;
updatedAt: Date;
}
const DrawingSchema = new Schema<IDrawing>(
{
owner_id: {
type: String,
required: true,
},
title: {
type: String,
required: true,
},
data: {
type: Schema.Types.Mixed,
required: true,
},
shared_with: {
type: [String],
},
},
{
timestamps: true,
}
);
export default mongoose.models.Drawing ||
mongoose.model<IDrawing>("Drawing", DrawingSchema);

View File

@ -3368,6 +3368,11 @@ jws@^3.2.2:
jwa "^1.4.1"
safe-buffer "^5.0.1"
jwt-decode@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-4.0.0.tgz#2270352425fd413785b2faf11f6e755c5151bd4b"
integrity sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==
kareem@2.6.3:
version "2.6.3"
resolved "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz"