diff options
31 files changed, 576 insertions, 202 deletions
diff --git a/apps/Notes/Notes.module.scss b/apps/Notes/Notes.module.scss index e2dbe06..a87adac 100644 --- a/apps/Notes/Notes.module.scss +++ b/apps/Notes/Notes.module.scss @@ -242,38 +242,3 @@ margin: .5em .5em 0 0; } } - -.loader, -.connection { - display: flex; - justify-content: center; - align-items: center; - height: 100%; - flex-direction: column; - - @keyframes rotating { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } - } - - svg { - font-size: 600%; - color: #ccc; - } - - p { - padding-top: 1em; - color: #ccc; - font-weight: 600; - } -} - -.loader { - svg { - animation: rotating 1s linear infinite; - } -} diff --git a/apps/Notes/components/List.js b/apps/Notes/components/List.js index 54f66b5..1fd03af 100644 --- a/apps/Notes/components/List.js +++ b/apps/Notes/components/List.js @@ -5,7 +5,7 @@ import useNotes from '../hooks/useNotes' import useSort from '../hooks/useSort' import ListItem from './ListItem' import Actions from './Actions' -import Splash from './Splash' +import {Splash} from 'components' const List = () => { const [fetchedNote, setFetchedNote] = useState() diff --git a/apps/Notes/components/ListItem.js b/apps/Notes/components/ListItem.js index 0e34ffc..4a404db 100644 --- a/apps/Notes/components/ListItem.js +++ b/apps/Notes/components/ListItem.js @@ -1,10 +1,10 @@ import styles from '../Notes.module.scss' -import React, { useContext } from 'react' +import React from 'react' import {getNote, exportNote, removeNote} from '../helpers/noteActions.js' import useNotes from '../hooks/useNotes' -import Context from 'context'; -import { faEdit, faDownload, faTrash } from '@fortawesome/free-solid-svg-icons' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import usePopup from 'hooks/usePopup' +import {faEdit, faDownload, faTrash } from '@fortawesome/free-solid-svg-icons' +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome' const datestring = date => { const d = new Date(date); @@ -13,8 +13,8 @@ const datestring = date => { }; const ListItem = ({note, setAction, setFetchedNote, setLoading}) => { + const {setPopup} = usePopup() const {mutateNotes} = useNotes() - const {setPopup} = useContext(Context) const handleNoteAction = async (a, note, e) => { if (e) e.stopPropagation() @@ -28,8 +28,7 @@ const ListItem = ({note, setAction, setFetchedNote, setLoading}) => { key={note._id} onClick={() => handleNoteAction('showNote', note)} > - <td - > + <td> <span>{`${note.title}`}</span> <span onClick={e => handleNoteAction('editNote', note, e)}> <FontAwesomeIcon icon={faEdit} /> diff --git a/apps/Notes/components/NoteEdit.js b/apps/Notes/components/NoteEdit.js index ad936a4..40a33c0 100644 --- a/apps/Notes/components/NoteEdit.js +++ b/apps/Notes/components/NoteEdit.js @@ -1,12 +1,12 @@ import styles from '../Notes.module.scss' -import React, {useContext} from 'react' -import Context from 'context'; +import React from 'react' import useNotes from '../hooks/useNotes' +import usePopup from 'hooks/usePopup' import {addNote, updateNote} from '../helpers/noteActions.js' const NoteEdit = ({setAction, fetchedNote}) => { const {mutateNotes} = useNotes() - const {setPopup} = useContext(Context) + const {setPopup} = usePopup() const handleSubmit = e => { e.preventDefault() diff --git a/apps/Notes/components/NoteView.js b/apps/Notes/components/NoteView.js index 29aba34..6926b09 100644 --- a/apps/Notes/components/NoteView.js +++ b/apps/Notes/components/NoteView.js @@ -1,14 +1,14 @@ import styles from '../Notes.module.scss' -import React, {useContext} from 'react'; -import Context from 'context'; +import React from 'react'; import useNotes from '../hooks/useNotes' +import usePopup from 'hooks/usePopup' import {removeNote, exportNote} from '../helpers/noteActions.js' import copyToClipboard from '../helpers/copyToClipboard.js' -import Splash from './Splash' +import Splash from 'components/Splash' const NoteView = ({fetchedNote, setFetchedNote, setAction}) => { + const {setPopup} = usePopup() const {mutateNotes} = useNotes() - const {setPopup} = useContext(Context) if (!fetchedNote) return <Splash /> if (fetchedNote.error) { diff --git a/apps/Notes/helpers/noteActions.js b/apps/Notes/helpers/noteActions.js index 7b51e20..4b47011 100644 --- a/apps/Notes/helpers/noteActions.js +++ b/apps/Notes/helpers/noteActions.js @@ -99,7 +99,7 @@ export const removeNote = (e, _id, mutateNotes, setPopup, setAction) => { setPopup({ content: 'Do you want to remove note?', yes: { label: 'Remove', action: remove }, - no: { label: 'Cancel', action: () => {} }, + no: { label: 'Cancel', action: async () => {} }, error: true, }) } diff --git a/apps/Settings/components/Settings.js b/apps/Settings/components/Settings.js new file mode 100644 index 0000000..2d7f238 --- /dev/null +++ b/apps/Settings/components/Settings.js @@ -0,0 +1,68 @@ +import styles from '../styles/Settings.module.scss' +import React, {useContext} from 'react' +import fetchJson from 'lib/fetchJson' +import useUser from 'lib/useUser' +import usePopup from 'hooks/usePopup' +import Context from 'context'; +import {close} from 'helpers/windowActions' + +const Settings = () => { + const {setPopup} = usePopup() + const {settings: {theme, language}, setSettings, setApps} = useContext(Context) + const {user} = useUser() + + const handleSave = e => { + e.preventDefault; + + try { + fetchJson('/api/settings', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({_id: user._id, theme, language}), + }) + setPopup({ + content: 'Settings saved', + time: 2000, + }) + close('Settings', setApps) + } catch (err) { + setpopup({ + content: 'could not save settings', + time: 2000, + error: true, + }) + } + } + + return ( + <div className={styles.settings}> + <div>Language:</div> + <div> + <span className={styles.settings__active}>English</span> + </div> + <div>Theme: </div> + <div> + <span + onClick={()=>{setSettings({language, theme:'green'})}} + className={theme === 'green' ? styles.settings__active : ''} + /> + <span + onClick={()=>{setSettings({language, theme:'blue'})}} + className={theme === 'blue' ? styles.settings__active : ''} + /> + <span + onClick={()=>{setSettings({language, theme:'black'})}} + className={theme === 'black' ? styles.settings__active : ''} + /> + </div> + <input + type="button" + className="window__button" + value="Save" + onClick={handleSave} + /> + </div> + ) +} + +export default Settings diff --git a/apps/Settings/index.js b/apps/Settings/index.js new file mode 100644 index 0000000..0a09ad4 --- /dev/null +++ b/apps/Settings/index.js @@ -0,0 +1,3 @@ +import Settings from './components/Settings' + +export default Settings diff --git a/apps/Settings/styles/Settings.module.scss b/apps/Settings/styles/Settings.module.scss new file mode 100644 index 0000000..9edef38 --- /dev/null +++ b/apps/Settings/styles/Settings.module.scss @@ -0,0 +1,51 @@ +.settings { + text-align: center; + padding: 1em; + + span { + padding: .5em; + } + + div { + text-align: left; + margin: 1.5em 0; + + &:nth-of-type(1) { + margin-top: .5em; + } + + &:nth-of-type(3) { + margin-top: 3em; + } + + &:nth-of-type(4) > span { + width: 2em; + height: 2em; + display: inline-block; + margin: 0 1em; + outline-offset: 4px; + + &:nth-of-type(1) { + background-color: #53e3a6; + } + + &:nth-of-type(2) { + background-color: #2986b3; + } + + &:nth-of-type(3) { + background-color: #000; + } + } + } + + input { + margin: 1em auto; + } + + &__active { + outline: 3px solid var(--color-window-buttons); + } +} + + diff --git a/apps/index.js b/apps/index.js index fa08e0e..08b454a 100644 --- a/apps/index.js +++ b/apps/index.js @@ -1 +1,2 @@ export {default as Notes} from './Notes' +export {default as Settings} from './Settings' diff --git a/components/App.js b/components/App.js index a68e593..59fd5a0 100644 --- a/components/App.js +++ b/components/App.js @@ -3,11 +3,11 @@ import {close, toggleMin, toggleMax, move} from 'helpers/windowActions' import {faArrowUp, faExpandAlt, faTimes, faCompressAlt} from '@fortawesome/free-solid-svg-icons' import {FontAwesomeIcon} from '@fortawesome/react-fontawesome' -const App = ({children, app, apps, setApps}) => { +const App = ({children, app, setApps}) => { const winRef = useRef(null); useEffect(() => { - move(app.name, winRef, apps, setApps) + move(app.name, winRef, setApps) }, []) return ( @@ -15,26 +15,40 @@ const App = ({children, app, apps, setApps}) => { <div ref={winRef} className={ - 'list window' + 'window' + (app.min ? ' hidden' : '') + (app.max ? ' maximized' : '') } - style={app.pos.length ? {top: app.pos[1], left: app.pos[0]} : {}} - tabIndex={0} + style={{ + maxHeight: app.height, + maxWidth: app.width, + ...app.pos.length + ? {top: app.pos[1], left: app.pos[0]} + : { + // top: `calc((( 100vh - ${app.height} ) / 2) + 2em)`, + // left: `calc(( 100vw - ${app.width} ) / 2)`, + } + }} > - <h2 className='window__title'>Notes</h2> + <h2 className='window__title'>{app.name}</h2> + <div className='window__content'>{children}</div> <div className='window__title-buttons'> - <span onClick={() => toggleMin('Notes', apps, setApps)}> - <FontAwesomeIcon icon={faArrowUp} /> - </span> - <span onClick={() => toggleMax('Notes', apps, setApps)}> - <FontAwesomeIcon icon={app.max ? faCompressAlt : faExpandAlt} /> - </span> - <span onClick={() => close('Notes', apps, setApps)}> - <FontAwesomeIcon icon={faTimes} /> - </span> + { app.buttons.includes('min') && ( + <span onClick={() => toggleMin(app.name, setApps)}> + <FontAwesomeIcon icon={faArrowUp} /> + </span> + )} + { app.buttons.includes('max') && ( + <span onClick={() => toggleMax(app.name, setApps)}> + <FontAwesomeIcon icon={app.max ? faCompressAlt : faExpandAlt} /> + </span> + )} + { app.buttons.includes('close') && ( + <span onClick={() => close(app.name, setApps)}> + <FontAwesomeIcon icon={faTimes} /> + </span> + )} </div> - <div className='window__content'>{children}</div> </div> </> ) diff --git a/components/Header.js b/components/Header.js index a208e70..5279c80 100644 --- a/components/Header.js +++ b/components/Header.js @@ -1,10 +1,12 @@ import styles from 'styles/Main.module.scss' import React, {useState} from 'react' +import {useRouter} from 'next/router' import Link from 'next/link' import useUser from 'lib/useUser' import fetchJson from 'lib/fetchJson' -import {toggleMin} from 'helpers/windowActions' -import {useRouter} from 'next/router' +import {focus, toggleMin} from 'helpers/windowActions' +import {open} from 'helpers/windowActions' +import appList from 'helpers/appList' const Header = ({apps, setApps}) => { const [userMenu, setUserMenu] = useState(false); @@ -20,18 +22,32 @@ const Header = ({apps, setApps}) => { router.push('/login') } + + const handleClick = (app, setApps) => { + if (app.min) { + toggleMin(app.name, setApps) + } + focus(app.name, setApps) + } + return ( <header className={styles.header}> <nav> <ul> { - apps && apps.map(app => ( + apps && [...apps].sort((a,b) => a.name > b.name).map(app => ( <li key={app.name} - style={app.min ? {fontWeight: 600} : {}} - onClick={() => toggleMin(app.name, apps, setApps)} + onClick={() => handleClick(app, setApps)} > - <span>{app.name}</span> + <span + style={{ + ...apps.findIndex(a => a.name === app.name) === apps.length - 1 ? {fontWeight: 600} : {}, + ...app.min ? {color: '#888'} : {} + }} + > + {app.name} + </span> </li> )) } @@ -64,7 +80,10 @@ const Header = ({apps, setApps}) => { <div className={styles.headerOverlay} onClick={() => setUserMenu(false)} /> <ul className={styles.submenu}> <li> - <span onClick={() => {}}> + <span onClick={() => { + open({appName: 'Settings', ...appList.Settings}, setApps) + setUserMenu() + }}> Settings </span> </li> diff --git a/components/Layout.js b/components/Layout.js index 0b8348c..e915285 100644 --- a/components/Layout.js +++ b/components/Layout.js @@ -1,30 +1,26 @@ import styles from 'styles/Main.module.scss' -import React, {useState} from 'react' +import React from 'react' import Head from 'next/head' -import Context from '../context'; -import Header from './Header' -import Popup from './Popup' +import {Header, Popup} from 'components' import PropTypes from 'prop-types' -const Layout = ({ children, apps, setApps}) => { - const [options, setOptions] = useState({theme: 'dark'}) - const [popup, setPopup] = useState({}) - - return ( - <Context.Provider value={{ setPopup }}> - <section className={styles.layout+' '+options.theme}> - <Head> - <title>My Apps</title> - </Head> - <main> - <div className="container">{children}</div> - </main> - <Header apps={apps} setApps={setApps} /> - <Popup popup={popup} /> - </section> - </Context.Provider> - ) -} +const Layout = ({ + children, + apps, + setApps, + settings, +}) => ( + <section className={styles.layout +' '+ (settings ? settings.theme : 'green')}> + <Head> + <title>My Apps</title> + </Head> + <main> + <div className="container">{children}</div> + </main> + <Header apps={apps} setApps={setApps} /> + <Popup/> + </section> +) export default Layout diff --git a/components/Popup.js b/components/Popup.js index 911d2fa..3df11c5 100644 --- a/components/Popup.js +++ b/components/Popup.js @@ -1,32 +1,24 @@ -import React, {useState, useEffect} from 'react' +import React from 'react' +import usePopup from 'hooks/usePopup' -const Popup = ({popup}) => { - const [visible, setVisible] = useState(false) - const { - content = null, - time = null, - error = null, - yes = null, - no = null, - } = popup +const Popup = () => { + const {popupData: p} = usePopup() - useEffect(() => { - setVisible(true) - time && setTimeout(() => setVisible(false), time) - }, [popup]) + if (!p || !p.content) return null - if (!content) return null - - return visible ? ( - <div className={`window window--popup${error ? ' window--error' : ''}`}> - <div className="window__content--popup">{ content }</div> + return ( + <div className={`window window--popup${p.error ? ' window--error' : ''}`}> + <div className="window__content--popup">{p.content}</div> { - (yes || no) && (<div className="window__buttons--popup"> - {[yes, no].map(a => a && <input key={a.label} className='window__button' type="button" onClick={() => { setVisible(false); a.action() }} value={a.label} />)} - </div>) + (p.yes || p.no) && ( + <div className="window__buttons--popup"> + {[p.no, p.yes].map(a => a && <input key={a.label} className='window__button' type="button" onClick={async () => a.action()} value={a.label} />)} + </div> + ) } </div> - ) : null + ) } export default Popup + diff --git a/apps/Notes/components/Splash.js b/components/Splash.js index b17fa6b..7976de4 100644 --- a/apps/Notes/components/Splash.js +++ b/components/Splash.js @@ -1,10 +1,10 @@ -import styles from '../Notes.module.scss' +import styles from 'styles/Main.module.scss' import React from 'react' import {FontAwesomeIcon} from '@fortawesome/react-fontawesome' import {faBan, faSpinner} from '@fortawesome/free-solid-svg-icons' -const Splash = ({type}) => ( - <div className={type === 'connection' ? styles.connection : styles.loader}> +const Splash = ({type, fixed = false}) => ( + <div className={`${type === 'connection' ? styles.connection : styles.loader} ${fixed ? styles.fixed : ''}`}> <FontAwesomeIcon icon={type === 'connection' ? faBan : faSpinner} /> <p>{type === 'connection' ? 'No connection' : 'Loading...'}</p> </div> diff --git a/components/index.js b/components/index.js index 4875559..19d8173 100644 --- a/components/index.js +++ b/components/index.js @@ -2,3 +2,5 @@ export {default as Form} from './Form' export {default as Header} from './Header' export {default as Layout} from './Layout' export {default as App} from './App' +export {default as Popup} from './Popup' +export {default as Splash} from './Splash' diff --git a/helpers/appList.js b/helpers/appList.js new file mode 100644 index 0000000..a114a70 --- /dev/null +++ b/helpers/appList.js @@ -0,0 +1,8 @@ +import {Notes, Settings} from 'apps' + +const appList = { + Notes: {component: Notes, icon: true, buttons: ['min', 'max', 'close'], height: '64em', width: '64em'}, + Settings: {component: Settings, icon: false, buttons: ['min'], height: '23em', width: '20em'}, +}; + +export default appList diff --git a/helpers/queue.js b/helpers/queue.js new file mode 100644 index 0000000..69e708a --- /dev/null +++ b/helpers/queue.js @@ -0,0 +1,53 @@ +class Queue { + static queue = []; + static pendingPromise = false; + static stop = false; + + static enqueue(promise) { + return new Promise((resolve, reject) => { + this.queue.push({ + promise, + resolve, + reject + }); + this.dequeue(); + }); + } + + static dequeue() { + if (this.workingOnPromise) { + return false; + } + if (this.stop) { + this.queue = []; + this.stop = false; + return; + } + const item = this.queue.shift(); + if (!item) { + return false; + } + try { + this.workingOnPromise = true; + item + .promise() + .then((value) => { + this.workingOnPromise = false; + item.resolve(value); + this.dequeue(); + }) + .catch((err) => { + this.workingOnPromise = false; + item.reject(err); + this.dequeue(); + }); + } catch (err) { + this.workingOnPromise = false; + item.reject(err); + this.dequeue(); + } + return true; + } +} + +export default Queue diff --git a/helpers/windowActions.js b/helpers/windowActions.js index 60e697c..8ff361b 100644 --- a/helpers/windowActions.js +++ b/helpers/windowActions.js @@ -1,33 +1,59 @@ -export const toggleMin = (appName, apps, setApps) => setApps([ - ...apps.map(a => a.name === appName ? {...a, min: !a.min} : a ) -]) +export const close = (appName, setApps) => {setApps(apps => apps.filter(a => a.name !== appName))} -export const toggleMax = (appName, apps, setApps) => setApps([ - ...apps.map(a => a.name === appName ? {...a, max: !a.max} : a ) -]) +export const open = ({appName, buttons, height, width}, setApps) => { + setApps(apps => ( + !apps.some(a => a.name === appName) + ? [...apps, {name: appName, min: false, max: false, height, width, pos: [], buttons}] + : apps + )) +} -export const close = (appName, apps, setApps) => { setApps(apps.filter(a => a.name !== appName)) } +export const focus = (appName, setApps) => { + setApps(apps => { + const i = apps.findIndex(a => a.name === appName) + return i !== apps.length - 1 + ? [...apps.filter((_,n) => n !== i), apps[i]] + : apps + }) +} + +export const unfocus = (appName, setApps) => { + setApps(apps => { + const i = apps.findIndex(a => a.name === appName) + return i !== 0 + ? [apps[i], ...apps.filter((_,n) => n !== i)] + : apps + }) +} -export const open = (appName, apps, setApps) => { - apps.some(app => app.name === appName) - ? toggleMin(appName, apps, setApps) - : setApps([...apps, {name: appName, min: false, max: false, pos: []}]) +export const toggleMin = (appName, setApps) => { + setApps(apps => ([ + ...apps.map(a => a.name === appName ? {...a, min: !a.min} : a ) + ])) + unfocus(appName, setApps) } -export const move = (appName, winRef, apps, setApps) => { +export const toggleMax = (appName, setApps) => setApps(apps => ([ + ...apps.map(a => a.name === appName ? {...a, max: !a.max} : a ) +])) + + +export const move = (appName, winRef, setApps) => { winRef.current.onmousedown = (event) => { if (event.target.classList && event.target.classList.contains('window__title')) { const shiftX = event.clientX - winRef.current.getBoundingClientRect().left const shiftY = event.clientY - winRef.current.getBoundingClientRect().top winRef.current.classList.add('moving') + focus(appName, setApps) function moveAt(pageX, pageY) { const x = pageX - shiftX const y = pageY - shiftY - 32 - setApps([...apps.map(a => a.name === appName + + setApps(apps => ([...apps.map(a => a.name === appName ? {...a, pos: [x + 'px', y < 0 ? 0 : y + 'px']} : a - )]) + )])) } const onMouseMove = (event) => { diff --git a/hooks/usePopup.js b/hooks/usePopup.js new file mode 100644 index 0000000..615d403 --- /dev/null +++ b/hooks/usePopup.js @@ -0,0 +1,39 @@ +import React, { createContext, useState, useContext } from 'react' +import Queue from 'helpers/queue' + +const PopupContext = createContext() + +export const PopupProvider = ({children}) => { + const [popupData, setPopupData] = useState() + + const setPopup = p => { + Queue.enqueue( + () => + new Promise(r => { + if (p.time) { + setPopupData(p) + setTimeout(() => { + r(setPopupData()); + }, p.time); + } else { + setPopupData({ + ...p, + ...(p.yes && {yes: {label: p.yes.label, action: () => p.yes.action().then(() => r(setPopupData()))}}), + ...(p.no && {no: {label: p.no.label, action: () => p.no.action().then(() => {r(setPopupData())})}}), + }) + } + }) + ); + + } + + return ( + <PopupContext.Provider value={{popupData, setPopup}}> + {children} + </PopupContext.Provider> + ) +} + +const usePopup = () => useContext(PopupContext) + +export default usePopup diff --git a/lib/dbConnect.js b/lib/dbConnect.js index 92f34bd..a58ef79 100644 --- a/lib/dbConnect.js +++ b/lib/dbConnect.js @@ -1,6 +1,7 @@ import mongoose from 'mongoose' -const MONGODB_URI = process.env.MYAPPS_MONGODB_URI +// const MONGODB_URI = process.env.MYAPPS_MONGODB_URI +const MONGODB_URI = 'mongodb://localhost:27017/myapps' if (!MONGODB_URI) { throw new Error( diff --git a/models/User.js b/models/User.js index fa3303f..44d9619 100644 --- a/models/User.js +++ b/models/User.js @@ -2,7 +2,14 @@ const mongoose = require("mongoose") const bcrypt = require('bcryptjs') const {nanoid} = require('nanoid') -const userResponse = u => ({_id: u._id, email: u.email, isVerified: u.isVerified, noteList: u.noteList}) +const userResponse = u => ({ + _id: u._id, + email: u.email, + isVerified: u.isVerified, + noteList: u.noteList, + theme: u.theme, + language: u.language +}) const userSchema = new mongoose.Schema({ email: { @@ -38,6 +45,8 @@ const userSchema = new mongoose.Schema({ ref: "NoteList", required: true, }, + theme: {type: String, default: () => 'green'}, + language: {type: String, default: () => 'en'}, created_at: {type: Date, default: Date.now}, updated_at: {type: Date, default: Date.now} }) @@ -78,6 +87,16 @@ userSchema.statics.verifyUser = async function (_id, key){ return userResponse(user) } +userSchema.statics.saveSettings = async function ({_id, theme, language}){ + const user = await User.findOneAndUpdate({_id}, {theme, language}, {new: true}) + + if (!user) { + throw new Error('Could not save settings') + } + + return userResponse(user) +} + userSchema.statics.state = async function (_id){ const user = await User.findOne({_id}) diff --git a/pages/_app.js b/pages/_app.js index 4625291..0fec9f6 100644 --- a/pages/_app.js +++ b/pages/_app.js @@ -1,4 +1,5 @@ import { SWRConfig } from 'swr' +import {PopupProvider} from 'hooks/usePopup' import fetchJson from 'lib/fetchJson' import '/styles/global.scss' @@ -12,7 +13,9 @@ function MyApp({Component, pageProps}) { }, }} > - <Component {...pageProps} /> + <PopupProvider> + <Component {...pageProps} /> + </PopupProvider> </SWRConfig> ) } diff --git a/pages/api/login.js b/pages/api/login.js index 30edda1..db39bda 100644 --- a/pages/api/login.js +++ b/pages/api/login.js @@ -10,10 +10,10 @@ export default withSession(async (req, res) => { switch (method) { case 'POST': try { - const {_id, email, isVerified, noteList} = await User.findByCredentials(req.body.email, req.body.password); + const {_id, email, isVerified, noteList, theme, language} = await User.findByCredentials(req.body.email, req.body.password); if (!email) { throw new Error('Something went wrong') } - const user = {_id, email, isVerified, isLoggedIn: true, noteList} + const user = {_id, email, isVerified, isLoggedIn: true, noteList, theme, language} req.session.set('user', user) await req.session.save() res.status(201).json(user) diff --git a/pages/api/register.js b/pages/api/register.js index f89e347..1407146 100644 --- a/pages/api/register.js +++ b/pages/api/register.js @@ -13,12 +13,12 @@ export default withSession(async (req, res) => { try { const noteList = await NoteList.create({}) - const {_id, email, verificationKey: key} = await User.create({...req.body, noteList}) + const {_id, email, verificationKey: key, theme, language} = await User.create({...req.body, noteList}) if (!email) { throw new Error('Something went wrong') } sendMail(req.body.email, subject, text(key), html(key)) - const user = {_id, email, noteList, isVerified: false, isLoggedIn: true} + const user = {_id, email, noteList, theme, language, isVerified: false, isLoggedIn: true} req.session.set('user', user) await req.session.save() res.status(201).json(user) diff --git a/pages/api/settings.js b/pages/api/settings.js new file mode 100644 index 0000000..e70868e --- /dev/null +++ b/pages/api/settings.js @@ -0,0 +1,27 @@ +import withSession from 'lib/withSession' +import User from 'models/User' +import dbConnect from 'lib/dbConnect' + +export default withSession(async (req, res) => { + await dbConnect() + + switch (req.method) { + case 'POST': + try { + if (req.body.theme && req.body.language) { + const user = await User.saveSettings(req.body) + req.session.set('user', user) + await req.session.save() + res.status(200).json(user) + } + } catch (error) { + console.log(error) + res.status(400).send() + } + break + default: + res.status(400).send() + break + } +}) + diff --git a/pages/index.js b/pages/index.js index 4b420fb..ee1c277 100644 --- a/pages/index.js +++ b/pages/index.js @@ -1,73 +1,82 @@ import styles from 'styles/Main.module.scss' -import React, {useState} from 'react' +import React, {useState, useEffect} from 'react' import useUser from 'lib/useUser' -import {Layout, App} from 'components' +import {Layout, App, Splash} from 'components' import {open} from 'helpers/windowActions' -import {Notes} from 'apps' - -const appList = { - Notes, -}; +import appList from 'helpers/appList' +import Context from '../context'; const Home = () => { + const [settings, setSettings] = useState() const [apps, setApps] = useState([]) - useUser({ + const {user} = useUser({ redirectToLogin: true, redirectToVerify: true, redirectToApps: true, }) - const handleClick = (e, appName) => { + useEffect(() => { + if (user) { + setSettings({theme: user.theme, language: user.language}) + } + }, [user]) + + if (!user) return ( + <Layout><Splash fixed /></Layout> + ) + + const handleClick = (e, appProps) => { switch (e.detail) { case 1: e.currentTarget.focus() break; case 2: - open(appName, apps, setApps) + open(appProps, setApps) e.currentTarget.blur() break; } }; - const handleKey = (e, appName) => { + const handleKey = (e, appProps) => { if (e.key === 'Enter') { - open(appName, apps, setApps) + open(appProps, setApps) } } return ( - <Layout apps={apps} setApps={setApps}> - <> - { - Object.keys(appList).map(appName => ( - <div - key={`${appName}_icon`} - className={styles.icon} - onClick={e => handleClick(e, appName)} - onKeyDown={e => handleKey(e, appName)} - tabIndex="0" - > - <img src={`./${appName.toLowerCase()}.svg`} alt={`${appName} Icon`} /> - <p>{appName}</p> - </div> - )) - } - { apps && apps.map(app => { - const AppName = appList[app.name] + <Context.Provider value={{settings, setSettings, setApps}}> + <Layout apps={apps} setApps={setApps} settings={settings}> + <> + { + Object.entries(appList).filter(a => a[1].icon).map(a => ( + <div + key={`${a[0]}_icon`} + className={styles.icon} + onClick={e => handleClick(e, {appName: a[0], ...a[1]})} + onKeyDown={e => handleKey(e, {appName: a[0], ...a[1]})} + tabIndex="0" + > + <img src={`./${a[0].toLowerCase()}.svg`} alt={`${a[0]} Icon`} /> + <p>{a[0]}</p> + </div> + )) + } + {apps && apps.map(app => { + const AppComponent = appList[app.name].component - return ( - <App - key={`${app.name}_app`} - app={app} - apps={apps} - setApps={setApps} - > - <AppName /> - </App> - ); - })} - </> - </Layout> + return ( + <App + key={`${app.name}_app`} + app={app} + setApps={setApps} + > + <AppComponent /> + </App> + ); + })} + </> + </Layout> + </Context.Provider> ) } diff --git a/pages/verify.js b/pages/verify.js index 7161d67..5991035 100644 --- a/pages/verify.js +++ b/pages/verify.js @@ -3,6 +3,7 @@ import {useState} from 'react' import useUser from 'lib/useUser' import fetchJson from 'lib/fetchJson' import {Layout} from 'components' +import Splash from 'components/Splash' const Verify = () => { const {user, mutateUser} = useUser({ @@ -48,7 +49,7 @@ const Verify = () => { {!user ? ( <div className="window window--popup"> - <p>Loading...</p> + <Splash /> </div> ) : ( <div className={`window window--popup ${styles.verify}`}> diff --git a/styles/Main.module.scss b/styles/Main.module.scss index 7c3f9bc..3f0230c 100644 --- a/styles/Main.module.scss +++ b/styles/Main.module.scss @@ -3,6 +3,7 @@ height: 100vh; background: var(--color-bg); background: linear-gradient(to bottom right, var(--color-bg) 0%, var(--color-bg-alt) 100%); + position: relative; } .icon { @@ -24,7 +25,7 @@ } &:focus p { - background-color: var(--color-glass); + background-color: var(--color-selected); } } @@ -41,12 +42,15 @@ input[type=email], input[type=password] { + color: var(--color-text-alt); + background: var(--color-window-content); padding: .5em; margin: .5em 0; border: 1px dashed var(--color-decor); border-radius: .5px; } - .error { + + p { text-align: center; color: var(--color-error); margin: 1rem 0 0; @@ -156,3 +160,46 @@ color: var(--color-error); } } + +.fixed { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; +} + +.loader, +.connection { + display: flex; + justify-content: center; + align-items: center; + height: 100%; + flex-direction: column; + + @keyframes rotating { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } + } + + svg { + font-size: 600%; + color: #ccc; + } + + p { + padding-top: 1em; + color: #ccc; + font-weight: 600; + } +} + +.loader { + svg { + animation: rotating 1s linear infinite; + } +} diff --git a/styles/_themes.scss b/styles/_themes.scss index fbf63cc..9319fa5 100644 --- a/styles/_themes.scss +++ b/styles/_themes.scss @@ -1,4 +1,4 @@ -.light { +.green { --color-bg: #50a3a2; --color-bg-alt: #53e3a6; --color-text: #333; @@ -13,6 +13,7 @@ --color-window-border-top: rgba(255,255,255,.7); --color-window-border-bottom: #ccc; --color-window-content: #fff; + --color-window-popup: #fff; --color-window-buttons: #666; --color-window-buttons-alt: #222; --color-window-menu: #eee; @@ -20,14 +21,42 @@ --color-button: #9cdbb5; --color-button-alt: #a4eac0; --color-button-border: #8ecca6; - --color-popup-bg: #fff; --color-popup-error-bg: #ffe1e1; --color-popup-error-button: #ffbebe; --color-popup-error-button-alt: #ffa1a1; --color-popup-error-border: #ffb6b6; } -.dark { +.blue { + --color-bg: #71b0d7; + --color-bg-alt: #a4e4fd; + --color-text: #333; + --color-text-alt: #111; + --color-decor: #ccc; + --color-glass: rgba(151, 192, 230, 0.9); + --color-glass-alt: rgba(255, 255, 255, 0.5); + --color-error: #a00; + --color-success: #0a0; + --color-link: #006dd0; + --color-selected: rgba(0,0,0,.1); + --color-window-border-top: rgba(255,255,255,.7); + --color-window-border-bottom: #ccc; + --color-window-content: #fff; + --color-window-popup: #fff; + --color-window-buttons: #666; + --color-window-buttons-alt: #222; + --color-window-menu: #eee; + --color-window-menu-alt: #ddd; + --color-button: #81d5ff; + --color-button-alt: #ace0ff; + --color-button-border: #67c1f9; + --color-popup-error-bg: #ffe1e1; + --color-popup-error-button: #ffbebe; + --color-popup-error-button-alt: #ffa1a1; + --color-popup-error-border: #ffb6b6; +} + +.black { --color-bg: #000; --color-bg-alt: #222; --color-text: #eee; @@ -38,10 +67,11 @@ --color-error: #a00; --color-success: #0a0; --color-link: #006dd0; - --color-selected: rgba(0,0,0, .5); + --color-selected: rgba(255,255,255,.2); --color-window-border-top: rgba(255,255,255,.3); --color-window-border-bottom: #444; --color-window-content: #222; + --color-window-popup: #333; --color-window-buttons: #ccc; --color-window-buttons-alt: #fff; --color-window-menu: #282828; @@ -49,7 +79,6 @@ --color-button: #555; --color-button-alt: #444; --color-button-border: #666; - --color-popup-bg: #666; --color-popup-error-bg: #6f0b0b; --color-popup-error-button: #934e4e; --color-popup-error-button-alt: #a21010; diff --git a/styles/_window.scss b/styles/_window.scss index 5ce6e71..a2d0438 100644 --- a/styles/_window.scss +++ b/styles/_window.scss @@ -1,10 +1,9 @@ .window { position: absolute; - border-bottom-left-radius: .5em; - border-bottom-right-radius: .5em; transition-property: opacity, visibility, transform, width, height, top, left; transition-duration: .3s; box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px; + border-radius: .5em; color: var(--color-text); &.moving { @@ -13,14 +12,17 @@ &--popup { padding: 1em; - background: var(--color-popup-bg); + background: var(--color-window-popup); top: 50%; left: 50%; transform: translate(-50%, -50%); max-width: 21rem; margin: 0 auto; - border-top-left-radius: .5em; - border-top-right-radius: .5em; + + &:not(.window--popup-with-title) { + border-top-left-radius: .5em; + border-top-right-radius: .5em; + } } &:not(.window--popup) { @@ -41,7 +43,6 @@ } } - &__title { position: absolute; background-color: var(--color-glass); @@ -72,7 +73,6 @@ top: .5em; right: .5em; // cursor: pointer; - z-index: 1; & > span { padding: .5em; @@ -97,6 +97,8 @@ bottom: 0; left: 0; padding: 1em; + border-bottom-left-radius: .5em; + border-bottom-right-radius: .5em; } &--with-submenu { |