Auto commit: 2026-02-28T19:37:33.140Z

This commit is contained in:
Flatlogic Bot 2026-02-28 19:37:33 +00:00
parent 1ad6469a29
commit 428e4e9eed
6 changed files with 483 additions and 153 deletions

View File

@ -0,0 +1,40 @@
module.exports = {
async up(queryInterface) {
const createdAt = new Date();
const updatedAt = new Date();
const [roles] = await queryInterface.sequelize.query(
`SELECT id FROM roles WHERE name = 'Public' LIMIT 1;`
);
const publicRoleId = roles[0]?.id;
if (!publicRoleId) return;
const [permissions] = await queryInterface.sequelize.query(
`SELECT id FROM permissions WHERE name IN ('READ_PRODUCTS', 'READ_PRODUCT_CATEGORIES');`
);
if (permissions.length === 0) return;
const rolePermissions = permissions.map(p => ({
roles_permissionsId: publicRoleId,
permissionId: p.id,
createdAt,
updatedAt
}));
// Use a try-catch to ignore duplicate key errors if already exists
for (const rp of rolePermissions) {
try {
await queryInterface.bulkInsert('rolesPermissionsPermissions', [rp]);
} catch (e) {
console.log(`Permission ${rp.permissionId} already exists for role ${rp.roles_permissionsId}`);
}
}
},
async down(queryInterface) {
// Optional: remove the permissions if needed
}
};

View File

@ -1,4 +1,3 @@
const express = require('express');
const cors = require('cors');
const app = express();
@ -121,11 +120,11 @@ app.use('/api/permissions', passport.authenticate('jwt', {session: false}), perm
app.use('/api/customer_addresses', passport.authenticate('jwt', {session: false}), customer_addressesRoutes);
app.use('/api/product_categories', passport.authenticate('jwt', {session: false}), product_categoriesRoutes);
app.use('/api/product_categories', product_categoriesRoutes);
app.use('/api/product_tags', passport.authenticate('jwt', {session: false}), product_tagsRoutes);
app.use('/api/products', passport.authenticate('jwt', {session: false}), productsRoutes);
app.use('/api/products', productsRoutes);
app.use('/api/inventory_movements', passport.authenticate('jwt', {session: false}), inventory_movementsRoutes);
@ -191,4 +190,4 @@ db.sequelize.sync().then(function () {
});
});
module.exports = app;
module.exports = app;

View File

@ -0,0 +1,112 @@
import React from 'react';
import { mdiClose, mdiTrashCan, mdiMinus, mdiPlus } from '@mdi/js';
import { useAppDispatch, useAppSelector } from '../stores/hooks';
import { removeFromCart, updateQuantity, toggleCart } from '../stores/localCartSlice';
import BaseButton from './BaseButton';
import BaseIcon from './BaseIcon';
const CartDrawer = () => {
const dispatch = useAppDispatch();
const { items, isOpen } = useAppSelector((state) => state.localCart);
const totalPrice = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex justify-end">
{/* Backdrop */}
<div
className="fixed inset-0 bg-black bg-opacity-50 transition-opacity"
onClick={() => dispatch(toggleCart())}
></div>
{/* Drawer */}
<div className="relative w-full max-w-md bg-white h-full shadow-xl flex flex-col animate-fade-in-right">
<div className="p-4 border-b flex items-center justify-between bg-orange-600 text-white">
<h2 className="text-xl font-bold">Tu Carrito</h2>
<button onClick={() => dispatch(toggleCart())} className="p-1 hover:bg-orange-700 rounded">
<BaseIcon path={mdiClose} size={24} />
</button>
</div>
<div className="flex-grow overflow-y-auto p-4 space-y-4">
{items.length === 0 ? (
<div className="text-center py-10">
<p className="text-gray-500 mb-4">Tu carrito está vacío</p>
<BaseButton
label="Empezar a comprar"
color="info"
onClick={() => dispatch(toggleCart())}
/>
</div>
) : (
items.map((item) => (
<div key={item.id} className="flex items-center space-x-4 border-b pb-4">
{item.image && (
<img src={item.image} alt={item.name} className="w-16 h-16 object-cover rounded" />
)}
<div className="flex-grow">
<h3 className="font-semibold text-gray-800">{item.name}</h3>
<p className="text-orange-600 font-bold">${item.price.toFixed(2)}</p>
<div className="flex items-center space-x-2 mt-2">
<button
onClick={() => dispatch(updateQuantity({ id: item.id, quantity: item.quantity - 1 }))}
className="p-1 border rounded hover:bg-gray-100"
>
<BaseIcon path={mdiMinus} size={16} />
</button>
<span className="w-8 text-center">{item.quantity}</span>
<button
onClick={() => dispatch(updateQuantity({ id: item.id, quantity: item.quantity + 1 }))}
className="p-1 border rounded hover:bg-gray-100"
>
<BaseIcon path={mdiPlus} size={16} />
</button>
</div>
</div>
<button
onClick={() => dispatch(removeFromCart(item.id))}
className="text-red-500 hover:text-red-700"
>
<BaseIcon path={mdiTrashCan} size={20} />
</button>
</div>
))
)}
</div>
{items.length > 0 && (
<div className="p-4 border-t bg-gray-50">
<div className="flex justify-between items-center mb-4">
<span className="text-lg font-semibold text-gray-600">Total:</span>
<span className="text-2xl font-bold text-orange-600">${totalPrice.toFixed(2)}</span>
</div>
<BaseButton
label="Finalizar Compra"
color="success"
className="w-full py-3 text-lg font-bold"
onClick={() => {
alert('¡Próximamente! Estamos preparando el proceso de pago.');
}}
/>
<p className="text-xs text-center text-gray-400 mt-2">
Envío gratuito en pedidos superiores a $50
</p>
</div>
)}
</div>
<style jsx>{`
@keyframes fade-in-right {
from { opacity: 0; transform: translateX(100%); }
to { opacity: 1; transform: translateX(0); }
}
.animate-fade-in-right {
animation: fade-in-right 0.3s ease-out forwards;
}
`}</style>
</div>
);
};
export default CartDrawer;

