create-supa-kit 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -0
- package/bin/index.js +163 -0
- package/package.json +35 -0
- package/templates/react-supabase/.env.example +5 -0
- package/templates/react-supabase/README.md +64 -0
- package/templates/react-supabase/index.html +12 -0
- package/templates/react-supabase/package.json +20 -0
- package/templates/react-supabase/src/App.jsx +54 -0
- package/templates/react-supabase/src/components/Dashboard.jsx +117 -0
- package/templates/react-supabase/src/components/Login.jsx +181 -0
- package/templates/react-supabase/src/index.css +17 -0
- package/templates/react-supabase/src/lib/supabaseClient.js +19 -0
- package/templates/react-supabase/src/main.jsx +10 -0
- package/templates/react-supabase/vite.config.js +6 -0
- package/templates/react-supabase-tailwind/.env.example +5 -0
- package/templates/react-supabase-tailwind/index.html +12 -0
- package/templates/react-supabase-tailwind/package.json +23 -0
- package/templates/react-supabase-tailwind/postcss.config.js +6 -0
- package/templates/react-supabase-tailwind/src/App.jsx +32 -0
- package/templates/react-supabase-tailwind/src/components/Dashboard.jsx +45 -0
- package/templates/react-supabase-tailwind/src/components/Login.jsx +94 -0
- package/templates/react-supabase-tailwind/src/index.css +3 -0
- package/templates/react-supabase-tailwind/src/lib/supabaseClient.js +13 -0
- package/templates/react-supabase-tailwind/src/main.jsx +10 -0
- package/templates/react-supabase-tailwind/tailwind.config.js +8 -0
- package/templates/react-supabase-tailwind/vite.config.js +6 -0
- package/templates/react-supabase-ts/.env.example +5 -0
- package/templates/react-supabase-ts/index.html +12 -0
- package/templates/react-supabase-ts/package.json +23 -0
- package/templates/react-supabase-ts/src/App.tsx +33 -0
- package/templates/react-supabase-ts/src/components/Dashboard.tsx +56 -0
- package/templates/react-supabase-ts/src/components/Login.tsx +104 -0
- package/templates/react-supabase-ts/src/index.css +8 -0
- package/templates/react-supabase-ts/src/lib/supabaseClient.ts +13 -0
- package/templates/react-supabase-ts/src/main.tsx +10 -0
- package/templates/react-supabase-ts/tsconfig.json +20 -0
- package/templates/react-supabase-ts/vite.config.ts +6 -0
- package/templates/react-supabase-ts-tailwind/.env.example +5 -0
- package/templates/react-supabase-ts-tailwind/index.html +12 -0
- package/templates/react-supabase-ts-tailwind/package.json +26 -0
- package/templates/react-supabase-ts-tailwind/postcss.config.js +6 -0
- package/templates/react-supabase-ts-tailwind/src/App.tsx +33 -0
- package/templates/react-supabase-ts-tailwind/src/components/Dashboard.tsx +50 -0
- package/templates/react-supabase-ts-tailwind/src/components/Login.tsx +94 -0
- package/templates/react-supabase-ts-tailwind/src/index.css +3 -0
- package/templates/react-supabase-ts-tailwind/src/lib/supabaseClient.ts +13 -0
- package/templates/react-supabase-ts-tailwind/src/main.tsx +10 -0
- package/templates/react-supabase-ts-tailwind/tailwind.config.js +8 -0
- package/templates/react-supabase-ts-tailwind/tsconfig.json +20 -0
- package/templates/react-supabase-ts-tailwind/vite.config.ts +6 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { supabase } from '../lib/supabaseClient';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Login — formulario de autenticación con email y contraseña.
|
|
6
|
+
*
|
|
7
|
+
* Permite:
|
|
8
|
+
* - Iniciar sesión (sign in)
|
|
9
|
+
* - Registrarse (sign up) con el mismo formulario (toggle)
|
|
10
|
+
*
|
|
11
|
+
* Supabase gestiona la sesión automáticamente tras el login correcto.
|
|
12
|
+
* App.jsx reacciona al cambio de sesión y monta <Dashboard>.
|
|
13
|
+
*/
|
|
14
|
+
export default function Login() {
|
|
15
|
+
const [email, setEmail] = useState('');
|
|
16
|
+
const [password, setPassword] = useState('');
|
|
17
|
+
const [isSignUp, setIsSignUp] = useState(false);
|
|
18
|
+
const [loading, setLoading] = useState(false);
|
|
19
|
+
const [error, setError] = useState('');
|
|
20
|
+
const [message, setMessage] = useState('');
|
|
21
|
+
|
|
22
|
+
async function handleSubmit(e) {
|
|
23
|
+
e.preventDefault();
|
|
24
|
+
setLoading(true);
|
|
25
|
+
setError('');
|
|
26
|
+
setMessage('');
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
if (isSignUp) {
|
|
30
|
+
// ── Registro ───────────────────────────────────────────────────────────
|
|
31
|
+
const { error } = await supabase.auth.signUp({ email, password });
|
|
32
|
+
if (error) throw error;
|
|
33
|
+
setMessage('✅ Revisa tu email para confirmar tu cuenta.');
|
|
34
|
+
} else {
|
|
35
|
+
// ── Login ──────────────────────────────────────────────────────────────
|
|
36
|
+
const { error } = await supabase.auth.signInWithPassword({ email, password });
|
|
37
|
+
if (error) throw error;
|
|
38
|
+
// App.jsx detecta el cambio de sesión y renderiza Dashboard
|
|
39
|
+
}
|
|
40
|
+
} catch (err) {
|
|
41
|
+
setError(err.message);
|
|
42
|
+
} finally {
|
|
43
|
+
setLoading(false);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div style={styles.wrapper}>
|
|
49
|
+
<div style={styles.card}>
|
|
50
|
+
<h1 style={styles.title}>
|
|
51
|
+
{isSignUp ? 'Crear cuenta' : 'Iniciar sesión'}
|
|
52
|
+
</h1>
|
|
53
|
+
|
|
54
|
+
<form onSubmit={handleSubmit} style={styles.form}>
|
|
55
|
+
<label style={styles.label}>
|
|
56
|
+
Email
|
|
57
|
+
<input
|
|
58
|
+
style={styles.input}
|
|
59
|
+
type="email"
|
|
60
|
+
value={email}
|
|
61
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
62
|
+
placeholder="tu@email.com"
|
|
63
|
+
required
|
|
64
|
+
autoComplete="email"
|
|
65
|
+
/>
|
|
66
|
+
</label>
|
|
67
|
+
|
|
68
|
+
<label style={styles.label}>
|
|
69
|
+
Contraseña
|
|
70
|
+
<input
|
|
71
|
+
style={styles.input}
|
|
72
|
+
type="password"
|
|
73
|
+
value={password}
|
|
74
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
75
|
+
placeholder="••••••••"
|
|
76
|
+
required
|
|
77
|
+
autoComplete={isSignUp ? 'new-password' : 'current-password'}
|
|
78
|
+
minLength={6}
|
|
79
|
+
/>
|
|
80
|
+
</label>
|
|
81
|
+
|
|
82
|
+
{error && <p style={styles.error}>{error}</p>}
|
|
83
|
+
{message && <p style={styles.success}>{message}</p>}
|
|
84
|
+
|
|
85
|
+
<button style={styles.button} type="submit" disabled={loading}>
|
|
86
|
+
{loading
|
|
87
|
+
? 'Procesando...'
|
|
88
|
+
: isSignUp ? 'Registrarse' : 'Entrar'}
|
|
89
|
+
</button>
|
|
90
|
+
</form>
|
|
91
|
+
|
|
92
|
+
<p style={styles.toggle}>
|
|
93
|
+
{isSignUp ? '¿Ya tienes cuenta?' : '¿No tienes cuenta?'}{' '}
|
|
94
|
+
<button
|
|
95
|
+
style={styles.link}
|
|
96
|
+
type="button"
|
|
97
|
+
onClick={() => { setIsSignUp(!isSignUp); setError(''); setMessage(''); }}
|
|
98
|
+
>
|
|
99
|
+
{isSignUp ? 'Inicia sesión' : 'Regístrate'}
|
|
100
|
+
</button>
|
|
101
|
+
</p>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ─── Estilos inline mínimos (reemplaza con tu sistema de diseño) ───────────────
|
|
108
|
+
const styles = {
|
|
109
|
+
wrapper: {
|
|
110
|
+
display: 'flex',
|
|
111
|
+
justifyContent: 'center',
|
|
112
|
+
alignItems: 'center',
|
|
113
|
+
minHeight: '100vh',
|
|
114
|
+
padding: '1rem',
|
|
115
|
+
},
|
|
116
|
+
card: {
|
|
117
|
+
background: '#fff',
|
|
118
|
+
borderRadius: '12px',
|
|
119
|
+
padding: '2rem',
|
|
120
|
+
width: '100%',
|
|
121
|
+
maxWidth: '400px',
|
|
122
|
+
boxShadow: '0 4px 24px rgba(0,0,0,0.08)',
|
|
123
|
+
},
|
|
124
|
+
title: {
|
|
125
|
+
fontSize: '1.5rem',
|
|
126
|
+
fontWeight: 700,
|
|
127
|
+
marginBottom: '1.5rem',
|
|
128
|
+
textAlign: 'center',
|
|
129
|
+
},
|
|
130
|
+
form: {
|
|
131
|
+
display: 'flex',
|
|
132
|
+
flexDirection: 'column',
|
|
133
|
+
gap: '1rem',
|
|
134
|
+
},
|
|
135
|
+
label: {
|
|
136
|
+
display: 'flex',
|
|
137
|
+
flexDirection: 'column',
|
|
138
|
+
gap: '4px',
|
|
139
|
+
fontSize: '0.875rem',
|
|
140
|
+
fontWeight: 500,
|
|
141
|
+
},
|
|
142
|
+
input: {
|
|
143
|
+
padding: '0.625rem 0.75rem',
|
|
144
|
+
borderRadius: '8px',
|
|
145
|
+
border: '1px solid #cbd5e1',
|
|
146
|
+
fontSize: '1rem',
|
|
147
|
+
outline: 'none',
|
|
148
|
+
},
|
|
149
|
+
button: {
|
|
150
|
+
marginTop: '0.5rem',
|
|
151
|
+
padding: '0.75rem',
|
|
152
|
+
borderRadius: '8px',
|
|
153
|
+
background: '#3ecf8e',
|
|
154
|
+
color: '#fff',
|
|
155
|
+
fontWeight: 600,
|
|
156
|
+
fontSize: '1rem',
|
|
157
|
+
border: 'none',
|
|
158
|
+
},
|
|
159
|
+
error: {
|
|
160
|
+
color: '#ef4444',
|
|
161
|
+
fontSize: '0.875rem',
|
|
162
|
+
},
|
|
163
|
+
success: {
|
|
164
|
+
color: '#22c55e',
|
|
165
|
+
fontSize: '0.875rem',
|
|
166
|
+
},
|
|
167
|
+
toggle: {
|
|
168
|
+
marginTop: '1.25rem',
|
|
169
|
+
textAlign: 'center',
|
|
170
|
+
fontSize: '0.875rem',
|
|
171
|
+
color: '#64748b',
|
|
172
|
+
},
|
|
173
|
+
link: {
|
|
174
|
+
background: 'none',
|
|
175
|
+
border: 'none',
|
|
176
|
+
color: '#3ecf8e',
|
|
177
|
+
fontWeight: 600,
|
|
178
|
+
fontSize: '0.875rem',
|
|
179
|
+
textDecoration: 'underline',
|
|
180
|
+
},
|
|
181
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/* Reset y base mínima — reemplaza con tu sistema de diseño */
|
|
2
|
+
*, *::before, *::after {
|
|
3
|
+
box-sizing: border-box;
|
|
4
|
+
margin: 0;
|
|
5
|
+
padding: 0;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
body {
|
|
9
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
10
|
+
background: #f8fafc;
|
|
11
|
+
color: #1e293b;
|
|
12
|
+
min-height: 100vh;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
button {
|
|
16
|
+
cursor: pointer;
|
|
17
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createClient } from '@supabase/supabase-js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Cliente único de Supabase para toda la app.
|
|
5
|
+
*
|
|
6
|
+
* Las variables de entorno se leen desde el archivo .env (copia de .env.example).
|
|
7
|
+
* Vite expone sólo las variables que empiezan con VITE_.
|
|
8
|
+
*/
|
|
9
|
+
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
|
|
10
|
+
const supabaseKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
|
|
11
|
+
|
|
12
|
+
if (!supabaseUrl || !supabaseKey) {
|
|
13
|
+
throw new Error(
|
|
14
|
+
'❌ Faltan variables de entorno de Supabase.\n' +
|
|
15
|
+
'Copia .env.example a .env y completa VITE_SUPABASE_URL y VITE_SUPABASE_ANON_KEY.'
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const supabase = createClient(supabaseUrl, supabaseKey);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# ─── Supabase ──────────────────────────────────────────────────────────────────
|
|
2
|
+
# Obtén estos valores en: https://app.supabase.com → Settings → API
|
|
3
|
+
|
|
4
|
+
VITE_SUPABASE_URL=https://xxxxxxxxxxxxxxxxxxx.supabase.co
|
|
5
|
+
VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="es">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Mi App Supabase</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/main.jsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "template-react-supabase-tailwind",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "vite build",
|
|
9
|
+
"preview": "vite preview"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@supabase/supabase-js": "^2.39.7",
|
|
13
|
+
"react": "^18.2.0",
|
|
14
|
+
"react-dom": "^18.2.0"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@vitejs/plugin-react": "^4.2.1",
|
|
18
|
+
"tailwindcss": "^3.4.1",
|
|
19
|
+
"autoprefixer": "^10.4.17",
|
|
20
|
+
"postcss": "^8.4.35",
|
|
21
|
+
"vite": "^5.1.4"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { supabase } from './lib/supabaseClient';
|
|
3
|
+
import Login from './components/Login';
|
|
4
|
+
import Dashboard from './components/Dashboard';
|
|
5
|
+
|
|
6
|
+
export default function App() {
|
|
7
|
+
const [session, setSession] = useState(null);
|
|
8
|
+
const [loading, setLoading] = useState(true);
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
supabase.auth.getSession().then(({ data: { session } }) => {
|
|
12
|
+
setSession(session);
|
|
13
|
+
setLoading(false);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const { data: { subscription } } = supabase.auth.onAuthStateChange(
|
|
17
|
+
(_event, session) => setSession(session)
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
return () => subscription.unsubscribe();
|
|
21
|
+
}, []);
|
|
22
|
+
|
|
23
|
+
if (loading) {
|
|
24
|
+
return (
|
|
25
|
+
<div className="flex items-center justify-center min-h-screen">
|
|
26
|
+
<p className="text-slate-500">Cargando...</p>
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return session ? <Dashboard session={session} /> : <Login />;
|
|
32
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { supabase } from '../lib/supabaseClient';
|
|
2
|
+
|
|
3
|
+
export default function Dashboard({ session }) {
|
|
4
|
+
const { user } = session;
|
|
5
|
+
|
|
6
|
+
async function handleLogout() {
|
|
7
|
+
const { error } = await supabase.auth.signOut();
|
|
8
|
+
if (error) console.error('Error al cerrar sesión:', error.message);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<div className="min-h-screen bg-slate-50 flex justify-center items-start py-8 px-4">
|
|
13
|
+
<div className="bg-white rounded-2xl shadow-lg p-8 w-full max-w-2xl">
|
|
14
|
+
{/* Cabecera */}
|
|
15
|
+
<header className="flex items-center justify-between mb-6">
|
|
16
|
+
<h1 className="text-2xl font-bold text-slate-800">Dashboard</h1>
|
|
17
|
+
<button
|
|
18
|
+
onClick={handleLogout}
|
|
19
|
+
className="px-4 py-2 text-sm font-medium rounded-lg border border-slate-200 bg-slate-100 hover:bg-slate-200 transition-colors"
|
|
20
|
+
>
|
|
21
|
+
Cerrar sesión
|
|
22
|
+
</button>
|
|
23
|
+
</header>
|
|
24
|
+
|
|
25
|
+
{/* Contenido */}
|
|
26
|
+
<section className="mb-6">
|
|
27
|
+
<p className="text-lg text-slate-700 mb-1">
|
|
28
|
+
¡Hola, <span className="font-semibold">{user.email}</span>!
|
|
29
|
+
</p>
|
|
30
|
+
<p className="text-slate-500 text-sm">Estás autenticado correctamente. 🎉</p>
|
|
31
|
+
</section>
|
|
32
|
+
|
|
33
|
+
{/* Debug */}
|
|
34
|
+
<details className="mt-4">
|
|
35
|
+
<summary className="text-xs text-slate-400 cursor-pointer select-none">
|
|
36
|
+
Ver datos de sesión (debug)
|
|
37
|
+
</summary>
|
|
38
|
+
<pre className="mt-3 bg-slate-100 rounded-lg p-4 text-xs overflow-x-auto text-slate-600">
|
|
39
|
+
{JSON.stringify({ id: user.id, email: user.email, role: user.role }, null, 2)}
|
|
40
|
+
</pre>
|
|
41
|
+
</details>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { supabase } from '../lib/supabaseClient';
|
|
3
|
+
|
|
4
|
+
export default function Login() {
|
|
5
|
+
const [email, setEmail] = useState('');
|
|
6
|
+
const [password, setPassword] = useState('');
|
|
7
|
+
const [isSignUp, setIsSignUp] = useState(false);
|
|
8
|
+
const [loading, setLoading] = useState(false);
|
|
9
|
+
const [error, setError] = useState('');
|
|
10
|
+
const [message, setMessage] = useState('');
|
|
11
|
+
|
|
12
|
+
async function handleSubmit(e) {
|
|
13
|
+
e.preventDefault();
|
|
14
|
+
setLoading(true);
|
|
15
|
+
setError('');
|
|
16
|
+
setMessage('');
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
if (isSignUp) {
|
|
20
|
+
const { error } = await supabase.auth.signUp({ email, password });
|
|
21
|
+
if (error) throw error;
|
|
22
|
+
setMessage('✅ Revisa tu email para confirmar tu cuenta.');
|
|
23
|
+
} else {
|
|
24
|
+
const { error } = await supabase.auth.signInWithPassword({ email, password });
|
|
25
|
+
if (error) throw error;
|
|
26
|
+
}
|
|
27
|
+
} catch (err) {
|
|
28
|
+
setError(err.message);
|
|
29
|
+
} finally {
|
|
30
|
+
setLoading(false);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className="flex items-center justify-center min-h-screen bg-slate-50 px-4">
|
|
36
|
+
<div className="bg-white rounded-2xl shadow-lg p-8 w-full max-w-md">
|
|
37
|
+
<h1 className="text-2xl font-bold text-center mb-6 text-slate-800">
|
|
38
|
+
{isSignUp ? 'Crear cuenta' : 'Iniciar sesión'}
|
|
39
|
+
</h1>
|
|
40
|
+
|
|
41
|
+
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
|
42
|
+
<label className="flex flex-col gap-1 text-sm font-medium text-slate-700">
|
|
43
|
+
Email
|
|
44
|
+
<input
|
|
45
|
+
type="email"
|
|
46
|
+
value={email}
|
|
47
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
48
|
+
placeholder="tu@email.com"
|
|
49
|
+
required
|
|
50
|
+
autoComplete="email"
|
|
51
|
+
className="px-3 py-2.5 rounded-lg border border-slate-300 text-base focus:outline-none focus:ring-2 focus:ring-emerald-400"
|
|
52
|
+
/>
|
|
53
|
+
</label>
|
|
54
|
+
|
|
55
|
+
<label className="flex flex-col gap-1 text-sm font-medium text-slate-700">
|
|
56
|
+
Contraseña
|
|
57
|
+
<input
|
|
58
|
+
type="password"
|
|
59
|
+
value={password}
|
|
60
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
61
|
+
placeholder="••••••••"
|
|
62
|
+
required
|
|
63
|
+
minLength={6}
|
|
64
|
+
autoComplete={isSignUp ? 'new-password' : 'current-password'}
|
|
65
|
+
className="px-3 py-2.5 rounded-lg border border-slate-300 text-base focus:outline-none focus:ring-2 focus:ring-emerald-400"
|
|
66
|
+
/>
|
|
67
|
+
</label>
|
|
68
|
+
|
|
69
|
+
{error && <p className="text-red-500 text-sm">{error}</p>}
|
|
70
|
+
{message && <p className="text-emerald-600 text-sm">{message}</p>}
|
|
71
|
+
|
|
72
|
+
<button
|
|
73
|
+
type="submit"
|
|
74
|
+
disabled={loading}
|
|
75
|
+
className="mt-1 py-3 rounded-lg bg-emerald-500 hover:bg-emerald-600 text-white font-semibold text-base transition-colors disabled:opacity-60"
|
|
76
|
+
>
|
|
77
|
+
{loading ? 'Procesando...' : isSignUp ? 'Registrarse' : 'Entrar'}
|
|
78
|
+
</button>
|
|
79
|
+
</form>
|
|
80
|
+
|
|
81
|
+
<p className="mt-5 text-center text-sm text-slate-500">
|
|
82
|
+
{isSignUp ? '¿Ya tienes cuenta?' : '¿No tienes cuenta?'}{' '}
|
|
83
|
+
<button
|
|
84
|
+
type="button"
|
|
85
|
+
onClick={() => { setIsSignUp(!isSignUp); setError(''); setMessage(''); }}
|
|
86
|
+
className="text-emerald-600 font-semibold underline"
|
|
87
|
+
>
|
|
88
|
+
{isSignUp ? 'Inicia sesión' : 'Regístrate'}
|
|
89
|
+
</button>
|
|
90
|
+
</p>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createClient } from '@supabase/supabase-js';
|
|
2
|
+
|
|
3
|
+
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
|
|
4
|
+
const supabaseKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
|
|
5
|
+
|
|
6
|
+
if (!supabaseUrl || !supabaseKey) {
|
|
7
|
+
throw new Error(
|
|
8
|
+
'❌ Faltan variables de entorno de Supabase.\n' +
|
|
9
|
+
'Copia .env.example a .env y completa VITE_SUPABASE_URL y VITE_SUPABASE_ANON_KEY.'
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const supabase = createClient(supabaseUrl, supabaseKey);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# ─── Supabase ──────────────────────────────────────────────────────────────────
|
|
2
|
+
# Obtén estos valores en: https://app.supabase.com → Settings → API
|
|
3
|
+
|
|
4
|
+
VITE_SUPABASE_URL=https://xxxxxxxxxxxxxxxxxxx.supabase.co
|
|
5
|
+
VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="es">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Mi App Supabase</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "template-react-supabase-ts",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "tsc && vite build",
|
|
9
|
+
"preview": "vite preview"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@supabase/supabase-js": "^2.39.7",
|
|
13
|
+
"react": "^18.2.0",
|
|
14
|
+
"react-dom": "^18.2.0"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/react": "^18.2.55",
|
|
18
|
+
"@types/react-dom": "^18.2.19",
|
|
19
|
+
"@vitejs/plugin-react": "^4.2.1",
|
|
20
|
+
"typescript": "^5.3.3",
|
|
21
|
+
"vite": "^5.1.4"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import type { Session } from '@supabase/supabase-js';
|
|
3
|
+
import { supabase } from './lib/supabaseClient';
|
|
4
|
+
import Login from './components/Login';
|
|
5
|
+
import Dashboard from './components/Dashboard';
|
|
6
|
+
|
|
7
|
+
export default function App() {
|
|
8
|
+
const [session, setSession] = useState<Session | null>(null);
|
|
9
|
+
const [loading, setLoading] = useState(true);
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
supabase.auth.getSession().then(({ data: { session } }) => {
|
|
13
|
+
setSession(session);
|
|
14
|
+
setLoading(false);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const { data: { subscription } } = supabase.auth.onAuthStateChange(
|
|
18
|
+
(_event, session) => setSession(session)
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
return () => subscription.unsubscribe();
|
|
22
|
+
}, []);
|
|
23
|
+
|
|
24
|
+
if (loading) {
|
|
25
|
+
return (
|
|
26
|
+
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh' }}>
|
|
27
|
+
<p>Cargando...</p>
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return session ? <Dashboard session={session} /> : <Login />;
|
|
33
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Session } from '@supabase/supabase-js';
|
|
2
|
+
import { supabase } from '../lib/supabaseClient';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
session: Session;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default function Dashboard({ session }: Props) {
|
|
9
|
+
const { user } = session;
|
|
10
|
+
|
|
11
|
+
async function handleLogout() {
|
|
12
|
+
const { error } = await supabase.auth.signOut();
|
|
13
|
+
if (error) console.error('Error al cerrar sesión:', error.message);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div style={styles.wrapper}>
|
|
18
|
+
<div style={styles.card}>
|
|
19
|
+
<header style={styles.header}>
|
|
20
|
+
<h1 style={styles.title}>Dashboard</h1>
|
|
21
|
+
<button style={styles.logoutBtn} onClick={handleLogout}>
|
|
22
|
+
Cerrar sesión
|
|
23
|
+
</button>
|
|
24
|
+
</header>
|
|
25
|
+
|
|
26
|
+
<section style={styles.section}>
|
|
27
|
+
<p style={styles.welcome}>
|
|
28
|
+
¡Hola, <strong>{user.email}</strong>!
|
|
29
|
+
</p>
|
|
30
|
+
<p style={styles.hint}>Estás autenticado correctamente. 🎉</p>
|
|
31
|
+
</section>
|
|
32
|
+
|
|
33
|
+
<details style={styles.details}>
|
|
34
|
+
<summary style={styles.summary}>Ver datos de sesión (debug)</summary>
|
|
35
|
+
<pre style={styles.pre}>
|
|
36
|
+
{JSON.stringify({ id: user.id, email: user.email, role: user.role }, null, 2)}
|
|
37
|
+
</pre>
|
|
38
|
+
</details>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const styles: Record<string, React.CSSProperties> = {
|
|
45
|
+
wrapper: { display: 'flex', justifyContent: 'center', alignItems: 'flex-start', minHeight: '100vh', padding: '2rem 1rem', background: '#f8fafc' },
|
|
46
|
+
card: { background: '#fff', borderRadius: '12px', padding: '2rem', width: '100%', maxWidth: '640px', boxShadow: '0 4px 24px rgba(0,0,0,0.08)' },
|
|
47
|
+
header: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1.5rem' },
|
|
48
|
+
title: { fontSize: '1.5rem', fontWeight: 700 },
|
|
49
|
+
logoutBtn: { padding: '0.5rem 1rem', borderRadius: '8px', background: '#f1f5f9', border: '1px solid #e2e8f0', fontWeight: 500, fontSize: '0.875rem' },
|
|
50
|
+
section: { marginBottom: '1.5rem' },
|
|
51
|
+
welcome: { fontSize: '1.125rem', marginBottom: '0.5rem' },
|
|
52
|
+
hint: { color: '#64748b', fontSize: '0.9rem' },
|
|
53
|
+
details: { marginTop: '1rem' },
|
|
54
|
+
summary: { cursor: 'pointer', fontSize: '0.875rem', color: '#94a3b8' },
|
|
55
|
+
pre: { marginTop: '0.75rem', background: '#f1f5f9', borderRadius: '8px', padding: '1rem', fontSize: '0.8rem', overflowX: 'auto' },
|
|
56
|
+
};
|