aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--apps/Notes/Notes.module.scss35
-rw-r--r--apps/Notes/components/List.js2
-rw-r--r--apps/Notes/components/ListItem.js13
-rw-r--r--apps/Notes/components/NoteEdit.js6
-rw-r--r--apps/Notes/components/NoteView.js8
-rw-r--r--apps/Notes/helpers/noteActions.js2
-rw-r--r--apps/Settings/components/Settings.js68
-rw-r--r--apps/Settings/index.js3
-rw-r--r--apps/Settings/styles/Settings.module.scss51
-rw-r--r--apps/index.js1
-rw-r--r--components/App.js46
-rw-r--r--components/Header.js33
-rw-r--r--components/Layout.js42
-rw-r--r--components/Popup.js38
-rw-r--r--components/Splash.js (renamed from apps/Notes/components/Splash.js)6
-rw-r--r--components/index.js2
-rw-r--r--helpers/appList.js8
-rw-r--r--helpers/queue.js53
-rw-r--r--helpers/windowActions.js54
-rw-r--r--hooks/usePopup.js39
-rw-r--r--lib/dbConnect.js3
-rw-r--r--models/User.js21
-rw-r--r--pages/_app.js5
-rw-r--r--pages/api/login.js4
-rw-r--r--pages/api/register.js4
-rw-r--r--pages/api/settings.js27
-rw-r--r--pages/index.js95
-rw-r--r--pages/verify.js3
-rw-r--r--styles/Main.module.scss51
-rw-r--r--styles/_themes.scss39
-rw-r--r--styles/_window.scss16
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 {