View File

@ -1,166 +1,280 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect } from 'react';
import type { ReactElement } from 'react';
import Head from 'next/head';
import Link from 'next/link';
import BaseButton from '../components/BaseButton';
import CardBox from '../components/CardBox';
import SectionFullScreen from '../components/SectionFullScreen';
import LayoutGuest from '../layouts/Guest';
import BaseDivider from '../components/BaseDivider';
import BaseButtons from '../components/BaseButtons';
import { getPageTitle } from '../config';
import { useAppSelector } from '../stores/hooks';
import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
import { useAppSelector, useAppDispatch } from '../stores/hooks';
import { fetch as fetchProducts } from '../stores/products/productsSlice';
import { fetch as fetchCategories } from '../stores/product_categories/product_categoriesSlice';
import ImageField from '../components/ImageField';
import { mdiCartOutline, mdiArrowRight } from '@mdi/js';
import BaseIcon from '../components/BaseIcon';
import LoadingSpinner from '../components/LoadingSpinner';
import CartDrawer from '../components/CartDrawer';
import { addToCart, toggleCart } from '../stores/localCartSlice';
export default function Home() {
const dispatch = useAppDispatch();
const { products, loading: productsLoading } = useAppSelector((state) => state.products);
const { product_categories, loading: categoriesLoading } = useAppSelector((state) => state.product_categories);
const { currentUser } = useAppSelector((state) => state.auth);
const darkMode = useAppSelector((state) => state.style.darkMode);
const { items: cartItems } = useAppSelector((state) => state.localCart);
export default function Starter() {
const [illustrationImage, setIllustrationImage] = useState({
src: undefined,
photographer: undefined,
photographer_url: undefined,
})
const [illustrationVideo, setIllustrationVideo] = useState({video_files: []})
const [contentType, setContentType] = useState('image');
const [contentPosition, setContentPosition] = useState('background');
const textColor = useAppSelector((state) => state.style.linkColor);
const cartCount = cartItems.reduce((sum, item) => sum + item.quantity, 0);
const title = 'El Calentico Store'
// Fetch Pexels image/video
useEffect(() => {
async function fetchData() {
const image = await getPexelsImage();
const video = await getPexelsVideo();
setIllustrationImage(image);
setIllustrationVideo(video);
}
fetchData();
}, []);
dispatch(fetchProducts({ query: '?limit=8' }));
dispatch(fetchCategories({ query: '' }));
}, [dispatch]);
const imageBlock = (image) => (
<div
className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'
style={{
backgroundImage: `${
image
? `url(${image?.src?.original})`
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
}`,
backgroundSize: 'cover',
backgroundPosition: 'left center',
backgroundRepeat: 'no-repeat',
}}
>
<div className='flex justify-center w-full bg-blue-300/20'>
<a
className='text-[8px]'
href={image?.photographer_url}
target='_blank'
rel='noreferrer'
>
Photo by {image?.photographer} on Pexels
</a>
</div>
</div>
);
const videoBlock = (video) => {
if (video?.video_files?.length > 0) {
return (
<div className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'>
<video
className='absolute top-0 left-0 w-full h-full object-cover'
autoPlay
loop
muted
>
<source src={video?.video_files[0]?.link} type='video/mp4'/>
Your browser does not support the video tag.
</video>
<div className='flex justify-center w-full bg-blue-300/20 z-10'>
<a
className='text-[8px]'
href={video?.user?.url}
target='_blank'
rel='noreferrer'
>
Video by {video.user.name} on Pexels
</a>
</div>
</div>)
}
const handleAddToCart = (product: any) => {
dispatch(addToCart({
id: product.id,
name: product.name,
price: product.price,
quantity: 1,
image: product.images ? product.images[0]?.url : null
}));
dispatch(toggleCart());
};
return (
<div
style={
contentPosition === 'background'
? {
backgroundImage: `${
illustrationImage
? `url(${illustrationImage.src?.original})`
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
}`,
backgroundSize: 'cover',
backgroundPosition: 'left center',
backgroundRepeat: 'no-repeat',
}
: {}
}
>
<Head>
<title>{getPageTitle('Starter Page')}</title>
</Head>
const heroImage = "https://images.pexels.com/photos/958545/pexels-photo-958545.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1";
<SectionFullScreen bg='violet'>
<div
className={`flex ${
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'
} min-h-screen w-full`}
>
{contentType === 'image' && contentPosition !== 'background'
? imageBlock(illustrationImage)
: null}
{contentType === 'video' && contentPosition !== 'background'
? videoBlock(illustrationVideo)
: null}
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
<CardBoxComponentTitle title="Welcome to your El Calentico Store app!"/>
<div className="space-y-3">
<p className='text-center text-gray-500'>This is a React.js/Node.js app generated by the <a className={`${textColor}`} href="https://flatlogic.com/generator">Flatlogic Web App Generator</a></p>
<p className='text-center text-gray-500'>For guides and documentation please check
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p>
</div>
<BaseButtons>
<BaseButton
href='/login'
label='Login'
color='info'
className='w-full'
/>
return (
<div className={`min-h-screen ${darkMode ? 'bg-dark-900 text-white' : 'bg-gray-50 text-gray-900'}`}>
<Head>
<title>{getPageTitle('El Calentico - Tu Tienda Virtual')}</title>
</Head>
</BaseButtons>
</CardBox>
<CartDrawer />
{/* Navigation / Header */}
<nav className={`sticky top-0 z-40 w-full border-b ${darkMode ? 'bg-dark-800 border-dark-700' : 'bg-white border-gray-200'} px-4 py-3 shadow-sm`}>
<div className="container mx-auto flex items-center justify-between">
<Link href="/" className="flex items-center gap-2">
<span className="text-2xl font-bold text-orange-600">El Calentico</span>
</Link>
<div className="flex items-center gap-4">
<Link href="/search" className="hidden md:block text-sm font-medium hover:text-orange-600 transition-colors">
Buscar
</Link>
{/* Cart Toggle Button */}
<button
onClick={() => dispatch(toggleCart())}
className="relative p-2 text-gray-600 hover:text-orange-600 transition-colors"
>
<BaseIcon path={mdiCartOutline} size={28} />
{cartCount > 0 && (
<span className="absolute top-0 right-0 bg-orange-600 text-white text-[10px] font-bold w-5 h-5 flex items-center justify-center rounded-full border-2 border-white">
{cartCount}
</span>
)}
</button>
{currentUser ? (
<Link href="/dashboard" className="text-sm font-medium px-4 py-2 rounded-full bg-orange-600 text-white hover:bg-orange-700 transition-colors">
Dashboard
</Link>
) : (
<div className="flex items-center gap-2">
<Link href="/login" className="hidden sm:block text-sm font-medium hover:text-orange-600 transition-colors">
Iniciar Sesión
</Link>
<Link href="/register" className="text-sm font-medium px-4 py-2 rounded-full bg-orange-600 text-white hover:bg-orange-700 transition-colors">
Registrarse
</Link>
</div>
)}
</div>
</div>
</nav>
{/* Hero Section */}
<section className="relative h-[500px] w-full overflow-hidden flex items-center">
<div className="absolute inset-0 z-0">
<img src={heroImage} alt="Food hero" className="w-full h-full object-cover" />
<div className="absolute inset-0 bg-black/40"></div>
</div>
<div className="container mx-auto relative z-10 px-4">
<div className="max-w-2xl text-white">
<h1 className="text-5xl md:text-6xl font-extrabold mb-6 leading-tight">
Sabor y Frescura <br />
<span className="text-orange-400">Directo a tu Mesa</span>
</h1>
<p className="text-xl mb-8 text-gray-100">
Descubre la mejor selección de alimentos, bebidas y productos locales en El Calentico. Calidad garantizada en cada pedido.
</p>
<div className="flex gap-4">
<BaseButton
label="Comprar Ahora"
color="warning"
roundedFull
className="px-8 py-3 text-lg font-bold"
href="#products"
/>
<BaseButton
label="Ver Categorías"
color="white"
outline
roundedFull
className="px-8 py-3 text-lg font-bold"
href="#categories"
/>
</div>
</div>
</div>
</section>
{/* Categories Section */}
<section id="categories" className="py-16 bg-white dark:bg-dark-800">
<div className="container mx-auto px-4">
<div className="flex justify-between items-end mb-10">
<div>
<h2 className="text-3xl font-bold mb-2">Categorías</h2>
<p className="text-gray-500">Explora nuestra variedad de productos</p>
</div>
</div>
{categoriesLoading ? (
<div className="flex justify-center py-10"><LoadingSpinner /></div>
) : (
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
{product_categories?.map((cat: any) => (
<Link
key={cat.id}
href={`/search?category=${cat.id}`}
className={`group flex flex-col items-center p-6 rounded-2xl transition-all hover:shadow-lg ${darkMode ? 'bg-dark-700 border-dark-600 hover:bg-dark-600' : 'bg-gray-50 border-gray-100 hover:bg-orange-50 hover:border-orange-200'} border`}
>
<div className="w-16 h-16 bg-orange-100 rounded-full flex items-center justify-center mb-4 group-hover:scale-110 transition-transform">
<span className="text-2xl font-bold text-orange-600">{cat.name.charAt(0)}</span>
</div>
<span className="font-semibold text-center group-hover:text-orange-600">{cat.name}</span>
</Link>
))}
</div>
)}
</div>
</section>
{/* Featured Products */}
<section id="products" className="py-16">
<div className="container mx-auto px-4">
<div className="flex justify-between items-end mb-10">
<div>
<h2 className="text-3xl font-bold mb-2">Productos Destacados</h2>
<p className="text-gray-500">Lo más buscado de esta semana</p>
</div>
<Link href="/search" className="text-orange-600 font-bold flex items-center gap-1 hover:underline">
Ver todos <BaseIcon path={mdiArrowRight} size={20} />
</Link>
</div>
{productsLoading ? (
<div className="flex justify-center py-10"><LoadingSpinner /></div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
{products?.map((product: any) => (
<div key={product.id} className={`group flex flex-col overflow-hidden rounded-2xl shadow-sm border transition-all hover:shadow-xl ${darkMode ? 'bg-dark-800 border-dark-700' : 'bg-white border-gray-200'}`}>
<Link href={`/products/products-view/?id=${product.id}`} className="relative h-64 overflow-hidden">
<ImageField
image={product.images}
className="h-full w-full object-cover transition-transform duration-500 group-hover:scale-110"
/>
{product.price < product.compare_at_price && (
<div className="absolute top-4 left-4 bg-red-600 text-white text-xs font-bold px-3 py-1 rounded-full">
OFERTA
</div>
)}
</Link>
<div className="p-6 flex flex-col flex-grow">
<div className="text-xs text-gray-500 mb-2 uppercase tracking-wider font-semibold">
{product.category?.name || 'Producto'}
</div>
<Link href={`/products/products-view/?id=${product.id}`}>
<h3 className="text-lg font-bold mb-2 group-hover:text-orange-600 transition-colors line-clamp-2">{product.name}</h3>
</Link>
<div className="mt-auto flex items-center justify-between">
<div className="flex flex-col">
{product.compare_at_price > 0 && (
<span className="text-sm text-gray-400 line-through">${product.compare_at_price}</span>
)}
<span className="text-2xl font-extrabold text-orange-600">${product.price}</span>
</div>
<button
onClick={() => handleAddToCart(product)}
className="p-3 bg-gray-100 hover:bg-orange-600 hover:text-white rounded-full transition-all duration-300 group/cart"
title="Añadir al carrito"
>
<BaseIcon path={mdiCartOutline} size={24} />
</button>
</div>
</div>
</div>
))}
</div>
)}
</div>
</section>
{/* CTA Section */}
<section className="py-20 bg-orange-600 text-white">
<div className="container mx-auto px-4 text-center">
<h2 className="text-4xl font-bold mb-6">¿Listo para hacer tu primer pedido?</h2>
<p className="text-xl mb-10 max-w-2xl mx-auto opacity-90">
Únete a miles de clientes satisfechos que ya disfrutan de la frescura de El Calentico en sus hogares.
</p>
<BaseButton
label="Crear una Cuenta Gratis"
color="white"
roundedFull
className="px-10 py-4 text-lg font-bold text-orange-600"
href="/register"
/>
</div>
</section>
{/* Footer */}
<footer className={`py-12 ${darkMode ? 'bg-dark-800 text-gray-400' : 'bg-gray-100 text-gray-600'}`}>
<div className="container mx-auto px-4">
<div className="grid grid-cols-1 md:grid-cols-4 gap-12 mb-12">
<div className="col-span-1 md:col-span-2">
<h3 className="text-2xl font-bold text-orange-600 mb-6">El Calentico</h3>
<p className="mb-6 max-w-md">
Tu tienda virtual de confianza para alimentos, bebidas y productos del día a día. Llevamos la mejor calidad directamente a tu puerta.
</p>
</div>
<div>
<h4 className="text-lg font-bold mb-6 text-gray-900 dark:text-white">Enlaces Rápidos</h4>
<ul className="space-y-4">
<li><Link href="/search" className="hover:text-orange-600 transition-colors">Productos</Link></li>
<li><Link href="#categories" className="hover:text-orange-600 transition-colors">Categorías</Link></li>
<li><Link href="/login" className="hover:text-orange-600 transition-colors">Mi Cuenta</Link></li>
</ul>
</div>
<div>
<h4 className="text-lg font-bold mb-6 text-gray-900 dark:text-white">Legal</h4>
<ul className="space-y-4">
<li><Link href="/privacy-policy" className="hover:text-orange-600 transition-colors">Privacidad</Link></li>
<li><Link href="/terms-of-use" className="hover:text-orange-600 transition-colors">Términos</Link></li>
</ul>
</div>
</div>
<div className="border-t border-gray-200 dark:border-dark-700 pt-8 flex flex-col md:flex-row justify-between items-center gap-4">
<p className="text-sm">© 2026 El Calentico. Todos los derechos reservados.</p>
<div className="flex gap-6">
{/* Social icons could go here */}
</div>
</div>
</div>
</footer>
</div>
</div>
</SectionFullScreen>
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. All rights reserved</p>
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
Privacy Policy
</Link>
</div>
</div>
);
);
}
Starter.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
};
Home.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
};

View File

@ -0,0 +1,63 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
image?: string;
}
interface LocalCartState {
items: CartItem[];
isOpen: boolean;
}
const initialState: LocalCartState = {
items: typeof window !== 'undefined' ? JSON.parse(localStorage.getItem('cart') || '[]') : [],
isOpen: false,
};
export const localCartSlice = createSlice({
name: 'localCart',
initialState,
reducers: {
addToCart: (state, action: PayloadAction<CartItem>) => {
const existingItem = state.items.find((item) => item.id === action.payload.id);
if (existingItem) {
existingItem.quantity += action.payload.quantity;
} else {
state.items.push(action.payload);
}
localStorage.setItem('cart', JSON.stringify(state.items));
},
removeFromCart: (state, action: PayloadAction<string>) => {
state.items = state.items.filter((item) => item.id !== action.payload);
localStorage.setItem('cart', JSON.stringify(state.items));
},
updateQuantity: (state, action: PayloadAction<{ id: string; quantity: number }>) => {
const item = state.items.find((item) => item.id === action.id);
if (item) {
item.quantity = Math.max(0, action.payload.quantity);
if (item.quantity === 0) {
state.items = state.items.filter((i) => i.id !== action.payload.id);
}
}
localStorage.setItem('cart', JSON.stringify(state.items));
},
clearCart: (state) => {
state.items = [];
localStorage.removeItem('cart');
},
toggleCart: (state) => {
state.isOpen = !state.isOpen;
},
setCartOpen: (state, action: PayloadAction<boolean>) => {
state.isOpen = action.payload;
},
},
});
export const { addToCart, removeFromCart, updateQuantity, clearCart, toggleCart, setCartOpen } = localCartSlice.actions;
export default localCartSlice.reducer;

View File

@ -3,6 +3,7 @@ import styleReducer from './styleSlice';
import mainReducer from './mainSlice';
import authSlice from './authSlice';
import openAiSlice from './openAiSlice';
import localCartReducer from './localCartSlice';
import usersSlice from "./users/usersSlice";
import rolesSlice from "./roles/rolesSlice";
@ -28,6 +29,7 @@ export const store = configureStore({
main: mainReducer,
auth: authSlice,
openAi: openAiSlice,
localCart: localCartReducer,
users: usersSlice,
roles: rolesSlice,
@ -52,4 +54,4 @@ product_reviews: product_reviewsSlice,
// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState>
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = typeof store.dispatch
export type AppDispatch = typeof store.dispatch