SAVE
This commit is contained in:
parent
0ebef51f5c
commit
ddd0eba390
|
@ -1 +1,2 @@
|
|||
JWT_SECRET=146acd46a3932b75db1cf64b21753ee1b933dd73e2fdff3eab8f000f6a7493e7
|
||||
MONGODB_URI=mongodb+srv://adrianborrageirosmourelos:FOcz19ZnmULvFZeY@cluster0.gf3secu.mongodb.net/draw_dev?retryWrites=true&w=majority&appName=Cluster0
|
|
@ -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",
|
||||
|
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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 };
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 };
|
||||
}
|
||||
}
|
161
src/lib/api.ts
161
src/lib/api.ts
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
|
@ -30,7 +30,6 @@ export function LanguageProvider({ children }: { children: ReactNode }) {
|
|||
if (locales.includes(newLocale)) {
|
||||
setLocale(newLocale);
|
||||
localStorage.setItem('locale', newLocale);
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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>',
|
||||
};
|
|
@ -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);
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue
Block a user