david
2 years ago
15 changed files with 1762 additions and 106 deletions
@ -1,38 +0,0 @@ |
|||||||
.App { |
|
||||||
text-align: center; |
|
||||||
} |
|
||||||
|
|
||||||
.App-logo { |
|
||||||
height: 40vmin; |
|
||||||
pointer-events: none; |
|
||||||
} |
|
||||||
|
|
||||||
@media (prefers-reduced-motion: no-preference) { |
|
||||||
.App-logo { |
|
||||||
animation: App-logo-spin infinite 20s linear; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
.App-header { |
|
||||||
background-color: #282c34; |
|
||||||
min-height: 100vh; |
|
||||||
display: flex; |
|
||||||
flex-direction: column; |
|
||||||
align-items: center; |
|
||||||
justify-content: center; |
|
||||||
font-size: calc(10px + 2vmin); |
|
||||||
color: white; |
|
||||||
} |
|
||||||
|
|
||||||
.App-link { |
|
||||||
color: #61dafb; |
|
||||||
} |
|
||||||
|
|
||||||
@keyframes App-logo-spin { |
|
||||||
from { |
|
||||||
transform: rotate(0deg); |
|
||||||
} |
|
||||||
to { |
|
||||||
transform: rotate(360deg); |
|
||||||
} |
|
||||||
} |
|
@ -1,8 +0,0 @@ |
|||||||
import { render, screen } from '@testing-library/react'; |
|
||||||
import App from './App'; |
|
||||||
|
|
||||||
test('renders learn react link', () => { |
|
||||||
render(<App />); |
|
||||||
const linkElement = screen.getByText(/learn react/i); |
|
||||||
expect(linkElement).toBeInTheDocument(); |
|
||||||
}); |
|
@ -0,0 +1,176 @@ |
|||||||
|
import { useState } from 'react'; |
||||||
|
import { |
||||||
|
Box, |
||||||
|
Tabs, |
||||||
|
Tab, |
||||||
|
Typography, |
||||||
|
AppBar, |
||||||
|
TextField, |
||||||
|
Stack, |
||||||
|
} from '@mui/material'; |
||||||
|
|
||||||
|
import LoadingButton from '@mui/lab/LoadingButton'; |
||||||
|
import { useSnackbar } from '../Contexts/SnackbarContext'; |
||||||
|
import { useAuth } from '../Contexts/AuthContext'; |
||||||
|
|
||||||
|
export function AuthForm({ setSession }) { |
||||||
|
const [tabindex, setTabIndex] = useState(0); |
||||||
|
|
||||||
|
const handleChange = (event, newIndex) => { |
||||||
|
setTabIndex(newIndex); |
||||||
|
}; |
||||||
|
|
||||||
|
const TabPanel = (props) => { |
||||||
|
const { children, value, index, ...other } = props; |
||||||
|
|
||||||
|
return ( |
||||||
|
<div |
||||||
|
role="tabpanel" |
||||||
|
hidden={value !== index} |
||||||
|
id={`full-width-tabpanel-${index}`} |
||||||
|
aria-labelledby={`full-width-tab-${index}`} |
||||||
|
{...other} |
||||||
|
> |
||||||
|
{value === index && ( |
||||||
|
<Box sx={{ p: 3 }}> |
||||||
|
<Typography component={'div'}>{children}</Typography> |
||||||
|
</Box> |
||||||
|
)} |
||||||
|
</div> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
const a11yProps = (index) => { |
||||||
|
return { |
||||||
|
id: `full-width-tab-${index}`, |
||||||
|
'aria-controls': `full-width-tabpanel-${index}`, |
||||||
|
}; |
||||||
|
}; |
||||||
|
|
||||||
|
return ( |
||||||
|
<Box |
||||||
|
sx={{ |
||||||
|
minWidth: 250, |
||||||
|
display: 'flex', |
||||||
|
marginTop: 5, |
||||||
|
}} |
||||||
|
> |
||||||
|
<Box |
||||||
|
sx={{ |
||||||
|
flexGrow: 1, |
||||||
|
border: 1, |
||||||
|
}} |
||||||
|
> |
||||||
|
<AppBar position="static"> |
||||||
|
<Tabs |
||||||
|
value={tabindex} |
||||||
|
onChange={handleChange} |
||||||
|
indicatorColor="secondary" |
||||||
|
textColor="inherit" |
||||||
|
variant="fullWidth" |
||||||
|
aria-label="auth-tabs" |
||||||
|
> |
||||||
|
<Tab label="login" {...a11yProps(0)} /> |
||||||
|
<Tab label="register" {...a11yProps(1)} /> |
||||||
|
</Tabs> |
||||||
|
</AppBar> |
||||||
|
<TabPanel value={tabindex} index={0}> |
||||||
|
<AuthInputs tabIndex={0} setSession={setSession} /> |
||||||
|
</TabPanel> |
||||||
|
<TabPanel value={tabindex} index={1}> |
||||||
|
<AuthInputs tabIndex={1} /> |
||||||
|
</TabPanel> |
||||||
|
</Box> |
||||||
|
</Box> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
const AuthInputs = ({ tabIndex }) => { |
||||||
|
const initial = { email: '', password: '', confirmPassword: '' }; |
||||||
|
const [credentials, setCredentials] = useState(initial); |
||||||
|
const { showSnackBar } = useSnackbar(); |
||||||
|
const { signIn, signUp } = useAuth(); |
||||||
|
|
||||||
|
const handleValueChanged = ({ target: { name, value } }) => { |
||||||
|
setCredentials({ ...credentials, [name]: value }); |
||||||
|
}; |
||||||
|
|
||||||
|
const handleSubmit = async (e) => { |
||||||
|
e.preventDefault(); |
||||||
|
|
||||||
|
if (credentials.email === '' || credentials.password === '') |
||||||
|
return showSnackBar('error', 'missing inputs'); |
||||||
|
|
||||||
|
if (tabIndex === 1 && credentials.password !== credentials.confirmPassword) |
||||||
|
return showSnackBar('error', `passwords don't match`); |
||||||
|
|
||||||
|
const { user, error } = |
||||||
|
tabIndex === 0 |
||||||
|
? await signIn(credentials.email, credentials.password) |
||||||
|
: await signUp(credentials.email, credentials.password); |
||||||
|
|
||||||
|
if (error) return showSnackBar('error', error.message); |
||||||
|
|
||||||
|
if (user) { |
||||||
|
tabIndex === 0 |
||||||
|
? showSnackBar('success', `logged in!`) |
||||||
|
: showSnackBar( |
||||||
|
'info', |
||||||
|
`verification email has been sent to ${credentials.email} pls check this before signing in` |
||||||
|
); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
return ( |
||||||
|
<Stack |
||||||
|
key="auth-stack" |
||||||
|
id="auth-stack" |
||||||
|
spacing={2} |
||||||
|
direction="column" |
||||||
|
component="form" |
||||||
|
> |
||||||
|
<Typography component={'span'} variant={'body2'}> |
||||||
|
supachat 👋 |
||||||
|
</Typography> |
||||||
|
<TextField |
||||||
|
name="email" |
||||||
|
label="email" |
||||||
|
value={credentials.email} |
||||||
|
type="email" |
||||||
|
onChange={handleValueChanged} |
||||||
|
required |
||||||
|
autoComplete="email" |
||||||
|
/> |
||||||
|
<TextField |
||||||
|
name="password" |
||||||
|
label="password" |
||||||
|
value={credentials.password} |
||||||
|
type="password" |
||||||
|
onChange={handleValueChanged} |
||||||
|
required |
||||||
|
autoComplete="current-password" |
||||||
|
/> |
||||||
|
{tabIndex === 1 ? ( |
||||||
|
<TextField |
||||||
|
name="confirmPassword" |
||||||
|
label="confirm password" |
||||||
|
value={credentials.confirmPassword} |
||||||
|
type="password" |
||||||
|
onChange={handleValueChanged} |
||||||
|
required |
||||||
|
autoComplete="new-password" |
||||||
|
/> |
||||||
|
) : null} |
||||||
|
|
||||||
|
<LoadingButton |
||||||
|
fullWidth |
||||||
|
variant="outlined" |
||||||
|
loading={false} |
||||||
|
onClick={(e) => handleSubmit(e)} |
||||||
|
type="submit" |
||||||
|
> |
||||||
|
{tabIndex === 0 ? 'Login' : 'Register'} |
||||||
|
</LoadingButton> |
||||||
|
</Stack> |
||||||
|
); |
||||||
|
}; |
@ -0,0 +1,66 @@ |
|||||||
|
import { useEffect, useState, createContext, useContext } from 'react'; |
||||||
|
import { supabase } from '../supabaseClient'; |
||||||
|
|
||||||
|
const AuthContext = createContext(); |
||||||
|
|
||||||
|
const AuthProvider = ({ children }) => { |
||||||
|
const [user, setUser] = useState(); |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
const session = supabase.auth.session(); |
||||||
|
setUser(session?.user ?? null); |
||||||
|
|
||||||
|
const { data: listener } = supabase.auth.onAuthStateChange( |
||||||
|
async (event, session) => { |
||||||
|
setUser(session?.user ?? null); |
||||||
|
} |
||||||
|
); |
||||||
|
|
||||||
|
return () => { |
||||||
|
listener?.unsubscribe(); |
||||||
|
}; |
||||||
|
}, []); |
||||||
|
|
||||||
|
const signUp = async (email, password) => { |
||||||
|
const { user, session, error } = await supabase.auth.signUp({ |
||||||
|
email, |
||||||
|
password, |
||||||
|
}); |
||||||
|
|
||||||
|
if (error) return { error }; |
||||||
|
|
||||||
|
return { user, session }; |
||||||
|
}; |
||||||
|
|
||||||
|
const signIn = async (email, password) => { |
||||||
|
const { user, session, error } = await supabase.auth.signIn({ |
||||||
|
email, |
||||||
|
password, |
||||||
|
}); |
||||||
|
|
||||||
|
if (error) return { error }; |
||||||
|
|
||||||
|
return { user, session }; |
||||||
|
}; |
||||||
|
|
||||||
|
const signOut = async () => { |
||||||
|
const { error } = await supabase.auth.signOut(); |
||||||
|
|
||||||
|
if (error) return error.message; |
||||||
|
return { status: 'success' }; |
||||||
|
}; |
||||||
|
|
||||||
|
const value = { signUp, signIn, signOut, user }; |
||||||
|
|
||||||
|
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>; |
||||||
|
}; |
||||||
|
|
||||||
|
const useAuth = () => { |
||||||
|
const context = useContext(AuthContext); |
||||||
|
|
||||||
|
if (!context) throw new Error('useAuth must be used within AuthProvider'); |
||||||
|
|
||||||
|
return context; |
||||||
|
}; |
||||||
|
|
||||||
|
export { AuthProvider, useAuth }; |
@ -0,0 +1,53 @@ |
|||||||
|
import { useState, createContext, useContext } from 'react'; |
||||||
|
import { Snackbar, Alert } from '@mui/material'; |
||||||
|
|
||||||
|
const SnackbarContext = createContext(); |
||||||
|
|
||||||
|
const SnackbarProvider = ({ children }) => { |
||||||
|
const initialSnack = { show: false, severity: 'info', msg: '' }; |
||||||
|
const [snack, setSnack] = useState(initialSnack); |
||||||
|
|
||||||
|
const showSnackBar = (severity, msg) => { |
||||||
|
setSnack({ |
||||||
|
...snack, |
||||||
|
show: true, |
||||||
|
severity, |
||||||
|
msg, |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
const handleSnackClose = (event, reason) => { |
||||||
|
if (reason === 'clickaway') return; |
||||||
|
setSnack(initialSnack); |
||||||
|
}; |
||||||
|
|
||||||
|
return ( |
||||||
|
<SnackbarContext.Provider value={{ showSnackBar }}> |
||||||
|
<Snackbar |
||||||
|
open={snack.show} |
||||||
|
autoHideDuration={6000} |
||||||
|
onClose={handleSnackClose} |
||||||
|
> |
||||||
|
<Alert |
||||||
|
onClose={handleSnackClose} |
||||||
|
severity={snack.severity} |
||||||
|
sx={{ width: '100%' }} |
||||||
|
> |
||||||
|
{snack.msg} |
||||||
|
</Alert> |
||||||
|
</Snackbar> |
||||||
|
{children} |
||||||
|
</SnackbarContext.Provider> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
const useSnackbar = () => { |
||||||
|
const context = useContext(SnackbarContext); |
||||||
|
|
||||||
|
if (!context) |
||||||
|
throw new Error('useSnackbar must be used within an SnackBarProvider'); |
||||||
|
|
||||||
|
return context; |
||||||
|
}; |
||||||
|
|
||||||
|
export { SnackbarProvider, useSnackbar }; |
@ -1,13 +0,0 @@ |
|||||||
body { |
|
||||||
margin: 0; |
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', |
|
||||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', |
|
||||||
sans-serif; |
|
||||||
-webkit-font-smoothing: antialiased; |
|
||||||
-moz-osx-font-smoothing: grayscale; |
|
||||||
} |
|
||||||
|
|
||||||
code { |
|
||||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', |
|
||||||
monospace; |
|
||||||
} |
|
@ -1,13 +0,0 @@ |
|||||||
const reportWebVitals = onPerfEntry => { |
|
||||||
if (onPerfEntry && onPerfEntry instanceof Function) { |
|
||||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { |
|
||||||
getCLS(onPerfEntry); |
|
||||||
getFID(onPerfEntry); |
|
||||||
getFCP(onPerfEntry); |
|
||||||
getLCP(onPerfEntry); |
|
||||||
getTTFB(onPerfEntry); |
|
||||||
}); |
|
||||||
} |
|
||||||
}; |
|
||||||
|
|
||||||
export default reportWebVitals; |
|
@ -1,5 +0,0 @@ |
|||||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
|
||||||
// allows you to do things like:
|
|
||||||
// expect(element).toHaveTextContent(/react/i)
|
|
||||||
// learn more: https://github.com/testing-library/jest-dom
|
|
||||||
import '@testing-library/jest-dom'; |
|
@ -0,0 +1,6 @@ |
|||||||
|
import { createClient } from '@supabase/supabase-js'; |
||||||
|
|
||||||
|
const supabaseUrl = process.env.REACT_APP_SUPABASE_URL; |
||||||
|
const supabaseAnonKey = process.env.REACT_APP_SUPABASE_ANON_KEY; |
||||||
|
|
||||||
|
export const supabase = createClient(supabaseUrl, supabaseAnonKey); |
Loading…
Reference in new issue