
Como desarrollador junior en México y Latinoamérica, tienes más oportunidades que nunca para generar ingresos mientras construyes tu experiencia. El mercado tech en la región está en constante crecimiento, y hay múltiples caminos para monetizar tus habilidades en JavaScript y React. Aquí te presento 5 estrategias alcanzables que te permitirán empezar a ganar dinero (y si no inmediatamente, ganar experiencia invaluable).
El freelancing es ideal para desarrolladores junior porque puedes empezar con proyectos pequeños y crecer gradualmente. Plataformas como Workana, Freelancer, y Upwork tienen alta demanda de desarrolladores que hablen español.
Aquí tienes un template básico que puedes usar para proyectos de landing pages:
1// components/LandingPage.tsx
2import React from 'react';
3import { useState } from 'react';
4
5interface ContactFormData {
6 name: string;
7 email: string;
8 message: string;
9}
10
11const LandingPage: React.FC = () => {
12 const [formData, setFormData] = useState<ContactFormData>({
13 name: '',
14 email: '',
15 message: ''
16 });
17
18 const handleSubmit = async (e: React.FormEvent) => {
19 e.preventDefault();
20
21 try {
22 const response = await fetch('/api/contact', {
23 method: 'POST',
24 headers: {
25 'Content-Type': 'application/json',
26 },
27 body: JSON.stringify(formData),
28 });
29
30 if (response.ok) {
31 alert('¡Mensaje enviado exitosamente!');
32 setFormData({ name: '', email: '', message: '' });
33 }
34 } catch (error) {
35 console.error('Error:', error);
36 }
37 };
38
39 return (
40 <div className="min-h-screen bg-gradient-to-br from-blue-600 to-purple-700">
41 <header className="container mx-auto px-4 py-6">
42 <nav className="flex justify-between items-center text-white">
43 <h1 className="text-2xl font-bold">MiEmpresa</h1>
44 <div className="space-x-6">
45 <a href="#servicios" className="hover:underline">Servicios</a>
46 <a href="#contacto" className="hover:underline">Contacto</a>
47 </div>
48 </nav>
49 </header>
50
51 <main className="container mx-auto px-4 py-12">
52 <section className="text-center text-white mb-16">
53 <h2 className="text-5xl font-bold mb-6">
54 Transformamos tus ideas en realidad digital
55 </h2>
56 <p className="text-xl mb-8 max-w-2xl mx-auto">
57 Desarrollamos sitios web modernos y aplicaciones que impulsan tu negocio
58 </p>
59 <button className="bg-white text-blue-600 px-8 py-3 rounded-lg font-semibold hover:bg-gray-100 transition">
60 Comenzar Proyecto
61 </button>
62 </section>
63
64 <section id="contacto" className="bg-white rounded-lg shadow-xl p-8 max-w-md mx-auto">
65 <h3 className="text-2xl font-bold mb-6 text-center">Contáctanos</h3>
66 <form onSubmit={handleSubmit} className="space-y-4">
67 <input
68 type="text"
69 placeholder="Tu nombre"
70 value={formData.name}
71 onChange={(e) => setFormData({...formData, name: e.target.value})}
72 className="w-full p-3 border rounded-lg focus:ring-2 focus:ring-blue-500"
73 required
74 />
75 <input
76 type="email"
77 placeholder="Tu email"
78 value={formData.email}
79 onChange={(e) => setFormData({...formData, email: e.target.value})}
80 className="w-full p-3 border rounded-lg focus:ring-2 focus:ring-blue-500"
81 required
82 />
83 <textarea
84 placeholder="Tu mensaje"
85 value={formData.message}
86 onChange={(e) => setFormData({...formData, message: e.target.value})}
87 className="w-full p-3 border rounded-lg h-32 focus:ring-2 focus:ring-blue-500"
88 required
89 />
90 <button
91 type="submit"
92 className="w-full bg-blue-600 text-white py-3 rounded-lg hover:bg-blue-700 transition"
93 >
94 Enviar Mensaje
95 </button>
96 </form>
97 </section>
98 </main>
99 </div>
100 );
101};
102
103export default LandingPage;1// pages/api/contact.ts
2import type { NextApiRequest, NextApiResponse } from 'next';
3
4interface ContactData {
5 name: string;
6 email: string;
7 message: string;
8}
9
10export default async function handler(
11 req: NextApiRequest,
12 res: NextApiResponse
13) {
14 if (req.method !== 'POST') {
15 return res.status(405).json({ message: 'Método no permitido' });
16 }
17
18 const { name, email, message }: ContactData = req.body;
19
20 // Validación básica
21 if (!name || !email || !message) {
22 return res.status(400).json({ message: 'Todos los campos son requeridos' });
23 }
24
25 try {
26 // Aquí integrarías con un servicio de email como SendGrid, Resend, etc.
27 // Por ahora, simulamos el envío
28 console.log('Nuevo contacto:', { name, email, message });
29
30 // Podrías guardar en base de datos aquí
31
32 res.status(200).json({ message: 'Mensaje enviado exitosamente' });
33 } catch (error) {
34 console.error('Error al procesar contacto:', error);
35 res.status(500).json({ message: 'Error interno del servidor' });
36 }
37}Precio sugerido: $100-300 USD por una landing page completa con formulario de contacto.
Los portfolios interactivos son ideales porque puedes crear el tuyo primero, perfeccionarlo, y luego ofrecerlo como servicio. Muchos profesionales (diseñadores, fotógrafos, arquitectos, otros developers) necesitan destacar online.
1// types/portfolio.ts
2export interface Project {
3 id: string;
4 title: string;
5 description: string;
6 technologies: string[];
7 imageUrl: string;
8 liveUrl?: string;
9 githubUrl?: string;
10}
11
12export interface PersonalInfo {
13 name: string;
14 title: string;
15 email: string;
16 phone: string;
17 location: string;
18 bio: string;
19 profileImage: string;
20}
21
22// components/Portfolio.tsx
23import { useState } from 'react';
24import { ChevronDown, Mail, Phone, MapPin, Github, ExternalLink } from 'lucide-react';
25
26const InteractivePortfolio: React.FC = () => {
27 const [activeSection, setActiveSection] = useState('about');
28 const [selectedProject, setSelectedProject] = useState<Project | null>(null);
29
30 const personalInfo: PersonalInfo = {
31 name: "María González",
32 title: "Desarrolladora Frontend",
33 email: "maria@ejemplo.com",
34 phone: "+52 55 1234 5678",
35 location: "Ciudad de México, México",
36 bio: "Desarrolladora apasionada por crear experiencias web increíbles. Especializada en React y diseño responsivo.",
37 profileImage: "/profile-placeholder.jpg"
38 };
39
40 const projects: Project[] = [
41 {
42 id: "1",
43 title: "App de Recetas Mexicanas",
44 description: "Aplicación web para compartir y descubrir recetas tradicionales mexicanas",
45 technologies: ["React", "TypeScript", "Tailwind CSS"],
46 imageUrl: "/project1-placeholder.jpg",
47 liveUrl: "https://recetas-demo.vercel.app",
48 githubUrl: "https://github.com/maria/recetas-app"
49 },
50 {
51 id: "2",
52 title: "Dashboard de Finanzas Personales",
53 description: "Herramienta para tracking de gastos con gráficos interactivos",
54 technologies: ["Next.js", "Chart.js", "Firebase"],
55 imageUrl: "/project2-placeholder.jpg",
56 liveUrl: "https://finanzas-demo.vercel.app",
57 githubUrl: "https://github.com/maria/finanzas-dashboard"
58 }
59 ];
60
61 const skills = ["JavaScript", "TypeScript", "React", "Next.js", "Tailwind CSS", "Git", "Figma"];
62
63 return (
64 <div className="min-h-screen bg-gradient-to-br from-purple-50 to-blue-50">
65 {/* Header con navegación fija */}
66 <nav className="fixed top-0 w-full bg-white/90 backdrop-blur-md shadow-sm z-50">
67 <div className="container mx-auto px-4 py-4">
68 <div className="flex justify-between items-center">
69 <h1 className="font-bold text-xl text-gray-800">{personalInfo.name}</h1>
70 <div className="hidden md:flex space-x-6">
71 {['about', 'projects', 'skills', 'contact'].map((section) => (
72 <button
73 key={section}
74 onClick={() => setActiveSection(section)}
75 className={`capitalize font-medium transition ${
76 activeSection === section
77 ? 'text-purple-600 border-b-2 border-purple-600'
78 : 'text-gray-600 hover:text-purple-600'
79 }`}
80 >
81 {section === 'about' ? 'Sobre mí' :
82 section === 'projects' ? 'Proyectos' :
83 section === 'skills' ? 'Habilidades' : 'Contacto'}
84 </button>
85 ))}
86 </div>
87 </div>
88 </div>
89 </nav>
90
91 <div className="pt-20">
92 {/* Sección Hero */}
93 {activeSection === 'about' && (
94 <section className="container mx-auto px-4 py-16">
95 <div className="max-w-4xl mx-auto text-center">
96 <div className="mb-8">
97 <img
98 src={personalInfo.profileImage}
99 alt={personalInfo.name}
100 className="w-32 h-32 rounded-full mx-auto mb-6 shadow-lg"
101 />
102 <h2 className="text-4xl font-bold text-gray-800 mb-2">
103 {personalInfo.name}
104 </h2>
105 <p className="text-xl text-purple-600 mb-4">{personalInfo.title}</p>
106 <div className="flex justify-center items-center space-x-4 text-gray-600 mb-6">
107 <div className="flex items-center">
108 <MapPin className="w-4 h-4 mr-1" />
109 {personalInfo.location}
110 </div>
111 </div>
112 </div>
113
114 <div className="bg-white rounded-lg shadow-lg p-8 max-w-2xl mx-auto">
115 <p className="text-lg text-gray-700 leading-relaxed">
116 {personalInfo.bio}
117 </p>
118 <div className="mt-6 flex justify-center space-x-4">
119 <button
120 onClick={() => setActiveSection('contact')}
121 className="bg-purple-600 text-white px-6 py-3 rounded-lg hover:bg-purple-700 transition"
122 >
123 Contáctame
124 </button>
125 <button
126 onClick={() => setActiveSection('projects')}
127 className="border border-purple-600 text-purple-600 px-6 py-3 rounded-lg hover:bg-purple-50 transition"
128 >
129 Ver Proyectos
130 </button>
131 </div>
132 </div>
133 </div>
134 </section>
135 )}
136
137 {/* Sección Proyectos */}
138 {activeSection === 'projects' && (
139 <section className="container mx-auto px-4 py-16">
140 <h2 className="text-3xl font-bold text-center mb-12">Mis Proyectos</h2>
141 <div className="grid grid-cols-1 md:grid-cols-2 gap-8 max-w-4xl mx-auto">
142 {projects.map(project => (
143 <div
144 key={project.id}
145 className="bg-white rounded-lg shadow-lg overflow-hidden hover:shadow-xl transition cursor-pointer"
146 onClick={() => setSelectedProject(project)}
147 >
148 <img
149 src={project.imageUrl}
150 alt={project.title}
151 className="w-full h-48 object-cover"
152 />
153 <div className="p-6">
154 <h3 className="font-bold text-xl mb-2">{project.title}</h3>
155 <p className="text-gray-600 mb-4">{project.description}</p>
156 <div className="flex flex-wrap gap-2">
157 {project.technologies.map(tech => (
158 <span
159 key={tech}
160 className="bg-purple-100 text-purple-700 px-3 py-1 rounded-full text-sm"
161 >
162 {tech}
163 </span>
164 ))}
165 </div>
166 </div>
167 </div>
168 ))}
169 </div>
170 </section>
171 )}
172
173 {/* Sección Skills */}
174 {activeSection === 'skills' && (
175 <section className="container mx-auto px-4 py-16">
176 <h2 className="text-3xl font-bold text-center mb-12">Habilidades</h2>
177 <div className="max-w-2xl mx-auto">
178 <div className="grid grid-cols-2 md:grid-cols-3 gap-4">
179 {skills.map((skill, index) => (
180 <div
181 key={skill}
182 className="bg-white rounded-lg p-4 text-center shadow-md hover:shadow-lg transition"
183 style={{ animationDelay: `${index * 0.1}s` }}
184 >
185 <span className="font-semibold text-gray-800">{skill}</span>
186 </div>
187 ))}
188 </div>
189 </div>
190 </section>
191 )}
192
193 {/* Sección Contacto */}
194 {activeSection === 'contact' && (
195 <section className="container mx-auto px-4 py-16">
196 <h2 className="text-3xl font-bold text-center mb-12">Contacto</h2>
197 <div className="max-w-md mx-auto bg-white rounded-lg shadow-lg p-8">
198 <div className="space-y-4">
199 <div className="flex items-center">
200 <Mail className="w-5 h-5 text-purple-600 mr-3" />
201 <a href={`mailto:${personalInfo.email}`} className="text-gray-700 hover:text-purple-600">
202 {personalInfo.email}
203 </a>
204 </div>
205 <div className="flex items-center">
206 <Phone className="w-5 h-5 text-purple-600 mr-3" />
207 <a href={`tel:${personalInfo.phone}`} className="text-gray-700 hover:text-purple-600">
208 {personalInfo.phone}
209 </a>
210 </div>
211 <div className="flex items-center">
212 <MapPin className="w-5 h-5 text-purple-600 mr-3" />
213 <span className="text-gray-700">{personalInfo.location}</span>
214 </div>
215 </div>
216
217 <div className="mt-8 pt-6 border-t">
218 <p className="text-center text-gray-600 mb-4">
219 ¿Tienes un proyecto en mente?
220 </p>
221 <a
222 href={`mailto:${personalInfo.email}?subject=Proyecto%20Web`}
223 className="block w-full bg-purple-600 text-white text-center py-3 rounded-lg hover:bg-purple-700 transition"
224 >
225 Enviar mensaje
226 </a>
227 </div>
228 </div>
229 </section>
230 )}
231 </div>
232
233 {/* Modal de proyecto */}
234 {selectedProject && (
235 <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
236 <div className="bg-white rounded-lg p-6 max-w-lg w-full">
237 <h3 className="text-2xl font-bold mb-4">{selectedProject.title}</h3>
238 <img
239 src={selectedProject.imageUrl}
240 alt={selectedProject.title}
241 className="w-full h-48 object-cover rounded-lg mb-4"
242 />
243 <p className="text-gray-700 mb-4">{selectedProject.description}</p>
244
245 <div className="flex flex-wrap gap-2 mb-6">
246 {selectedProject.technologies.map(tech => (
247 <span
248 key={tech}
249 className="bg-gray-100 text-gray-700 px-3 py-1 rounded-full text-sm"
250 >
251 {tech}
252 </span>
253 ))}
254 </div>
255
256 <div className="flex gap-4">
257 {selectedProject.liveUrl && (
258 <a
259 href={selectedProject.liveUrl}
260 target="_blank"
261 rel="noopener noreferrer"
262 className="flex items-center bg-purple-600 text-white px-4 py-2 rounded hover:bg-purple-700 transition"
263 >
264 <ExternalLink className="w-4 h-4 mr-2" />
265 Ver Demo
266 </a>
267 )}
268 {selectedProject.githubUrl && (
269 <a
270 href={selectedProject.githubUrl}
271 target="_blank"
272 rel="noopener noreferrer"
273 className="flex items-center border border-gray-300 px-4 py-2 rounded hover:bg-gray-50 transition"
274 >
275 <Github className="w-4 h-4 mr-2" />
276 Ver Código
277 </a>
278 )}
279 <button
280 onClick={() => setSelectedProject(null)}
281 className="flex-1 bg-gray-200 px-4 py-2 rounded hover:bg-gray-300 transition"
282 >
283 Cerrar
284 </button>
285 </div>
286 </div>
287 </div>
288 )}
289 </div>
290 );
291};
292
293export default InteractivePortfolio;Precio sugerido: $100-300 USD por una landing page completa con formulario de contacto.
Muchos emprendedores y pequeños negocios buscan plantillas personalizadas para WordPress, Shopify, o sitios estáticos. Es un mercado menos saturado que los themes genéricos y puedes especializarte en nichos específicos.
1// types/restaurant.ts
2export interface MenuItem {
3 id: string;
4 name: string;
5 description: string;
6 price: number;
7 category: 'entradas' | 'platos-principales' | 'postres' | 'bebidas';
8 imageUrl: string;
9 isSpicy?: boolean;
10 isVegetarian?: boolean;
11}
12
13export interface RestaurantInfo {
14 name: string;
15 tagline: string;
16 address: string;
17 phone: string;
18 hours: string;
19 socialMedia: {
20 instagram?: string;
21 facebook?: string;
22 whatsapp?: string;
23 };
24}
25
26// components/RestaurantTheme.tsx
27import { useState } from 'react';
28import { Clock, MapPin, Phone, Instagram, Facebook, MessageCircle } from 'lucide-react';
29
30const RestaurantTheme: React.FC = () => {
31 const [activeCategory, setActiveCategory] = useState<string>('platos-principales');
32 const [isMenuOpen, setIsMenuOpen] = useState(false);
33
34 const restaurantInfo: RestaurantInfo = {
35 name: "La Cocina de Abuela",
36 tagline: "Sabores tradicionales mexicanos desde 1985",
37 address: "Av. Reforma 123, Centro Histórico, CDMX",
38 phone: "+52 55 1234 5678",
39 hours: "Lun-Dom: 8:00 AM - 10:00 PM",
40 socialMedia: {
41 instagram: "https://instagram.com/lacocinadeabuela",
42 facebook: "https://facebook.com/lacocinadeabuela",
43 whatsapp: "https://wa.me/5215512345678"
44 }
45 };
46
47 const menuItems: MenuItem[] = [
48 {
49 id: "1",
50 name: "Mole Poblano",
51 description: "Pollo en salsa de mole tradicional con ajonjolí",
52 price: 185,
53 category: "platos-principales",
54 imageUrl: "/mole-placeholder.jpg",
55 isSpicy: true
56 },
57 {
58 id: "2",
59 name: "Tacos de Carnitas",
60 description: "3 tacos de carnitas michoacanas con cebolla y cilantro",
61 price: 85,
62 category: "platos-principales",
63 imageUrl: "/tacos-placeholder.jpg"
64 },
65 {
66 id: "3",
67 name: "Guacamole con Totopos",
68 description: "Guacamole fresco preparado al momento con chile serrano",
69 price: 75,
70 category: "entradas",
71 imageUrl: "/guacamole-placeholder.jpg",
72 isVegetarian: true,
73 isSpicy: true
74 },
75 {
76 id: "4",
77 name: "Flan Napolitano",
78 description: "Flan casero con caramelo y crema batida",
79 price: 65,
80 category: "postres",
81 imageUrl: "/flan-placeholder.jpg"
82 }
83 ];
84
85 const categories = [
86 { id: 'entradas', name: 'Entradas' },
87 { id: 'platos-principales', name: 'Platos Principales' },
88 { id: 'postres', name: 'Postres' },
89 { id: 'bebidas', name: 'Bebidas' }
90 ];
91
92 const filteredItems = menuItems.filter(item => item.category === activeCategory);
93
94 return (
95 <div className="min-h-screen bg-warm-50">
96 {/* Header con imagen de fondo */}
97 <header
98 className="relative h-screen bg-cover bg-center flex items-center justify-center"
99 style={{
100 backgroundImage: 'linear-gradient(rgba(0,0,0,0.5), rgba(0,0,0,0.5)), url("/restaurant-hero.jpg")'
101 }}
102 >
103 {/* Navegación */}
104 <nav className="absolute top-0 w-full p-6">
105 <div className="container mx-auto flex justify-between items-center">
106 <h1 className="text-white text-2xl font-bold">{restaurantInfo.name}</h1>
107 <div className="hidden md:flex space-x-8 text-white">
108 <a href="#inicio" className="hover:text-yellow-400 transition">Inicio</a>
109 <a href="#menu" className="hover:text-yellow-400 transition">Menú</a>
110 <a href="#ubicacion" className="hover:text-yellow-400 transition">Ubicación</a>
111 <a href="#contacto" className="hover:text-yellow-400 transition">Contacto</a>
112 </div>
113 <button
114 className="md:hidden text-white"
115 onClick={() => setIsMenuOpen(!isMenuOpen)}
116 >
117 ☰
118 </button>
119 </div>
120 </nav>
121
122 {/* Hero Content */}
123 <div className="text-center text-white max-w-2xl mx-auto px-4">
124 <h2 className="text-5xl md:text-6xl font-bold mb-6">
125 {restaurantInfo.name}
126 </h2>
127 <p className="text-xl md:text-2xl mb-8">
128 {restaurantInfo.tagline}
129 </p>
130 <button className="bg-yellow-600 text-white px-8 py-4 rounded-lg text-lg font-semibold hover:bg-yellow-700 transition">
131 Ver Menú
132 </button>
133 </div>
134
135 {/* Scroll indicator */}
136 <div className="absolute bottom-8 left-1/2 transform -translate-x-1/2 text-white animate-bounce">
137 ↓
138 </div>
139 </header>
140
141 {/* Sección Menú */}
142 <section id="menu" className="py-16 bg-white">
143 <div className="container mx-auto px-4">
144 <h2 className="text-4xl font-bold text-center mb-12 text-gray-800">
145 Nuestro Menú
146 </h2>
147
148 {/* Filtros de categoría */}
149 <div className="flex justify-center mb-12">
150 <div className="flex flex-wrap gap-4">
151 {categories.map(category => (
152 <button
153 key={category.id}
154 onClick={() => setActiveCategory(category.id)}
155 className={`px-6 py-3 rounded-lg font-semibold transition ${
156 activeCategory === category.id
157 ? 'bg-yellow-600 text-white'
158 : 'bg-gray-100 text-gray-700 hover:bg-gray-200'
159 }`}
160 >
161 {category.name}
162 </button>
163 ))}
164 </div>
165 </div>
166
167 {/* Grid de platillos */}
168 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
169 {filteredItems.map(item => (
170 <div key={item.id} className="bg-white rounded-lg shadow-lg overflow-hidden hover:shadow-xl transition">
171 <img
172 src={item.imageUrl}
173 alt={item.name}
174 className="w-full h-48 object-cover"
175 />
176 <div className="p-6">
177 <div className="flex justify-between items-start mb-2">
178 <h3 className="font-bold text-xl text-gray-800">{item.name}</h3>
179 <div className="flex space-x-1">
180 {item.isSpicy && <span className="text-red-500">🌶️</span>}
181 {item.isVegetarian && <span className="text-green-500">🥬</span>}
182 </div>
183 </div>
184 <p className="text-gray-600 mb-4">{item.description}</p>
185 <div className="flex justify-between items-center">
186 <span className="text-2xl font-bold text-yellow-600">
187 ${item.price}
188 </span>
189 <button className="bg-yellow-600 text-white px-4 py-2 rounded hover:bg-yellow-700 transition">
190 Ordenar
191 </button>
192 </div>
193 </div>
194 </div>
195 ))}
196 </div>
197 </div>
198 </section>
199
200 {/* Sección Ubicación y Contacto */}
201 <section className="py-16 bg-yellow-50">
202 <div className="container mx-auto px-4">
203 <div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
204
205 {/* Información de contacto */}
206 <div>
207 <h2 className="text-3xl font-bold mb-8 text-gray-800">Visítanos</h2>
208 <div className="space-y-6">
209 <div className="flex items-start">
210 <MapPin className="w-6 h-6 text-yellow-600 mr-4 mt-1" />
211 <div>
212 <h3 className="font-semibold text-lg">Dirección</h3>
213 <p className="text-gray-600">{restaurantInfo.address}</p>
214 </div>
215 </div>
216
217 <div className="flex items-start">
218 <Phone className="w-6 h-6 text-yellow-600 mr-4 mt-1" />
219 <div>
220 <h3 className="font-semibold text-lg">Teléfono</h3>
221 <p className="text-gray-600">{restaurantInfo.phone}</p>
222 </div>
223 </div>
224
225 <div className="flex items-start">
226 <Clock className="w-6 h-6 text-yellow-600 mr-4 mt-1" />
227 <div>
228 <h3 className="font-semibold text-lg">Horarios</h3>
229 <p className="text-gray-600">{restaurantInfo.hours}</p>
230 </div>
231 </div>
232 </div>
233
234 {/* Redes sociales */}
235 <div className="mt-8">
236 <h3 className="font-semibold text-lg mb-4">Síguenos</h3>
237 <div className="flex space-x-4">
238 {restaurantInfo.socialMedia.instagram && (
239 <a
240 href={restaurantInfo.socialMedia.instagram}
241 className="bg-pink-500 text-white p-3 rounded-full hover:bg-pink-600 transition"
242 >
243 <Instagram className="w-5 h-5" />
244 </a>
245 )}
246 {restaurantInfo.socialMedia.facebook && (
247 <a
248 href={restaurantInfo.socialMedia.facebook}
249 className="bg-blue-600 text-white p-3 rounded-full hover:bg-blue-700 transition"
250 >
251 <Facebook className="w-5 h-5" />
252 </a>
253 )}
254 {restaurantInfo.socialMedia.whatsapp && (
255 <a
256 href={restaurantInfo.socialMedia.whatsapp}
257 className="bg-green-500 text-white p-3 rounded-full hover:bg-green-600 transition"
258 >
259 <MessageCircle className="w-5 h-5" />
260 </a>
261 )}
262 </div>
263 </div>
264 </div>
265
266 {/* Mapa placeholder */}
267 <div className="bg-gray-200 rounded-lg h-96 flex items-center justify-center">
268 <p className="text-gray-600">Mapa de Google Maps aquí</p>
269 </div>
270 </div>
271 </div>
272 </section>
273
274 {/* Footer */}
275 <footer className="bg-gray-800 text-white py-8">
276 <div className="container mx-auto px-4 text-center">
277 <h3 className="text-2xl font-bold mb-2">{restaurantInfo.name}</h3>
278 <p className="text-gray-400 mb-4">{restaurantInfo.tagline}</p>
279 <p className="text-sm text-gray-500">
280 © 2024 {restaurantInfo.name}. Todos los derechos reservados.
281 </p>
282 </div>
283 </footer>
284 </div>
285 );
286};
287
288export default RestaurantTheme;1// config/restaurant-config.ts
2export const restaurantConfig = {
3 // El cliente puede modificar estos valores fácilmente
4 branding: {
5 primaryColor: '#D97706', // yellow-600
6 secondaryColor: '#FEF3C7', // yellow-50
7 accentColor: '#DC2626', // red-600 para elementos spicy
8 },
9
10 layout: {
11 showSocialMedia: true,
12 showSpicyIndicators: true,
13 showVegetarianIndicators: true,
14 enableOnlineOrdering: false, // Para versión básica
15 },
16
17 content: {
18 heroImage: '/restaurant-hero.jpg',
19 logoUrl: '/logo.png',
20 favicon: '/favicon.ico',
21 }
22};
23
24// Hook para usar la configuración
25export const useRestaurantConfig = () => {
26 return restaurantConfig;
27};Nichos populares para plantillas en Latam:
Reutilizable: Una vez que tienes el template base, solo cambias colores, textos e imágenes
Escalable: Puedes crear paquetes (básico, premium, enterprise)
Mercado constante: Siempre hay nuevos negocios que necesitan presencia web
Fácil de vender: Los clientes ven exactamente lo que van a recibir
Precio sugerido: $300-2,500 USD dependiendo del paquete y personalización.
1// Ejemplo de fix común: Mejorar accesibilidad en un componente
2interface ButtonProps {
3 children: React.ReactNode;
4 onClick: () => void;
5 variant?: 'primary' | 'secondary';
6 disabled?: boolean;
7 ariaLabel?: string; // Nueva prop para accesibilidad
8}
9
10const AccessibleButton: React.FC<ButtonProps> = ({
11 children,
12 onClick,
13 variant = 'primary',
14 disabled = false,
15 ariaLabel
16}) => {
17 const baseClasses = "px-4 py-2 rounded font-semibold transition focus:outline-none focus:ring-2";
18 const variantClasses = {
19 primary: "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500",
20 secondary: "bg-gray-200 text-gray-800 hover:bg-gray-300 focus:ring-gray-500"
21 };
22
23 return (
24 <button
25 onClick={onClick}
26 disabled={disabled}
27 aria-label={ariaLabel}
28 className={`${baseClasses} ${variantClasses[variant]} ${
29 disabled ? 'opacity-50 cursor-not-allowed' : ''
30 }`}
31 // Mejorar navegación por teclado
32 onKeyDown={(e) => {
33 if (e.key === 'Enter' || e.key === ' ') {
34 e.preventDefault();
35 if (!disabled) onClick();
36 }
37 }}
38 >
39 {children}
40 </button>
41 );
42};
43
44export default AccessibleButton;Rango de bounties: $50-500 USD por contribución, dependiendo de la complejidad.
México y Latam tienen una vibrante comunidad de eventos, conferencias y meetups que necesitan aplicaciones web personalizadas.
1// types/event.ts
2export interface Event {
3 id: string;
4 title: string;
5 description: string;
6 date: Date;
7 location: string;
8 maxAttendees: number;
9 currentAttendees: number;
10 price: number;
11 imageUrl: string;
12}
13
14export interface Attendee {
15 id: string;
16 name: string;
17 email: string;
18 eventId: string;
19 registrationDate: Date;
20}
21
22// components/EventManager.tsx
23import { useState, useEffect } from 'react';
24
25const EventManager: React.FC = () => {
26 const [events, setEvents] = useState<Event[]>([]);
27 const [selectedEvent, setSelectedEvent] = useState<Event | null>(null);
28 const [attendeeForm, setAttendeeForm] = useState({
29 name: '',
30 email: ''
31 });
32
33 // Simular carga de eventos
34 useEffect(() => {
35 const mockEvents: Event[] = [
36 {
37 id: '1',
38 title: 'React Meetup CDMX',
39 description: 'Aprende las últimas tendencias en React',
40 date: new Date('2024-06-15'),
41 location: 'WeWork Polanco, Ciudad de México',
42 maxAttendees: 50,
43 currentAttendees: 23,
44 price: 0,
45 imageUrl: '/event-placeholder.jpg'
46 },
47 {
48 id: '2',
49 title: 'Workshop de TypeScript',
50 description: 'Domina TypeScript desde cero',
51 date: new Date('2024-06-20'),
52 location: 'Hub de Innovación, Guadalajara',
53 maxAttendees: 30,
54 currentAttendees: 15,
55 price: 500,
56 imageUrl: '/workshop-placeholder.jpg'
57 }
58 ];
59 setEvents(mockEvents);
60 }, []);
61
62 const handleRegistration = async (eventId: string) => {
63 if (!attendeeForm.name || !attendeeForm.email) {
64 alert('Por favor completa todos los campos');
65 return;
66 }
67
68 try {
69 // Aquí harías la llamada a tu API
70 const response = await fetch('/api/events/register', {
71 method: 'POST',
72 headers: {
73 'Content-Type': 'application/json',
74 },
75 body: JSON.stringify({
76 ...attendeeForm,
77 eventId
78 }),
79 });
80
81 if (response.ok) {
82 alert('¡Registro exitoso!');
83 setAttendeeForm({ name: '', email: '' });
84 setSelectedEvent(null);
85
86 // Actualizar contador de asistentes
87 setEvents(prev => prev.map(event =>
88 event.id === eventId
89 ? { ...event, currentAttendees: event.currentAttendees + 1 }
90 : event
91 ));
92 }
93 } catch (error) {
94 console.error('Error en registro:', error);
95 alert('Error al registrarse. Intenta nuevamente.');
96 }
97 };
98
99 return (
100 <div className="container mx-auto px-4 py-8">
101 <h1 className="text-3xl font-bold mb-8 text-center">
102 Eventos Tech en México
103 </h1>
104
105 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
106 {events.map(event => (
107 <div key={event.id} className="bg-white rounded-lg shadow-md overflow-hidden">
108 <img
109 src={event.imageUrl}
110 alt={event.title}
111 className="w-full h-48 object-cover"
112 />
113 <div className="p-6">
114 <h3 className="font-bold text-xl mb-2">{event.title}</h3>
115 <p className="text-gray-600 mb-3">{event.description}</p>
116
117 <div className="space-y-2 text-sm text-gray-700 mb-4">
118 <p>📅 {event.date.toLocaleDateString('es-MX')}</p>
119 <p>📍 {event.location}</p>
120 <p>👥 {event.currentAttendees}/{event.maxAttendees} asistentes</p>
121 <p className="font-semibold text-green-600">
122 {event.price === 0 ? 'Gratis' : `$${event.price} MXN`}
123 </p>
124 </div>
125
126 <button
127 onClick={() => setSelectedEvent(event)}
128 disabled={event.currentAttendees >= event.maxAttendees}
129 className={`w-full py-2 px-4 rounded font-semibold ${
130 event.currentAttendees >= event.maxAttendees
131 ? 'bg-gray-300 text-gray-500 cursor-not-allowed'
132 : 'bg-blue-600 text-white hover:bg-blue-700'
133 }`}
134 >
135 {event.currentAttendees >= event.maxAttendees ? 'Cupo lleno' : 'Registrarse'}
136 </button>
137 </div>
138 </div>
139 ))}
140 </div>
141
142 {/* Modal de registro */}
143 {selectedEvent && (
144 <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
145 <div className="bg-white rounded-lg p-6 w-full max-w-md">
146 <h2 className="text-2xl font-bold mb-4">
147 Registrarse a: {selectedEvent.title}
148 </h2>
149
150 <form onSubmit={(e) => {
151 e.preventDefault();
152 handleRegistration(selectedEvent.id);
153 }}>
154 <div className="space-y-4">
155 <input
156 type="text"
157 placeholder="Tu nombre completo"
158 value={attendeeForm.name}
159 onChange={(e) => setAttendeeForm({
160 ...attendeeForm,
161 name: e.target.value
162 })}
163 className="w-full p-3 border rounded-lg focus:ring-2 focus:ring-blue-500"
164 required
165 />
166
167 <input
168 type="email"
169 placeholder="Tu email"
170 value={attendeeForm.email}
171 onChange={(e) => setAttendeeForm({
172 ...attendeeForm,
173 email: e.target.value
174 })}
175 className="w-full p-3 border rounded-lg focus:ring-2 focus:ring-blue-500"
176 required
177 />
178 </div>
179
180 <div className="flex gap-4 mt-6">
181 <button
182 type="button"
183 onClick={() => setSelectedEvent(null)}
184 className="flex-1 py-2 px-4 border border-gray-300 rounded hover:bg-gray-50"
185 >
186 Cancelar
187 </button>
188 <button
189 type="submit"
190 className="flex-1 py-2 px-4 bg-blue-600 text-white rounded hover:bg-blue-700"
191 >
192 Confirmar Registro
193 </button>
194 </div>
195 </form>
196 </div>
197 </div>
198 )}
199 </div>
200 );
201};
202
203export default EventManager;Precio sugerido: $800-2,000 USD por una aplicación completa de gestión de eventos.
Usa GitHub Pages o Vercel para mostrar tus proyectos. Asegúrate de que cada proyecto tenga:
1# Setup básico para proyectos
2npx create-next-app@latest mi-proyecto --typescript --tailwind --eslint
3cd mi-proyecto
4npm install
5
6# Herramientas de desarrollo
7npm install -D prettier
8npm install lucide-react # Para iconos
9npm install @headlessui/react # Para componentes accesiblesRecuerda que cada proyecto, aunque no sea inmensamente rentable, es una inversión en tu experiencia. Los clientes satisfechos se convierten en referencias, y las habilidades que desarrollas te posicionan para oportunidades más grandes.
El mercado tech en México y Latinoamérica está madurando rápidamente, y hay espacio para desarrolladores junior motivados. Empieza con proyectos pequeños, construye tu reputación, y ve escalando gradualmente. Lo más importante es comenzar, porque la experiencia que ganes será tan valiosa como el dinero que puedas ganar.
¡El momento perfecto para empezar fue ayer, el segundo mejor momento es ahora! 🚀
¿Te resultó útil este artículo? Compártelo con otros desarrolladores que estén comenzando su carrera en tech.