criado API e primeiros cruds com a LP
This commit is contained in:
parent
ff34c5ab8a
commit
d47468238a
|
|
@ -0,0 +1,19 @@
|
||||||
|
# =====================
|
||||||
|
# API
|
||||||
|
# =====================
|
||||||
|
PORT=3000
|
||||||
|
NODE_ENV=development
|
||||||
|
|
||||||
|
# =====================
|
||||||
|
# MYSQL
|
||||||
|
# =====================
|
||||||
|
DB_HOST=18.228.91.135
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_NAME=lojazoe
|
||||||
|
DB_USER=root
|
||||||
|
DB_PASS=maker2018**==
|
||||||
|
|
||||||
|
# TRAY
|
||||||
|
TRAY_API_BASE_URL=https://seu-subdominio.tray.com.br/web_api
|
||||||
|
TRAY_API_TOKEN=SEU_ACCESS_TOKEN_AQUI
|
||||||
|
TRAY_STORE_URL=https://seu-subdominio.tray.com.br
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
// config/connector.js
|
||||||
|
const mysql = require("mysql2/promise");
|
||||||
|
|
||||||
|
let pool = null;
|
||||||
|
|
||||||
|
async function initDB() {
|
||||||
|
if (pool) return pool;
|
||||||
|
|
||||||
|
pool = mysql.createPool({
|
||||||
|
host: process.env.DB_HOST,
|
||||||
|
port: process.env.DB_PORT,
|
||||||
|
user: process.env.DB_USER,
|
||||||
|
password: process.env.DB_PASS,
|
||||||
|
database: process.env.DB_NAME,
|
||||||
|
|
||||||
|
waitForConnections: true,
|
||||||
|
connectionLimit: 10,
|
||||||
|
queueLimit: 0,
|
||||||
|
timezone: "Z", // UTC (bom pra sistemas distribuídos)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Teste de conexão imediato
|
||||||
|
try {
|
||||||
|
const conn = await pool.getConnection();
|
||||||
|
await conn.ping();
|
||||||
|
conn.release();
|
||||||
|
console.log("✅ MySQL conectado com sucesso");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ Erro ao conectar no MySQL:", err.message);
|
||||||
|
process.exit(1); // mata a API se o banco não subir
|
||||||
|
}
|
||||||
|
|
||||||
|
return pool;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function query(sql, params = []) {
|
||||||
|
if (!pool) {
|
||||||
|
await initDB();
|
||||||
|
}
|
||||||
|
|
||||||
|
const [rows] = await pool.execute(sql, params);
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
initDB,
|
||||||
|
query,
|
||||||
|
};
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"name": "api",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "server.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"start": "node server.js"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.13.3",
|
||||||
|
"cors": "^2.8.6",
|
||||||
|
"dotenv": "^17.2.3",
|
||||||
|
"express": "^5.2.1",
|
||||||
|
"mysql2": "^3.16.2",
|
||||||
|
"nodemon": "^3.1.11"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
const express = require("express");
|
||||||
|
const router = express.Router();
|
||||||
|
const { query } = require("../config/connector");
|
||||||
|
const { listCourses } = require("../services/tray_service");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /courses
|
||||||
|
* Lista cursos disponíveis para venda (LP)
|
||||||
|
*/
|
||||||
|
router.get("/list", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const cursos = await query(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
nome,
|
||||||
|
descricao,
|
||||||
|
icone,
|
||||||
|
preco,
|
||||||
|
ativado
|
||||||
|
FROM cursos
|
||||||
|
WHERE ativado = 1
|
||||||
|
ORDER BY nome ASC
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
ok: true,
|
||||||
|
cursos,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Erro ao buscar cursos:", err);
|
||||||
|
return res.status(500).json({
|
||||||
|
ok: false,
|
||||||
|
error: "Erro ao buscar cursos",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get("/listTray", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const courses = await listCourses();
|
||||||
|
return res.json({
|
||||||
|
ok: true,
|
||||||
|
courses,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Erro ao listar cursos na Tray:", err.response?.data || err);
|
||||||
|
return res.status(500).json({
|
||||||
|
ok: false,
|
||||||
|
error: "Erro ao buscar cursos na Tray",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
const express = require("express");
|
||||||
|
const router = express.Router();
|
||||||
|
const { getOrderById } = require("../services/tray_service");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Webhook de notificações de pedidos da Tray
|
||||||
|
* URL: POST /webhooks/tray/orders
|
||||||
|
*/
|
||||||
|
router.post("/tray/orders", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const payload = req.body;
|
||||||
|
console.log("📩 Webhook Tray recebido:", JSON.stringify(payload, null, 2));
|
||||||
|
|
||||||
|
// Aqui depende de como a Tray manda o payload:
|
||||||
|
// vamos supor que venha algo como:
|
||||||
|
// { order_id: 123, status: 'paid', ... }
|
||||||
|
|
||||||
|
const orderId = payload.order_id || payload.id || null;
|
||||||
|
|
||||||
|
if (!orderId) {
|
||||||
|
console.warn("Webhook Tray sem order_id identificável");
|
||||||
|
// Mesmo assim respondemos 200 pra não gerar reenvio infinito
|
||||||
|
return res.status(200).json({ received: true, ignored: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// (Opcional) Buscar detalhes do pedido na Tray pra ter certeza do status atual
|
||||||
|
try {
|
||||||
|
const orderData = await getOrderById(orderId);
|
||||||
|
console.log("🔍 Detalhes do pedido na Tray:", JSON.stringify(orderData, null, 2));
|
||||||
|
|
||||||
|
// Aqui você decide o que fazer:
|
||||||
|
// - Se status for pago -> mandar e-mail
|
||||||
|
// - Gravar log em arquivo
|
||||||
|
// - No futuro: salvar em banco, liberar curso, etc.
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Erro ao buscar pedido na Tray:", err.response?.data || err.message);
|
||||||
|
// Mesmo assim, respondemos 200 pra não ficar em loop de reenvio
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sempre responde 200 pra dizer "recebido"
|
||||||
|
return res.status(200).json({ received: true });
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Erro no webhook /webhooks/tray/orders:", err);
|
||||||
|
// Em webhook, na maioria dos casos ainda é melhor responder 200
|
||||||
|
// para não causar reenvios em massa
|
||||||
|
return res.status(200).json({ received: true, error: "internal error" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
// server.js
|
||||||
|
require("dotenv").config();
|
||||||
|
const express = require("express");
|
||||||
|
const cors = require("cors");
|
||||||
|
const { initDB } = require("./config/connector");
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
// Porta da API (padrão 3000 se não tiver no .env)
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
// Middlewares básicos
|
||||||
|
// 👇 CORS aqui
|
||||||
|
app.use(
|
||||||
|
cors({
|
||||||
|
origin: "http://localhost:8080", // origem do teu front
|
||||||
|
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||||
|
allowedHeaders: ["Content-Type", "Authorization"],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
app.use(express.json()); // parseia JSON no body
|
||||||
|
|
||||||
|
|
||||||
|
// Sobe o servidor
|
||||||
|
async function startServer() {
|
||||||
|
await initDB(); // garante que o banco está ok antes de subir a API
|
||||||
|
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`🚀 Zoe API rodando em http://localhost:${PORT}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
app.use("/courses", require("./routes/courses"));
|
||||||
|
app.use("/webhooks", require("./routes/webhooks"));
|
||||||
|
|
||||||
|
|
||||||
|
startServer();
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
// services/trayService.js
|
||||||
|
const axios = require("axios");
|
||||||
|
|
||||||
|
const trayApi = axios.create({
|
||||||
|
baseURL: process.env.TRAY_API_BASE_URL,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Se a Tray exigir token por header ou querystring, você ajusta aqui
|
||||||
|
function getAuthParams() {
|
||||||
|
return {
|
||||||
|
access_token: process.env.TRAY_API_TOKEN,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lista cursos (produtos) na Tray.
|
||||||
|
* Aqui você pode filtrar por categoria, tag, etc, dependendo de como organizar lá.
|
||||||
|
*/
|
||||||
|
async function listCourses() {
|
||||||
|
// Exemplo genérico: ajuste conforme o endpoint real da Tray
|
||||||
|
const resp = await trayApi.get("/products", {
|
||||||
|
params: {
|
||||||
|
...getAuthParams(),
|
||||||
|
// filtros opcionais:
|
||||||
|
// category_id: 123,
|
||||||
|
// active: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const products = resp.data?.Products || resp.data?.products || [];
|
||||||
|
|
||||||
|
// Mapeia para um modelo mais limpo pro front
|
||||||
|
const courses = products.map((p) => {
|
||||||
|
return {
|
||||||
|
id: p.id,
|
||||||
|
name: p.name,
|
||||||
|
description: p.description_small || p.description || "",
|
||||||
|
price: p.price,
|
||||||
|
promo_price: p.promotional_price || null,
|
||||||
|
image_url: p.images?.[0]?.https || null,
|
||||||
|
// URL pública do produto na loja Tray (exemplo)
|
||||||
|
trayUrl: `${process.env.TRAY_STORE_URL}/produto/${p.id}/${slugify(
|
||||||
|
p.name
|
||||||
|
)}`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return courses;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getOrderById(orderId) {
|
||||||
|
// Exemplo genérico, ajuste conforme a rota real da Tray
|
||||||
|
const resp = await trayApi.get(`/orders/${orderId}`, {
|
||||||
|
params: getAuthParams(),
|
||||||
|
});
|
||||||
|
return resp.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Função simples pra criar um slug a partir do nome (opcional)
|
||||||
|
function slugify(str = "") {
|
||||||
|
return str
|
||||||
|
.normalize("NFD")
|
||||||
|
.replace(/[\u0300-\u036f]/g, "")
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]+/g, "-")
|
||||||
|
.replace(/(^-|-$)+/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
listCourses,
|
||||||
|
getOrderById
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"name": "ZoeBuyCourses",
|
||||||
|
"lockfileVersion": 2,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { Smartphone, Users, BookOpen, Gift } from "lucide-react";
|
import { useState } from "react";
|
||||||
import ExtraProductCard from "./ExtraProductCard";
|
import ExtraProductCard from "./ExtraProductCard";
|
||||||
|
import { iconMap } from "@/utils/iconMap";
|
||||||
|
|
||||||
interface ExtraProduct {
|
interface ExtraProduct {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -10,47 +11,21 @@ interface ExtraProduct {
|
||||||
iconBgClass: string;
|
iconBgClass: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Extra {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
price: number;
|
||||||
|
icon: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface ExtrasSectionProps {
|
interface ExtrasSectionProps {
|
||||||
|
extras: Extra[];
|
||||||
selectedExtras: string[];
|
selectedExtras: string[];
|
||||||
onToggleExtra: (id: string) => void;
|
onToggleExtra: (id: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const extraProducts: ExtraProduct[] = [
|
const ExtrasSection = ({ extras, selectedExtras, onToggleExtra }: ExtrasSectionProps) => {
|
||||||
{
|
|
||||||
id: "device",
|
|
||||||
title: "Novo Dispositivo ZOE",
|
|
||||||
description: "Adicione a ZOE em mais um tablet ou celular da família.",
|
|
||||||
price: 29.9,
|
|
||||||
icon: <Smartphone className="w-7 h-7 text-zoe-blue" />,
|
|
||||||
iconBgClass: "bg-zoe-blue/20",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "license",
|
|
||||||
title: "Licença Adicional",
|
|
||||||
description: "Para outro filho ou familiar. Mesmo prazo do plano escolhido.",
|
|
||||||
price: 19.9,
|
|
||||||
icon: <Users className="w-7 h-7 text-zoe-pink" />,
|
|
||||||
iconBgClass: "bg-zoe-pink/20",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "book1",
|
|
||||||
title: "Livro de Atividades Vol. 1",
|
|
||||||
description: "Atividades educativas impressas para fazer com a família.",
|
|
||||||
price: 34.9,
|
|
||||||
icon: <BookOpen className="w-7 h-7 text-zoe-mint" />,
|
|
||||||
iconBgClass: "bg-zoe-mint/20",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "book2",
|
|
||||||
title: "Livro de Atividades Vol. 2",
|
|
||||||
description: "Mais atividades para diversão e aprendizado offline.",
|
|
||||||
price: 34.9,
|
|
||||||
icon: <Gift className="w-7 h-7 text-zoe-orange" />,
|
|
||||||
iconBgClass: "bg-zoe-orange/20",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const ExtrasSection = ({ selectedExtras, onToggleExtra }: ExtrasSectionProps) => {
|
|
||||||
return (
|
return (
|
||||||
<section className="zoe-section bg-secondary/30">
|
<section className="zoe-section bg-secondary/30">
|
||||||
<div className="container mx-auto px-4">
|
<div className="container mx-auto px-4">
|
||||||
|
|
@ -66,11 +41,16 @@ const ExtrasSection = ({ selectedExtras, onToggleExtra }: ExtrasSectionProps) =>
|
||||||
|
|
||||||
{/* Products grid */}
|
{/* Products grid */}
|
||||||
<div className="grid sm:grid-cols-2 gap-6 max-w-4xl mx-auto">
|
<div className="grid sm:grid-cols-2 gap-6 max-w-4xl mx-auto">
|
||||||
{extraProducts.map((product) => (
|
{extras.map((extra) => (
|
||||||
<ExtraProductCard
|
<ExtraProductCard
|
||||||
key={product.id}
|
key={extra.id}
|
||||||
{...product}
|
id={extra.id}
|
||||||
isSelected={selectedExtras.includes(product.id)}
|
title={extra.name}
|
||||||
|
description={extra.description}
|
||||||
|
price={extra.price}
|
||||||
|
icon={iconMap[extra.icon] ?? iconMap["gift"]}
|
||||||
|
iconBgClass="bg-secondary/20"
|
||||||
|
isSelected={selectedExtras.includes(extra.id)}
|
||||||
onToggle={onToggleExtra}
|
onToggle={onToggleExtra}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
@ -80,4 +60,6 @@ const ExtrasSection = ({ selectedExtras, onToggleExtra }: ExtrasSectionProps) =>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default ExtrasSection;
|
export default ExtrasSection;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useState, useRef, useCallback } from "react";
|
import { useState, useRef, useCallback, useEffect } from "react";
|
||||||
import Header from "@/components/Header";
|
import Header from "@/components/Header";
|
||||||
import HeroSection from "@/components/HeroSection";
|
import HeroSection from "@/components/HeroSection";
|
||||||
import GallerySection from "@/components/GallerySection";
|
import GallerySection from "@/components/GallerySection";
|
||||||
|
|
@ -9,6 +9,7 @@ import CartSummary from "@/components/CartSummary";
|
||||||
import TrustSection from "@/components/TrustSection";
|
import TrustSection from "@/components/TrustSection";
|
||||||
import Footer from "@/components/Footer";
|
import Footer from "@/components/Footer";
|
||||||
import { toast } from "@/hooks/use-toast";
|
import { toast } from "@/hooks/use-toast";
|
||||||
|
import type { Extra } from '@/components/ExtrasSection'
|
||||||
|
|
||||||
interface CartItem {
|
interface CartItem {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -21,18 +22,12 @@ const planDetails: Record<string, { name: string; price: number }> = {
|
||||||
"12months": { name: "Plano Anual (12 meses)", price: 119.9 },
|
"12months": { name: "Plano Anual (12 meses)", price: 119.9 },
|
||||||
};
|
};
|
||||||
|
|
||||||
const extraDetails: Record<string, { name: string; price: number }> = {
|
|
||||||
device: { name: "Novo Dispositivo ZOE", price: 29.9 },
|
|
||||||
license: { name: "Licença Adicional", price: 19.9 },
|
|
||||||
book1: { name: "Livro de Atividades Vol. 1", price: 34.9 },
|
|
||||||
book2: { name: "Livro de Atividades Vol. 2", price: 34.9 },
|
|
||||||
};
|
|
||||||
|
|
||||||
const Index = () => {
|
const Index = () => {
|
||||||
const [selectedPlanId, setSelectedPlanId] = useState<string | undefined>();
|
const [selectedPlanId, setSelectedPlanId] = useState<string | undefined>();
|
||||||
const [selectedExtras, setSelectedExtras] = useState<string[]>([]);
|
const [selectedExtras, setSelectedExtras] = useState<string[]>([]);
|
||||||
const [isCartOpen, setIsCartOpen] = useState(false);
|
const [isCartOpen, setIsCartOpen] = useState(false);
|
||||||
const plansSectionRef = useRef<HTMLElement>(null);
|
const plansSectionRef = useRef<HTMLElement>(null);
|
||||||
|
const [extras, setExtras] = useState<Extra[]>([]);
|
||||||
|
|
||||||
const scrollToPlans = useCallback(() => {
|
const scrollToPlans = useCallback(() => {
|
||||||
plansSectionRef.current?.scrollIntoView({ behavior: "smooth" });
|
plansSectionRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||||
|
|
@ -66,10 +61,12 @@ const Index = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedExtras.forEach((extraId) => {
|
selectedExtras.forEach((extraId) => {
|
||||||
if (extraDetails[extraId]) {
|
const extra = extras.find((e) => e.id === extraId);
|
||||||
|
if (extra) {
|
||||||
items.push({
|
items.push({
|
||||||
id: extraId,
|
id: extra.id,
|
||||||
...extraDetails[extraId],
|
name: extra.name,
|
||||||
|
price: extra.price,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -95,6 +92,28 @@ const Index = () => {
|
||||||
setSelectedExtras([]);
|
setSelectedExtras([]);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function loadExtras() {
|
||||||
|
const api_endpoint = import.meta.env.VITE_API_URL;
|
||||||
|
const res = await fetch(`${api_endpoint}/courses/list`);
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (!data.ok) return;
|
||||||
|
|
||||||
|
const mapped = data.cursos.map((c: any) => ({
|
||||||
|
id: String(c.id),
|
||||||
|
name: c.nome,
|
||||||
|
description: c.descricao,
|
||||||
|
price: c.preco,
|
||||||
|
icon: c.icone,
|
||||||
|
}));
|
||||||
|
|
||||||
|
setExtras(mapped);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadExtras();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const cartItems = getCartItems();
|
const cartItems = getCartItems();
|
||||||
const cartItemCount = cartItems.length;
|
const cartItemCount = cartItems.length;
|
||||||
|
|
||||||
|
|
@ -117,6 +136,7 @@ const Index = () => {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ExtrasSection
|
<ExtrasSection
|
||||||
|
extras={extras}
|
||||||
selectedExtras={selectedExtras}
|
selectedExtras={selectedExtras}
|
||||||
onToggleExtra={handleToggleExtra}
|
onToggleExtra={handleToggleExtra}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { Smartphone, Users, BookOpen, Gift } from "lucide-react";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
|
||||||
|
export const iconMap: Record<string, React.ReactNode> = {
|
||||||
|
smartphone: <Smartphone className="w-7 h-7 text-zoe-blue" />,
|
||||||
|
users: <Users className="w-7 h-7 text-zoe-pink" />,
|
||||||
|
book: <BookOpen className="w-7 h-7 text-zoe-mint" />,
|
||||||
|
gift: <Gift className="w-7 h-7 text-zoe-orange" />,
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue