diff --git a/backend/src/db/migrations/1772306633138.js b/backend/src/db/migrations/1772306633138.js
new file mode 100644
index 0000000..aa29599
--- /dev/null
+++ b/backend/src/db/migrations/1772306633138.js
@@ -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
+ }
+};
diff --git a/backend/src/index.js b/backend/src/index.js
index 16088f2..6166e26 100644
--- a/backend/src/index.js
+++ b/backend/src/index.js
@@ -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;
\ No newline at end of file
diff --git a/frontend/src/components/CartDrawer.tsx b/frontend/src/components/CartDrawer.tsx
new file mode 100644
index 0000000..57b1a43
--- /dev/null
+++ b/frontend/src/components/CartDrawer.tsx
@@ -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 (
+
+ {/* Backdrop */}
+
dispatch(toggleCart())}
+ >
+
+ {/* Drawer */}
+
+
+
Tu Carrito
+
+
+
+
+ {items.length === 0 ? (
+
+
Tu carrito está vacío
+
dispatch(toggleCart())}
+ />
+
+ ) : (
+ items.map((item) => (
+
+ {item.image && (
+

+ )}
+
+
{item.name}
+
${item.price.toFixed(2)}
+
+
+ {item.quantity}
+
+
+
+
+
+ ))
+ )}
+
+
+ {items.length > 0 && (
+
+
+ Total:
+ ${totalPrice.toFixed(2)}
+
+
{
+ alert('¡Próximamente! Estamos preparando el proceso de pago.');
+ }}
+ />
+
+ Envío gratuito en pedidos superiores a $50
+
+
+ )}
+
+
+
+ );
+};
+
+export default CartDrawer;
diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx
index df48bca..b6b4fa3 100644
--- a/frontend/src/pages/index.tsx
+++ b/frontend/src/pages/index.tsx
@@ -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) => (
-
- );
-
- const videoBlock = (video) => {
- if (video?.video_files?.length > 0) {
- return (
-
-
-
-
)
- }
+ 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 (
-
-
-
{getPageTitle('Starter Page')}
-
+ const heroImage = "https://images.pexels.com/photos/958545/pexels-photo-958545.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1";
-
-
- {contentType === 'image' && contentPosition !== 'background'
- ? imageBlock(illustrationImage)
- : null}
- {contentType === 'video' && contentPosition !== 'background'
- ? videoBlock(illustrationVideo)
- : null}
-
-
-
-
-
-
-
-
+ return (
+
+
+
{getPageTitle('El Calentico - Tu Tienda Virtual')}
+
-
-
+
+
+ {/* Navigation / Header */}
+
+
+ {/* Hero Section */}
+
+
+

+
+
+
+
+
+ Sabor y Frescura
+ Directo a tu Mesa
+
+
+ Descubre la mejor selección de alimentos, bebidas y productos locales en El Calentico. Calidad garantizada en cada pedido.
+
+
+
+
+
+
+
+
+
+ {/* Categories Section */}
+
+
+
+
+
Categorías
+
Explora nuestra variedad de productos
+
+
+
+ {categoriesLoading ? (
+
+ ) : (
+
+ {product_categories?.map((cat: any) => (
+
+
+ {cat.name.charAt(0)}
+
+
{cat.name}
+
+ ))}
+
+ )}
+
+
+
+ {/* Featured Products */}
+
+
+
+
+
Productos Destacados
+
Lo más buscado de esta semana
+
+
+ Ver todos
+
+
+
+ {productsLoading ? (
+
+ ) : (
+
+ {products?.map((product: any) => (
+
+
+
+ {product.price < product.compare_at_price && (
+
+ OFERTA
+
+ )}
+
+
+
+ {product.category?.name || 'Producto'}
+
+
+
{product.name}
+
+
+
+ {product.compare_at_price > 0 && (
+ ${product.compare_at_price}
+ )}
+ ${product.price}
+
+
+
+
+
+ ))}
+
+ )}
+
+
+
+ {/* CTA Section */}
+
+
+
¿Listo para hacer tu primer pedido?
+
+ Únete a miles de clientes satisfechos que ya disfrutan de la frescura de El Calentico en sus hogares.
+
+
+
+
+
+ {/* Footer */}
+
-
-
-
-
© 2026 {title}. All rights reserved
-
- Privacy Policy
-
-
-
-
- );
+ );
}
-Starter.getLayout = function getLayout(page: ReactElement) {
- return {page};
-};
-
+Home.getLayout = function getLayout(page: ReactElement) {
+ return {page};
+};
\ No newline at end of file
diff --git a/frontend/src/stores/localCartSlice.ts b/frontend/src/stores/localCartSlice.ts
new file mode 100644
index 0000000..0ac7d59
--- /dev/null
+++ b/frontend/src/stores/localCartSlice.ts
@@ -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) => {
+ 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) => {
+ 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) => {
+ state.isOpen = action.payload;
+ },
+ },
+});
+
+export const { addToCart, removeFromCart, updateQuantity, clearCart, toggleCart, setCartOpen } = localCartSlice.actions;
+
+export default localCartSlice.reducer;
diff --git a/frontend/src/stores/store.ts b/frontend/src/stores/store.ts
index 70d1525..349d806 100644
--- a/frontend/src/stores/store.ts
+++ b/frontend/src/stores/store.ts
@@ -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
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
-export type AppDispatch = typeof store.dispatch
+export type AppDispatch = typeof store.dispatch
\ No newline at end of file