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