Root.jsx
8079 Chars | 288 Lines | 8.08 kb | Text File
Parent Directory
import React, { useState, useEffect } from 'react'
import Container from 'react-bootstrap/Container'
import Nav from 'react-bootstrap/Nav'
import Navbar from 'react-bootstrap/Navbar'
import NavDropdown from 'react-bootstrap/NavDropdown'
import Button from 'react-bootstrap/Button'
import Form from 'react-bootstrap/Form'
import Modal from 'react-bootstrap/Modal'
// Auth
import {
auth,
authenticateWithGoogle,
fetchLocalStorage,
getUser,
signOutOfGoogle,
getUsername,
initUser,
useAuth
} from '@/lib/firebase'
import { ThemeContext } from '@/lib/context'
import { Outlet } from 'react-router-dom'
// import { useNavigate } from 'react-router-dom'
import { LinkContainer } from 'react-router-bootstrap'
import './Root.scss'
import { useUser } from '@/stores/user'
import { useShallow } from 'zustand/react/shallow'
import { debounce } from './lib/utilities'
import Theme from './Theme'
import { Themes } from './Themes'
function Root() {
const [user, loading] = useAuth(auth)
const [setUser] = useUser(useShallow((state) => [state.setUser]))
// const navigate = useNavigate()
// const [search, setSearch] = useState('')
const [render, setRender] = useState(false)
const [askForUsername, setAskForUsername] = useState(false)
const [username, setUsername] = useState('')
const [usernameAvailable, setUsernameAvailable] = useState('Great Username!')
const [theme, setTheme] = useState(localStorage.getItem('theme') || 0)
const [showAbout, setShowAbout] = useState(false)
useEffect(() => {
if (user?.displayName == undefined) {
setUser({})
} else {
setUser(user)
fetchLocalStorage(user?.uid)
}
}, [user, loading])
// Validate the new username
useEffect(() => {
async function checkAvailability() {
if ((await getUsername(username)) !== undefined) {
setUsernameAvailable('❌ Username has been taken')
}
}
function checkUsername() {
// Between 3 and 20 characters
let correctLength = username.length >= 3 && username.length <= 20
// // ASCII
// let correctCharacters = ![...username].some((char) => char.charCodeAt(0) > 127)
// Overlaps with ASCII but checks for no spaces, only alphanumeric
let alphaNumeric = /^[\w-]+$/.test(username)
if (!correctLength || !alphaNumeric) {
setUsernameAvailable(
'Username must be between 3 and 20 characters, not include spaces, and contain no special characters'
)
}
}
if (username == '') {
setUsernameAvailable('Please enter a username')
} else {
setUsernameAvailable('Great Username!')
checkUsername()
debounce(checkAvailability())
}
}, [username])
// // Lazy-load other themes
// // We need to use a glob since we can't use import.meta.url for a dynamic import
// useEffect(() => {
// Themes.forEach((theme) => async () => {
// if (theme.id !== 'light') await import(`./themes/${theme.id}.theme.scss`)
// })
// }, [])
// Save the theme for future visits
// useEffect(() => {
// if (Themes[theme] !== undefined) {
// localStorage.setItem('theme', theme)
// // if (confirm('Would you like to reload the page to apply the theme?'))
// // window.location.reload()
// }
// }, [theme])
// Apply the theme
// Only run this on initialization, these themes interfere with each-other
useEffect(() => {
async function applyTheme() {
document.documentElement.setAttribute('data-bs-theme', Themes[theme].color)
await import(`./themes/${Themes[theme].id}.scss`)
setRender(true)
}
if (Themes[theme] !== undefined) applyTheme()
}, [])
async function handleAuthClick() {
if (user) {
// Log-Out functionality
signOutOfGoogle()
} else {
// Log-In
let user = await initUser()
if ((await getUser(user.uid)) === undefined) {
// initUser returns the displayName as the default username
setUsername(await user.displayName)
// Open the modal
setAskForUsername(true)
}
}
}
async function confirmUsername() {
setUsernameAvailable('Saving to database...')
await authenticateWithGoogle(username)
setAskForUsername(false)
window.location.reload()
}
// function openSearchPage() {
// navigate(`/search/${encodeURIComponent(search)}`)
// }
if (!render) return
return (
<>
<Navbar
id="navbar"
data-bs-theme={Themes[theme]?.color}
expand="lg"
className="shadow-sm rounded-3 position-absolute fixed-top"
>
<Container fluid>
<LinkContainer to={'/'}>
<Navbar.Brand>📎 Pecto</Navbar.Brand>
</LinkContainer>
<Navbar.Toggle aria-controls="basic-navbar-nav" />
<Navbar.Collapse id="basic-navbar-nav">
<Nav
className="me-auto my-2 my-lg-0"
style={{ maxHeight: '100px' }}
navbarScroll
>
<LinkContainer to={'/'}>
<Nav.Link>Home</Nav.Link>
</LinkContainer>
<NavDropdown title="New">
<LinkContainer to={`/new/pack/`}>
<NavDropdown.Item>Pack</NavDropdown.Item>
</LinkContainer>
</NavDropdown>
<Nav.Link onClick={() => setShowAbout(true)}>About</Nav.Link>
</Nav>
<div className="d-flex justify-content-end align-items-center">
<Theme theme={theme} setTheme={setTheme} />
<Button
variant={user ? 'danger' : 'success'}
onClick={handleAuthClick}
className="text-nowrap"
>
{user ? 'Sign Out' : 'Sign in with Google'}
</Button>
</div>
</Navbar.Collapse>
</Container>
</Navbar>
{Themes[theme] && (
<ThemeContext.Provider value={Themes[theme]}>
<div id="detail">
<Outlet />
</div>
</ThemeContext.Provider>
)}
{/* Credits */}
<Modal show={showAbout} onHide={() => setShowAbout(false)}>
<Modal.Header closeButton>
<Modal.Title>About</Modal.Title>
</Modal.Header>
<Modal.Body>
<h4>Modes</h4>
<p>
Once you've created a pack or imported a quizlet, you can summarize,
improve, and learn the material.
</p>
<h5>Blitz</h5>
<p>
Write the correct definition in a fast-paced gamemode and get the last ten
questions correct to move on.
</p>
<h5>Master</h5>
<p>
Use Pecto's premier spaced-repetition algorithm to generate a study
plan. Master guarantees that you retain the information if you follow the
schedule.
</p>
<h5>Comprehend</h5>
<p>
Produce summaries using Artificial Intelligence and Natural Language
Processing.
</p>
<h5>Assess</h5>
<p>
Generate a personalized printable test to assess your knowledge. Choose from
Multiple Choice, Written, True or False, and Extended Multiple Choice
questions.
</p>
<h5>Extrapolate</h5>
<p>Enter text to automatically generate cards through AI.</p>
<h4>Features</h4>
<p />
<h5>Google Sign-In</h5>
<p>Sign in with Google to synchronize your packs</p>
<h5>Themes</h5>
<p>
Use the Dark/Darker theme for pleasant viewing and Neopolitan for a playful
colors.
</p>
</Modal.Body>
</Modal>
{/* Username sign-in modal */}
<Modal show={askForUsername}>
<Modal.Header>
<Modal.Title>Create a Username</Modal.Title>
</Modal.Header>
<Modal.Body>
<Form.Control
value={username}
onChange={(e) => {
setUsername(e.target.value)
}}
type="input"
placeholder="Enter a unique username 3-15 characters long"
aria-label="Enter a unique username 3-15 characters long"
/>
<Form.Text>{usernameAvailable}</Form.Text>
</Modal.Body>
<Modal.Footer>
<Button
variant="primary"
disabled={!usernameAvailable}
onClick={confirmUsername}
>
Confirm
</Button>
</Modal.Footer>
</Modal>
</>
)
}
export default Root