diff options
49 files changed, 885 insertions, 291 deletions
diff --git a/apps/Notes/components/Export.js b/apps/Notes/components/Export.js index e7966e4..0838cea 100644 --- a/apps/Notes/components/Export.js +++ b/apps/Notes/components/Export.js @@ -1,10 +1,12 @@ import styles from '../Notes.module.scss' import React, {useState} from 'react' +import useSettings from 'hooks/useSettings' import useNotes from '../hooks/useNotes' import {handleSelect, handleSelectAll, handleExport} from '../helpers/export' const Export = ({setAction}) => { const {notes} = useNotes() + const {t} = useSettings() const [ids, setIds] = useState(notes.map(n => n.noteId)) const sortFn = (a, b) => new Date(b.updated_at) - new Date(a.updated_at) @@ -13,17 +15,17 @@ const Export = ({setAction}) => { return ( <section> <div className='window__submenu'> - <div onClick={() => {setAction('')}}>Back</div> + <div onClick={() => {setAction('')}}>{t('back')}</div> </div> <div className={`window__scroll ${styles.export}`}> - <h3>Click to export your notes:</h3> + <h3>{t('notes_click_to_export')}</h3> <input className="window__button" type="submit" - value="Export" + value={t('export')} onClick={e => handleExport(e, ids, notes)} /> - <p>Notes to export:</p> + <p>{t('notes_to_export')}</p> <div className={styles.export__select}> <input type="checkbox" @@ -31,7 +33,7 @@ const Export = ({setAction}) => { onChange={e => handleSelectAll(e, notes, setIds)} checked={notes.length === ids.length} /> - <label htmlFor='selectAll'>Select all</label> + <label htmlFor='selectAll'>{t('select_all')}</label> </div> <ul> {notes.sort(sortFn).map(note => ( diff --git a/apps/Notes/components/Import.js b/apps/Notes/components/Import.js index 700acb4..ad2b831 100644 --- a/apps/Notes/components/Import.js +++ b/apps/Notes/components/Import.js @@ -1,10 +1,11 @@ import styles from '../Notes.module.scss' import React, {useState} from 'react' -import fetchJson from 'lib/fetchJson' +import useSettings from 'hooks/useSettings' import useNotes from '../hooks/useNotes' import {state, color, handleImport, handleChange} from '../helpers/import' const Import = ({setAction}) => { + const {t} = useSettings() const [files, setFiles] = useState() const [done, setDone] = useState([]) const {mutateNotes} = useNotes() @@ -12,14 +13,14 @@ const Import = ({setAction}) => { return ( <section className={styles.import}> <div className='window__submenu'> - <div onClick={() => {setAction('')}}>Back</div> + <div onClick={() => {setAction('')}}>{t('back')}</div> </div> <div className='window__scroll'> <form onSubmit={e => handleImport(e, files, mutateNotes, setDone)}> - Import new notes: + {t('notes_import')} <div> <label className="window__button"> - Choose files + {t('choose_files')} <input name="import" type="file" @@ -31,14 +32,14 @@ const Import = ({setAction}) => { </div> {files && ( <> - <p>Notes to import:</p> + <p>{t('notes_to_import')}</p> <ul> {[...files].map((f, i) => <li style={color(done[i])} key={f.name}>{f.name} {state(done[i])}</li>)} </ul> { done.length === 0 - ? <input type="submit" value="Import" className="window__button" /> - : <p>Import finished.<br/><br/>Go back to notes list or choose other notes to import.</p> + ? <input type="submit" value={t('import')} className="window__button" /> + : <p>{t('import_finished')}<br/><br/>{t('notes_import_go_back')}</p> } </> )} diff --git a/apps/Notes/components/List.js b/apps/Notes/components/List.js index 1fd03af..9655d74 100644 --- a/apps/Notes/components/List.js +++ b/apps/Notes/components/List.js @@ -1,6 +1,7 @@ import styles from '../Notes.module.scss' import React, {useState, useEffect} from 'react' -import useUser from 'lib/useUser' +import useUser from 'hooks/useUser' +import useSettings from 'hooks/useSettings' import useNotes from '../hooks/useNotes' import useSort from '../hooks/useSort' import ListItem from './ListItem' @@ -13,6 +14,7 @@ const List = () => { const [loading, setLoading] = useState(false) const {notes, error} = useNotes() const [sortedBy, sortBy, sortFn] = useSort(3) + const {t} = useSettings() const {user} = useUser({ redirectToLogin: true, redirectToVerify: true, @@ -32,16 +34,16 @@ const List = () => { action === '' ? ( <> <div className='window__submenu'> - <div onClick={() => setAction('addNote')}>New note</div> - <div onClick={() => setAction('importNotes')}>Import</div> - <div onClick={() => setAction('exportNotes')}>Export</div> + <div onClick={() => setAction('addNote')}>{t('notes_new')}</div> + <div onClick={() => setAction('importNotes')}>{t('import')}</div> + <div onClick={() => setAction('exportNotes')}>{t('export')}</div> </div> <table className={styles.notesList}> <thead> <tr> - <th onClick={() => sortBy(1)}>Title {sortedBy(1)}</th> - <th onClick={() => sortBy(2)}>Created {sortedBy(2)}</th> - <th onClick={() => sortBy(3)}>Modified {sortedBy(3)}</th> + <th onClick={() => sortBy(1)}>{t('title')} {sortedBy(1)}</th> + <th onClick={() => sortBy(2)}>{t('created')} {sortedBy(2)}</th> + <th onClick={() => sortBy(3)}>{t('modified')} {sortedBy(3)}</th> </tr> </thead> <tbody> @@ -57,7 +59,7 @@ const List = () => { /> ))) : ( <tr> - <td>Your notes list is empty.</td> + <td>{t('notes_list_empty')}</td> </tr> )} </tbody> diff --git a/apps/Notes/components/ListItem.js b/apps/Notes/components/ListItem.js index 4a404db..4a2d6f6 100644 --- a/apps/Notes/components/ListItem.js +++ b/apps/Notes/components/ListItem.js @@ -1,8 +1,9 @@ import styles from '../Notes.module.scss' import React from 'react' import {getNote, exportNote, removeNote} from '../helpers/noteActions.js' -import useNotes from '../hooks/useNotes' +import useSettings from 'hooks/useSettings' import usePopup from 'hooks/usePopup' +import useNotes from '../hooks/useNotes' import {faEdit, faDownload, faTrash } from '@fortawesome/free-solid-svg-icons' import {FontAwesomeIcon} from '@fortawesome/react-fontawesome' @@ -13,13 +14,14 @@ const datestring = date => { }; const ListItem = ({note, setAction, setFetchedNote, setLoading}) => { + const {t} = useSettings() const {setPopup} = usePopup() const {mutateNotes} = useNotes() const handleNoteAction = async (a, note, e) => { if (e) e.stopPropagation() setLoading(true) - await getNote(note, setFetchedNote, setPopup, () => setAction(a)) + await getNote(note, setFetchedNote, t, setPopup, () => setAction(a)) } return ( @@ -36,7 +38,7 @@ const ListItem = ({note, setAction, setFetchedNote, setLoading}) => { <span onClick={e => {e.stopPropagation(); exportNote(note)}}> <FontAwesomeIcon icon={faDownload} /> </span> - <span onClick={e => removeNote(e, note._id, mutateNotes, setPopup, setAction)}> + <span onClick={e => removeNote(e, note._id, mutateNotes, t, setPopup, setAction)}> <FontAwesomeIcon icon={faTrash} /> </span> </td> diff --git a/apps/Notes/components/NoteEdit.js b/apps/Notes/components/NoteEdit.js index 40a33c0..f9cc3f2 100644 --- a/apps/Notes/components/NoteEdit.js +++ b/apps/Notes/components/NoteEdit.js @@ -1,24 +1,26 @@ import styles from '../Notes.module.scss' import React from 'react' -import useNotes from '../hooks/useNotes' import usePopup from 'hooks/usePopup' +import useSettings from 'hooks/useSettings' +import useNotes from '../hooks/useNotes' import {addNote, updateNote} from '../helpers/noteActions.js' const NoteEdit = ({setAction, fetchedNote}) => { - const {mutateNotes} = useNotes() + const {t} = useSettings() const {setPopup} = usePopup() + const {mutateNotes} = useNotes() const handleSubmit = e => { e.preventDefault() fetchedNote - ? updateNote(e, fetchedNote, mutateNotes, setAction, setPopup) - : addNote(e, mutateNotes, setAction, setPopup) + ? updateNote(e, fetchedNote, mutateNotes, setAction, t, setPopup) + : addNote(e, mutateNotes, setAction, t, setPopup) } return ( <div className={styles.noteEdit}> - <h2>{fetchedNote ? 'Edit note:' : 'Add new note:'}</h2> + <h2>{fetchedNote ? t('notes_edit') : t('notes_add_new')}</h2> <form onSubmit={handleSubmit}> <input name='title' @@ -38,12 +40,12 @@ const NoteEdit = ({setAction, fetchedNote}) => { className='window__button' onClick={() => {setAction(fetchedNote ? 'showNote' : '')}} > - Cancel + {t('cancel')} </span> <input className='window__button' type="submit" - value={fetchedNote ? 'Save note' : 'Add note'} + value={fetchedNote ? t('notes_save') : t('notes_add')} /> </div> </form> diff --git a/apps/Notes/components/NoteView.js b/apps/Notes/components/NoteView.js index 6926b09..02937f8 100644 --- a/apps/Notes/components/NoteView.js +++ b/apps/Notes/components/NoteView.js @@ -1,12 +1,14 @@ import styles from '../Notes.module.scss' import React from 'react'; -import useNotes from '../hooks/useNotes' +import useSettings from 'hooks/useSettings' import usePopup from 'hooks/usePopup' +import useNotes from '../hooks/useNotes' import {removeNote, exportNote} from '../helpers/noteActions.js' import copyToClipboard from '../helpers/copyToClipboard.js' import Splash from 'components/Splash' const NoteView = ({fetchedNote, setFetchedNote, setAction}) => { + const {t} = useSettings() const {setPopup} = usePopup() const {mutateNotes} = useNotes() @@ -21,19 +23,17 @@ const NoteView = ({fetchedNote, setFetchedNote, setAction}) => { return ( <section className={styles.noteView}> <div className='window__submenu'> - <div onClick={() => {setFetchedNote(); setAction('')}}>Back</div> - <div onClick={() => copyToClipboard(content, setPopup)}>Copy</div> - <div onClick={() => {setAction('editNote')}}>Edit</div> - <div onClick={() => exportNote(fetchedNote)}>Export</div> - <div onClick={e => {removeNote(e, _id, mutateNotes, setPopup, setAction)}}>Remove</div> + <div onClick={() => {setFetchedNote(); setAction('')}}>{t('back')}</div> + <div onClick={() => copyToClipboard(content, t, setPopup)}>{t('copy')}</div> + <div onClick={() => {setAction('editNote')}}>{t('edit')}</div> + <div onClick={() => exportNote(fetchedNote)}>{t('export')}</div> + <div onClick={e => {removeNote(e, _id, mutateNotes, t, setPopup, setAction)}}>{t('remove')}</div> </div> <div className='window__scroll'> <h2>{title}</h2> <p>{content}</p> </div> - <style jsx>{` - `}</style> </section> ) } diff --git a/apps/Notes/helpers/copyToClipboard.js b/apps/Notes/helpers/copyToClipboard.js index 608d299..afaf7f6 100644 --- a/apps/Notes/helpers/copyToClipboard.js +++ b/apps/Notes/helpers/copyToClipboard.js @@ -1,15 +1,15 @@ -const copyToClipboard = (text, setPopup) => { +const copyToClipboard = (text, t, setPopup) => { navigator .clipboard.writeText(text) .then(() => { setPopup({ - content: 'Note content was copied to clipboard', + content: t('notes_copy_success'), time: 2000, }) }) .catch(() => { setPopup({ - content: 'Could not copy to clipboard', + content: t('notes_copy_error'), time: 2000, error: true }) diff --git a/apps/Notes/helpers/export.js b/apps/Notes/helpers/export.js index 011dc18..80fa100 100644 --- a/apps/Notes/helpers/export.js +++ b/apps/Notes/helpers/export.js @@ -1,4 +1,4 @@ -import fetchJson from 'lib/fetchJson' +import fetchJson from 'helpers/fetchJson' import JSZip from 'jszip' import saveFile from 'helpers/saveFile' import filename from '../helpers/fileName' diff --git a/apps/Notes/helpers/import.js b/apps/Notes/helpers/import.js index b05a458..c0c3714 100644 --- a/apps/Notes/helpers/import.js +++ b/apps/Notes/helpers/import.js @@ -1,4 +1,4 @@ -import fetchJson from 'lib/fetchJson' +import fetchJson from 'helpers/fetchJson' import {faCheck, faTimes} from '@fortawesome/free-solid-svg-icons' import {FontAwesomeIcon} from '@fortawesome/react-fontawesome' diff --git a/apps/Notes/helpers/noteActions.js b/apps/Notes/helpers/noteActions.js index 4b47011..f67e7a4 100644 --- a/apps/Notes/helpers/noteActions.js +++ b/apps/Notes/helpers/noteActions.js @@ -1,8 +1,8 @@ -import fetchJson from 'lib/fetchJson' -import filename from '../helpers/fileName' +import fetchJson from 'helpers/fetchJson' import saveFile from 'helpers/saveFile' +import filename from '../helpers/fileName' -export const getNote = async (note, setFetchedNote, setPopup, callback) => { +export const getNote = async (note, setFetchedNote, t, setPopup, callback) => { try { const {content} = await fetchJson(`/api/note/${note.noteId}`) setFetchedNote({ ...note, content}) @@ -10,14 +10,14 @@ export const getNote = async (note, setFetchedNote, setPopup, callback) => { } catch (err) { setFetchedNote() setPopup({ - content: 'Could not open note', + content: t('notes_open_error'), time: 2000, error: true, }) } } -export const addNote = async (e, mutateNotes, setAction, setPopup) => { +export const addNote = async (e, mutateNotes, setAction, t, setPopup) => { const content = e.currentTarget.content.value const title = e.currentTarget.title.value @@ -30,20 +30,20 @@ export const addNote = async (e, mutateNotes, setAction, setPopup) => { }) ) setPopup({ - content: 'New note added', + content: t('notes_added'), time: 2000, }) setAction('') } catch (e) { setPopup({ - content: 'Could not save note', + content: t(notes_added_error), time: 2000, error: true, }) } } -export const updateNote = async (e, note, mutateNotes, setAction, setPopup) => { +export const updateNote = async (e, note, mutateNotes, setAction, t, setPopup) => { const content = e.currentTarget.content.value const title = e.currentTarget.title.value const {_id, noteId} = note @@ -57,20 +57,20 @@ export const updateNote = async (e, note, mutateNotes, setAction, setPopup) => { }) ) setPopup({ - content: 'Note updated', + content: t('notes_updated'), time: 2000, }) setAction('') } catch (e) { setPopup({ - content: 'Could not update note', + content: t('notes_updated_error'), time: 2000, error: true, }) } } -export const removeNote = (e, _id, mutateNotes, setPopup, setAction) => { +export const removeNote = (e, _id, mutateNotes, t, setPopup, setAction) => { e.stopPropagation() const remove = async () => { @@ -83,13 +83,13 @@ export const removeNote = (e, _id, mutateNotes, setPopup, setAction) => { }) ) setPopup({ - content: 'Note was removed', + content: t('notes_removed'), time: 2000, }) setAction('') } catch (err) { setPopup({ - content: 'Could not remove note', + content: t('notes_removed_error'), time: 2000, error: true, }) @@ -97,9 +97,9 @@ 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: async () => {} }, + content: t('notes_remove_confirm'), + yes: { label: t('remove'), action: remove }, + no: { label: t('cancel'), action: async () => {} }, error: true, }) } diff --git a/apps/Settings/api.js b/apps/Settings/api.js new file mode 100644 index 0000000..559486f --- /dev/null +++ b/apps/Settings/api.js @@ -0,0 +1,10 @@ +import fetchJson from 'helpers/fetchJson' + +export const saveSettings = async (data) => { + fetchJson('/api/settings', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }) +} + diff --git a/apps/Settings/components/Settings.js b/apps/Settings/components/Settings.js index 2d7f238..c09a1bd 100644 --- a/apps/Settings/components/Settings.js +++ b/apps/Settings/components/Settings.js @@ -1,64 +1,64 @@ 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 React from 'react' import {close} from 'helpers/windowActions' +import useUser from 'hooks/useUser' +import usePopup from 'hooks/usePopup' +import useSettings from 'hooks/useSettings' +import useApps from 'hooks/useApps' +import {saveSettings} from '../api' const Settings = () => { const {setPopup} = usePopup() - const {settings: {theme, language}, setSettings, setApps} = useContext(Context) + const {setApps} = useApps() + const {settings: {theme, language}, setSettings, t} = useSettings() 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}), - }) + saveSettings({_id: user._id, theme, language}) + .then(() => { setPopup({ - content: 'Settings saved', + content: t('settings_saved'), time: 2000, }) close('Settings', setApps) - } catch (err) { + }) + .catch(() => { setpopup({ - content: 'could not save settings', + content: t('settings_save_error'), time: 2000, error: true, }) - } + }) } return ( <div className={styles.settings}> - <div>Language:</div> + <div>{t('language')}</div> <div> - <span className={styles.settings__active}>English</span> + {['en', 'pl', 'es', 'de'].map(l => ( + <span + key={l} + className={language === l ? styles.settings__langactive : ''} + onClick={()=>{setSettings(prev => ({...prev, language: l}))}} + >{l.toUpperCase()}</span> + ))} </div> - <div>Theme: </div> + <div>{t('color_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 : ''} - /> + {['green', 'blue', 'black'].map(c => ( + <span + key={c} + onClick={()=>{setSettings(prev => ({...prev, theme: c}))}} + className={theme === c ? styles.settings__active : ''} + /> + ))} </div> <input type="button" className="window__button" - value="Save" + value={t('save')} onClick={handleSave} /> </div> diff --git a/apps/Settings/styles/Settings.module.scss b/apps/Settings/styles/Settings.module.scss index 9edef38..0944dcc 100644 --- a/apps/Settings/styles/Settings.module.scss +++ b/apps/Settings/styles/Settings.module.scss @@ -14,6 +14,24 @@ margin-top: .5em; } + &:nth-of-type(2) { + span { + padding: 0 .75em; + transition: .3s color; + display: inline-block; + color: var(--color-decor); + + &:hover { + color: #666; + } + } + + .settings__langactive { + color: var(--color-text); + font-weight: 600; + } + } + &:nth-of-type(3) { margin-top: 3em; } diff --git a/components/App.js b/components/App.js index 59fd5a0..210e0e0 100644 --- a/components/App.js +++ b/components/App.js @@ -1,10 +1,14 @@ import React, {useEffect, useRef} from 'react' +import useSettings from 'hooks/useSettings' +import useMediaQuery from 'hooks/useMediaQuery' 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, setApps}) => { + const {t} = useSettings() const winRef = useRef(null); + const forceMax = useMediaQuery(`(max-width: ${app.width}) or (max-height: ${app.height})`); useEffect(() => { move(app.name, winRef, setApps) @@ -17,20 +21,20 @@ const App = ({children, app, setApps}) => { className={ 'window' + (app.min ? ' hidden' : '') - + (app.max ? ' maximized' : '') + + (app.max || forceMax ? ' maximized' : '') } style={{ - maxHeight: app.height, - maxWidth: app.width, + height: app.height, + width: 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)`, + top: `calc((( 100vh - ${app.height} ) / 2) + 2em)`, + left: `calc(( 100vw - ${app.width} ) / 2)`, } }} > - <h2 className='window__title'>{app.name}</h2> + <h2 className='window__title'>{t(app.name)}</h2> <div className='window__content'>{children}</div> <div className='window__title-buttons'> { app.buttons.includes('min') && ( @@ -38,7 +42,7 @@ const App = ({children, app, setApps}) => { <FontAwesomeIcon icon={faArrowUp} /> </span> )} - { app.buttons.includes('max') && ( + { app.buttons.includes('max') && !forceMax && ( <span onClick={() => toggleMax(app.name, setApps)}> <FontAwesomeIcon icon={app.max ? faCompressAlt : faExpandAlt} /> </span> diff --git a/components/Form.js b/components/Form.js index f41c958..59f9710 100644 --- a/components/Form.js +++ b/components/Form.js @@ -1,20 +1,72 @@ import styles from 'styles/Main.module.scss' import React from 'react' import PropTypes from 'prop-types' +import useSettings from 'hooks/useSettings' -const Form = ({errorMessage, onSubmit, isLogin}) => ( - <form className={`window window--popup ${styles.userForm}`} onSubmit={onSubmit}> - <div className="window__content--popup"> - {isLogin ? 'Login to access your notes' : 'Register new user'} - </div> - <input type="email" name="email" placeholder="email" required /> - <input type="password" name="password" minLength="6" placeholder="password" required /> +const Form = ({errorMessage, onSubmit, isLogin}) => { + const {settings, setSettings, t} = useSettings() - <input className='window__button' type="submit" value={isLogin ? 'Login' : 'Register'} /> + const themeChange = c => { + setSettings(prev => ({...prev, theme: c})) + } - {errorMessage && <p className="error">{errorMessage}</p>} - </form> -) + const languageChange = l => { + setSettings(prev => ({...prev, language: l})) + } + + return ( + <form className={`window window--popup ${styles.userForm}`} onSubmit={onSubmit}> + <div className="window__content--popup"> + {isLogin ? t('log_in') : t('register_user')} + </div> + <input type="email" name="email" placeholder="email" required /> + <input type="password" name="password" minLength="6" placeholder={t('password')} required /> + {!isLogin && ( + <> + <input type="password" name="password_confirm" placeholder={t('confirm_password')} required /> + <fieldset> + <legend>{t('language')}</legend> + {['en', 'pl', 'es', 'de'].map(l => ( + <> + <input + key={`i_${l}`} + id={`language_${l}`} + type="radio" + name="language" + value={l} + defaultChecked={settings.language === l} + onChange={() => languageChange(l)} + /> + <label key={`l_${l}`} htmlFor={`language_${l}`}>{l.toUpperCase()}</label> + </> + ))} + </fieldset> + <fieldset> + <legend>{t('color_theme')}</legend> + {['green', 'blue', 'black'].map(c => ( + <> + <input + key={`i_${c}`} + id={`theme_${c}`} + type="radio" + name="theme" + value={c} + defaultChecked={settings.theme === c} + onChange={() => themeChange(c)} + /> + <label key={`l_${c}`} htmlFor={`theme_${c}`} /> + </> + ))} + </fieldset> + </> + )} + + <input className='window__button' type="submit" value={isLogin ? t('login') : t('register')} /> + + {errorMessage && <p className="error">{t(errorMessage)}</p>} + </form> + ) +} export default Form diff --git a/components/Header.js b/components/Header.js index 5279c80..9ff3d75 100644 --- a/components/Header.js +++ b/components/Header.js @@ -2,15 +2,17 @@ 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 useUser from 'hooks/useUser' +import fetchJson from 'helpers/fetchJson' import {focus, toggleMin} from 'helpers/windowActions' import {open} from 'helpers/windowActions' -import appList from 'helpers/appList' +import appList from 'configs/appList' +import useSettings from 'hooks/useSettings' const Header = ({apps, setApps}) => { const [userMenu, setUserMenu] = useState(false); const {user, mutateUser} = useUser() + const {t} = useSettings() const router = useRouter() const handleLogout = async (e) => { @@ -46,7 +48,7 @@ const Header = ({apps, setApps}) => { ...app.min ? {color: '#888'} : {} }} > - {app.name} + {t(app.name)} </span> </li> )) @@ -56,14 +58,14 @@ const Header = ({apps, setApps}) => { {!user?.isLoggedIn && ( <li> <Link href="/register"> - <a>Register</a> + <a>{t('register')}</a> </Link> </li> )} {!user?.isLoggedIn && ( <li> <Link href="/login"> - <a>Login</a> + <a>{t('login')}</a> </Link> </li> )} @@ -79,17 +81,19 @@ const Header = ({apps, setApps}) => { <> <div className={styles.headerOverlay} onClick={() => setUserMenu(false)} /> <ul className={styles.submenu}> - <li> - <span onClick={() => { - open({appName: 'Settings', ...appList.Settings}, setApps) - setUserMenu() - }}> - Settings - </span> - </li> + {user.isVerified && ( + <li> + <span onClick={() => { + open({appName: 'Settings', ...appList.Settings}, setApps) + setUserMenu() + }}> + {t('Settings')} + </span> + </li> + )} <li> <a href="/api/logout" onClick={handleLogout}> - Logout + {t('logout')} </a> </li> </ul> diff --git a/components/Layout.js b/components/Layout.js index e915285..d5627e3 100644 --- a/components/Layout.js +++ b/components/Layout.js @@ -1,26 +1,32 @@ import styles from 'styles/Main.module.scss' import React from 'react' import Head from 'next/head' -import {Header, Popup} from 'components' +import {Header, Popup, Splash} from 'components' +import useSettings from 'hooks/useSettings' import PropTypes from 'prop-types' 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> -) +}) => { + const {settings} = useSettings() + + if (!settings || !settings.theme || !settings.language) return <Splash fixed /> + + return ( + <section className={styles.layout +' '+ settings.theme}> + <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/Splash.js b/components/Splash.js index 7976de4..f807202 100644 --- a/components/Splash.js +++ b/components/Splash.js @@ -1,13 +1,18 @@ import styles from 'styles/Main.module.scss' import React from 'react' +import useSettings from 'hooks/useSettings' import {FontAwesomeIcon} from '@fortawesome/react-fontawesome' import {faBan, faSpinner} from '@fortawesome/free-solid-svg-icons' -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> -) +const Splash = ({type, fixed = false}) => { + const {t} = useSettings() + + return ( + <div className={`${type === 'connection' ? styles.connection : styles.loader} ${fixed ? styles.fixed : ''}`}> + <FontAwesomeIcon icon={type === 'connection' ? faBan : faSpinner} /> + <p>{type === 'connection' ? t('no_connection') : t('loading')}</p> + </div> + ) +} export default Splash diff --git a/helpers/appList.js b/configs/appList.js index a114a70..a114a70 100644 --- a/helpers/appList.js +++ b/configs/appList.js diff --git a/lib/dbConnect.js b/configs/dbConnect.js index a58ef79..0a0d200 100644 --- a/lib/dbConnect.js +++ b/configs/dbConnect.js @@ -1,11 +1,11 @@ import mongoose from 'mongoose' -// const MONGODB_URI = process.env.MYAPPS_MONGODB_URI -const MONGODB_URI = 'mongodb://localhost:27017/myapps' +const MONGODB_URI = process.env.MYAPPS_MONGODB_URI +// const MONGODB_URI = 'mongodb://localhost:27017/myapps' if (!MONGODB_URI) { throw new Error( - 'Please define the MONGODB_URI environment variable inside .env.local' + 'MONGODB_URI missing in dbConnect' ) } diff --git a/lib/sendMail.js b/configs/sendMail.js index f50eaf1..f50eaf1 100644 --- a/lib/sendMail.js +++ b/configs/sendMail.js diff --git a/configs/translations.js b/configs/translations.js new file mode 100644 index 0000000..4d204ea --- /dev/null +++ b/configs/translations.js @@ -0,0 +1,284 @@ +const translations = { + en: { + register_user: 'Register new user', + log_in: 'Log in to My Apps', + password: 'password', + confirm_password: 'confirm password', + color_theme: 'Color theme:', + language: 'Language:', + login: 'Login', + login_error: 'Could not log in', + logout: 'Logout', + register: 'Register', + register_error: 'Could not register user', + edit: 'Edit', + remove: 'Remove', + copy: 'Copy', + save: 'Save', + back: 'Back', + cancel: 'Cancel', + select_all: 'Select all', + import: 'Import', + export: 'Export', + choose_files: 'Choose files', + import_finished: 'Import finished.', + no_connection: 'No connection', + loading: 'Loading...', + sending: 'Sending...', + title: 'Title', + created: 'Created', + modified: 'Modified', + Notes: 'Notes', + Settings: 'Settings', + mail_ver_subject: 'Verification of your new My Apps account', + mail_ver_t1: 'Thank you for creating an account in My Apps.', + mail_ver_t2: 'We are sending you the verification code:', + mail_ver_t3: 'To finish verification log in and paste this code.', + verification_mail_error: 'Could not send verification email', + verification_error: 'Could not verify user', + verification_title: 'One last step missing!', + verification_text: 'To start using My Apps type the verification code we sent to your email', + verification_key: 'Verification key', + verify: 'Verify', + verification_not_received: 'If you did not receive the verification email', + verification_send_again: 'send it again', + verification_sent_again: 'Mail was successfully sent again, check your mailbox!', + settings_saved: 'Settings saved', + settings_save_error: 'Could not save settings', + passwords_not_match: 'Passwords do not match', + notes_new: 'New note', + notes_edit: 'Edit note:', + notes_add: 'Add note', + notes_save: 'Save note', + notes_add_new: 'Add new note:', + notes_list_empty: 'Your notes list is empty.', + notes_click_to_export: 'Click to export your notes:', + notes_to_export: 'Notes to export:', + notes_import: 'Import new notes:', + notes_to_import: 'Notes to import:', + notes_import_go_back: 'Go back to notes list or choose other notes to import.', + notes_copy_success: 'Note content was copied to clipboard', + notes_copy_error: 'Could not copy to clipboard', + notes_open_error: 'Could not open note', + notes_added: 'New note added', + notes_added_error: 'Could not save note', + notes_updated: 'Note updated', + notes_updated_error: 'Could not update note', + notes_removed: 'Note was removed', + notes_removed_error: 'Could not remove note', + notes_remove_confirm: 'Do you want to remove note?', + }, + pl: { + register_user: 'Zarejestruj użytkownika', + log_in: 'Zaloguj się do My Apps', + password: 'hasło', + confirm_password: 'potwierdź hasło', + color_theme: 'Wybierz motyw:', + language: 'Język:', + login: 'Zaloguj', + login_error: 'Błąd podczas logowania', + logout: 'Wyloguj', + register: 'Zarejestruj', + register_error: 'Błąd podczas rejestracji', + edit: 'Edytuj', + remove: 'Usuń', + copy: 'Kopiuj', + save: 'Zapisz', + back: 'Powrót', + cancel: 'Anuluj', + select_all: 'Wybierz wszystkie', + import: 'Wyślij', + export: 'Pobierz', + choose_files: 'Wybierz pliki', + import_finished: 'Wysyłanie zakończone.', + no_connection: 'Brak połączenia', + loading: 'Ładowanie...', + sending: 'Wysyłanie...', + title: 'Tytuł', + created: 'Utworzono', + modified: 'Zmodyfikowano', + Notes: 'Notatki', + Settings: 'Ustawienia', + mail_ver_subject: 'Weryfikacja Twojego nowego konta w aplikacji My Apps.', + mail_ver_t1: 'Dziękujemy za założenie konta w My Apps', + mail_ver_t2: 'Przesyłamy Ci kod weryfikacyjny:', + mail_ver_t3: 'W celu zakończenia weryfikacji zaloguj się i podaj ten kod.', + verification_mail_error: 'Błąd podczas wysyłania maila z kodem weryfikacyjnym', + verification_error: 'Błąd podczas weryfikacji użytkownika', + verification_title: 'Pozostał ostatni krok!', + verification_text: 'Aby rozpocząć korzystanie z My Apps wpisz kod weryfikacyjny, który wysłaliśmy Ci mailem na adres', + verification_key: 'Kod weryfikacyjny', + verify: 'Weryfikuj', + verification_not_received: 'Jeżeli nie otrzymałeś maila z kodem weryfikacyjnym', + verification_send_again: 'wyślij go ponownie', + verification_sent_again: 'Mail został wysłany ponownie, sprawdź skrzynkę!', + settings_saved: 'Ustawienia zapisane', + settings_save_error: 'Błąd podczas zapisu ustawień', + passwords_not_match: 'Podane hasła nie są identyczne', + notes_new: 'Nowa notatka', + notes_edit: 'Edytuj notatkę:', + notes_add: 'Dodaj notatkę', + notes_save: 'Zapisz notatkę', + notes_add_new: 'Dodaj nową notatkę:', + notes_list_empty: 'Twoja lista notatek jest pusta.', + notes_click_to_export: 'Kliknij aby pobrać Twoje notatki:', + notes_to_export: 'Notatki, które zostaną pobrane:', + notes_import: 'Wyślij nowe notatki:', + notes_to_import: 'Notatki do wysłania:', + notes_import_go_back: 'Wróć do listy notatek lub wyślij więcej plików.', + notes_copy_success: 'Skopiowano notatkę do schowka', + notes_copy_error: 'Błąd podczas kopiowania notatki', + notes_open_error: 'Błąd podczas otwierania notatki', + notes_added: 'Dodano nową notatkę', + notes_added_error: 'Błąd podczas dodawania notatki', + notes_updated: 'Notatka zmieniona', + notes_updated_error: 'Błąd podczas zapisu zmian w notatce', + notes_removed: 'Usunięto notatkę', + notes_removed_error: 'Błąd podczas usuwania notatki', + notes_remove_confirm: 'Czy na pewno chcesz usunąć notatkę?', + }, + es: { + register_user: 'Registrar al usuario', + log_in: 'Iniciar sesión en Mis aplicaciones', + password: 'contraseña', + confirm_password: 'confirmar la contraseña', + color_theme: 'Elige un tema: ', + language: 'Lengua: ', + login: 'Acceso', + login_error: 'Error de inicio de sesión', + logout: 'Cerrar sesión', + register: 'Registrarse', + register_error: 'Error durante el registro', + edit: 'Editar', + remove: 'Borrar', + copy: 'Dupdo', + save: 'Ahorrar', + back: 'Regreso', + cancel: 'Cancelar', + select_all: 'Elegir todos', + import: 'Enviar', + export: 'Descargar', + choose_files: 'Seleccione archivos', + import_finished: 'Envío completo.', + no_connection: 'Sin conexión', + loading: 'Aterrizaje...', + sending: 'Enviando ...', + title: 'Título', + created: 'Creado', + modified: 'Modificado', + Notes: 'Notas', + Settings: 'Ajustes', + mail_ver_subject: 'Verificación de su nueva cuenta en la aplicación My Apps.', + mail_ver_t1: 'Gracias por crear una cuenta con Mis aplicaciones', + mail_ver_t2: 'Te enviamos un código de verificación: ', + mail_ver_t3: 'Para completar la verificación, inicie sesión e ingrese este código.', + verification_mail_error: 'Error al enviar el correo electrónico con el código de verificación', + verification_error: 'Error de verificación del usuario', + verification_title: '¡Queda un último paso!', + verification_text: 'Para comenzar a usar Mis aplicaciones, ingrese el código de verificación que le enviamos por correo electrónico a la dirección', + verification_key: 'Código de verificación', + verify: 'Verificar', + verification_not_received: 'Si no ha recibido el correo electrónico con el código de verificación', + verification_send_again: 'reenvialo', + verification_sent_again: 'El correo ha sido enviado de nuevo, por favor revise su bandeja de entrada.', + settings_saved: 'Se guardó la configuración', + settings_save_error: 'No se pudo guardar la configuración', + passwords_not_match: 'Las contraseñas proporcionadas no coinciden', + notes_new: 'Nueva nota', + notes_edit: 'Edita la nota: ', + notes_add: 'Agrega una nota', + notes_save: 'Guardar la nota', + notes_add_new: 'Agrega una nota nueva: ', + notes_list_empty: 'Tu lista de notas está vacía.', + notes_click_to_export: 'Haz clic para descargar tus notas: ', + notes_to_export: 'Notas que se descargarán: ', + notes_import: 'Enviar nuevas notas: ', + notes_to_import: 'Notas a enviar: ', + notes_import_go_back: 'Regrese a la lista de notas o envíe más archivos.', + notes_copy_success: 'La nota se copió en el portapapeles.', + notes_copy_error: 'Error al copiar la nota', + notes_open_error: 'Error al abrir la nota', + notes_added: 'Se ha agregado una nueva nota', + notes_added_error: 'Error al agregar la nota', + notes_updated: 'Nota modificada', + notes_updated_error: 'Error al guardar cambios en la nota', + notes_removed: 'Nota eliminada', + notes_removed_error: 'Error al eliminar la nota', + notes_remove_confirm: '¿Estás seguro de que deseas eliminar la nota?', + }, + de: { + register_user: 'Registrieren Sie den Benutzer', + log_in: 'Melden Sie sich bei Meine Apps an', + password: 'Passwort', + confirm_password: 'Bestätige das Passwort', + color_theme: 'Wähle ein Thema: ', + language: 'Zunge: ', + login: 'Anmeldung', + login_error: 'Login Fehler', + logout: 'Ausloggen', + register: 'Registrieren', + register_error: 'Fehler bei der Registrierung', + edit: 'Bearbeiten', + remove: 'Löschen', + copy: 'Kopieren', + save: 'Speichern', + back: 'Zurückkehren', + cancel: 'Abbrechen', + select_all: 'Alles auswählen', + import: 'Senden', + export: 'Herunterladen', + choose_files: 'Dateien auswählen', + import_finished: 'Senden abgeschlossen.', + no_connection: 'Keine Verbindung', + loading: 'Landung...', + sending: 'Senden ...', + title: 'Titel', + created: 'Erstellt', + modified: 'Geändert', + Notes: 'Anmerkungen', + Settings: 'Einstellungen', + mail_ver_subject: 'Bestätigen Ihres neuen Kontos in der Anwendung „Meine Apps“.', + mail_ver_t1: 'Vielen Dank, dass Sie ein Konto bei My Apps erstellt haben', + mail_ver_t2: 'Wir senden Ihnen einen Bestätigungscode: ', + mail_ver_t3: 'Um die Verifizierung abzuschließen, loggen Sie sich ein und geben Sie diesen Code ein.', + verification_mail_error: 'Fehler beim Senden der E-Mail mit dem Bestätigungscode', + verification_error: 'Fehler bei der Benutzerüberprüfung', + verification_title: 'Ein letzter Schritt bleibt!', + verification_text: 'Um mit der Nutzung von Meine Apps zu beginnen, geben Sie den Bestätigungscode ein, den wir Ihnen per E-Mail an die Adresse gesendet haben', + verification_key: 'Verifizierungs-Schlüssel', + verify: 'Verifizieren', + verification_not_received: 'Wenn Sie die E-Mail mit dem Bestätigungscode nicht erhalten haben', + verification_send_again: 'Sende es erneut', + verification_sent_again: 'E-Mail wurde erneut gesendet, bitte überprüfen Sie Ihren Posteingang!', + settings_saved: 'Einstellungen gespeichert', + settings_save_error: 'Einstellungen konnten nicht gespeichert werden', + passwords_not_match: 'Die angegebenen Passwörter stimmen nicht überein', + notes_new: 'Neue Notiz', + notes_edit: 'Bearbeiten Sie die Notiz: ', + notes_add: 'Füg ein Notiz hinzu', + notes_save: 'Notiz speichern', + notes_add_new: 'Fügen Sie eine neue Notiz hinzu: ', + notes_list_empty: 'Ihre Notizenliste ist leer.', + notes_click_to_export: 'Klicken Sie hier, um Ihre Notizen herunterzuladen: ', + notes_to_export: 'Notizen, die heruntergeladen werden: ', + notes_import: 'Neue Notizen senden: ', + notes_to_import: 'Zu sendende Hinweise: ', + notes_import_go_back: 'Kehren Sie zur Notizliste zurück oder senden Sie weitere Dateien.', + notes_copy_success: 'Die Notiz wurde in die Zwischenablage kopiert', + notes_copy_error: 'Fehler beim Kopieren der Notiz', + notes_open_error: 'Fehler beim Öffnen der Notiz', + notes_added: 'Eine neue Notiz wurde hinzugefügt', + notes_added_error: 'Fehler beim Hinzufügen einer Notiz', + notes_updated: 'Hinweis geändert', + notes_updated_error: 'Fehler beim Speichern der Änderungen an der Notiz', + notes_removed: 'Notiz gelöscht', + notes_removed_error: 'Fehler beim Löschen der Notiz', + notes_remove_confirm: 'Möchten Sie die Notiz wirklich löschen? ', + } +} + +export const t = (l, k) => translations[l][k] + ? translations[l][k] + : translations.en[k] + +export default translations diff --git a/context/index.js b/context/index.js deleted file mode 100644 index a802bf6..0000000 --- a/context/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import React from 'react'; - -export default React.createContext() diff --git a/lib/crypt.js b/helpers/crypt.js index 5d3d79e..5d3d79e 100644 --- a/lib/crypt.js +++ b/helpers/crypt.js diff --git a/helpers/email.js b/helpers/email.js index d79426c..30e95c8 100644 --- a/helpers/email.js +++ b/helpers/email.js @@ -1,4 +1,6 @@ -export const subject = 'Verification of your new Notes App account' -export const text = key => `Thank you for creating an account in Notes App.\nWe are sending you the verification code:\n\n${key}\n\nTo finish verification log in and paste this code.` -export const html = key => `<p>Thank you for creating an account in Notes App.<br/>We are sending you the verification code:</p><p style="font-size: 150%;padding: 1em;border: 1px solid black">${key}</p><p>To finish verification log in and paste this code.</p></br>` +import {t} from 'configs/translations' + +export const subject = l => t(l, 'mail_ver_subject') +export const text = (l, key) => `${t(l, 'mail_ver_t1')}\n${t(l, 'mail_ver_t2')}\n\n${key}\n\n${t(l, 'mail_ver_t3')}` +export const html = (l, key) => `<p>${t(l, 'mail_ver_t1')}<br/>${t(l, 'mail_ver_t2')}</p><p style="font-size: 150%;padding: 1em;border: 1px solid black">${key}</p><p>${t(l, 'mail_ver_t3')}</p></br>` diff --git a/lib/fetchJson.js b/helpers/fetchJson.js index 5db80b5..5db80b5 100644 --- a/lib/fetchJson.js +++ b/helpers/fetchJson.js diff --git a/helpers/submitForm.js b/helpers/submitForm.js index 77b283d..631c174 100644 --- a/helpers/submitForm.js +++ b/helpers/submitForm.js @@ -1,11 +1,31 @@ -import fetchJson from 'lib/fetchJson' +import fetchJson from 'helpers/fetchJson' const submitForm = async (e, url, mutateUser, setErrorMsg) => { e.preventDefault() + const isRegister = url.includes('register') + if ( + isRegister && e.currentTarget.password_confirm + && e.currentTarget.password_confirm.value + !== e.currentTarget.password.value + ) { + setErrorMsg('passwords_not_match') + return + } else { + setErrorMsg() + } + const body = { email: e.currentTarget.email.value, password: e.currentTarget.password.value, + ...(e.currentTarget.language + ? {language: e.currentTarget.language.value} + : {} + ), + ...(e.currentTarget.theme + ? {theme: e.currentTarget.theme.value} + : {} + ), } try { @@ -17,9 +37,7 @@ const submitForm = async (e, url, mutateUser, setErrorMsg) => { }) ) } catch (err) { - url.includes('login') - ? setErrorMsg('Could not log in') - : setErrorMsg('Could not register user') + setErrorMsg(isRegister ? 'register_error' : 'login_error') } } diff --git a/lib/withSession.js b/hocs/withSession.js index 0361db3..0361db3 100644 --- a/lib/withSession.js +++ b/hocs/withSession.js diff --git a/hooks/useApps.js b/hooks/useApps.js new file mode 100644 index 0000000..babf712 --- /dev/null +++ b/hooks/useApps.js @@ -0,0 +1,17 @@ +import React, { createContext, useState, useContext } from 'react' + +const AppsContext = createContext() + +export const AppsProvider = ({children}) => { + const [apps, setApps] = useState([]) + + return ( + <AppsContext.Provider value={{apps, setApps}}> + {children} + </AppsContext.Provider> + ) +} + +const useApps = () => useContext(AppsContext) + +export default useApps diff --git a/hooks/useMediaQuery.js b/hooks/useMediaQuery.js new file mode 100644 index 0000000..1adb499 --- /dev/null +++ b/hooks/useMediaQuery.js @@ -0,0 +1,19 @@ +import { useState, useEffect } from "react"; + +const useMediaQuery = (query) => { + const [matches, setMatches] = useState(false); + + useEffect(() => { + const media = window.matchMedia(query); + if (media.matches !== matches) { + setMatches(media.matches); + } + const listener = () => setMatches(media.matches); + window.addEventListener("resize", listener); + return () => window.removeEventListener("resize", listener); + }, [matches, query]); + + return matches; +} + +export default useMediaQuery; diff --git a/hooks/useSettings.js b/hooks/useSettings.js new file mode 100644 index 0000000..90ee72d --- /dev/null +++ b/hooks/useSettings.js @@ -0,0 +1,50 @@ +import React, { createContext, useState, useEffect, useContext } from 'react' +import translations from 'configs/translations' +import useUser from 'hooks/useUser' + +const SettingsContext = createContext() + +export const SettingsProvider = ({children}) => { + const defaultSettings = {theme: 'green', language: 'en'} + const {user} = useUser() + const [data, setData] = useState() + + const setSettings = s => { + if (typeof window !== "undefined") { + window.localStorage.setItem('loggedOutSettings', JSON.stringify(s(data))) + } + setData(s) + } + + const t = key => data && data.language && translations[data.language][key] + ? translations[data.language][key] + ? translations[data.language][key] + : translations.en[key] + : '...' + + useEffect(() => { + const loggedOutSettings = JSON.parse(localStorage.getItem('loggedOutSettings')) + + if (user && user.isLoggedIn) { + const settings = {theme: user.theme, language: user.language} + if (typeof window !== "undefined") { + window.localStorage.setItem('loggedOutSettings', JSON.stringify(settings)) + } + setData(settings) + } else if (loggedOutSettings) { + setData(loggedOutSettings) + } else { + setData(defaultSettings) + } + }, [user]) + + return ( + <SettingsContext.Provider value={{settings: data, setSettings, t}}> + {children} + </SettingsContext.Provider> + ) +} + +const useSettings = () => useContext(SettingsContext) + +export default useSettings diff --git a/lib/useUser.js b/hooks/useUser.js index 16833aa..16833aa 100644 --- a/lib/useUser.js +++ b/hooks/useUser.js diff --git a/models/Note.js b/models/Note.js index 4e1956e..d19eae8 100644 --- a/models/Note.js +++ b/models/Note.js @@ -1,4 +1,4 @@ -const {encrypt, decrypt} = require('lib/crypt') +const {encrypt, decrypt} = require('helpers/crypt') const mongoose = require('mongoose') const noteSchema = new mongoose.Schema({ diff --git a/models/NoteList.js b/models/NoteList.js index 159364f..bf7b155 100644 --- a/models/NoteList.js +++ b/models/NoteList.js @@ -1,4 +1,4 @@ -const {encrypt, decrypt} = require('lib/crypt') +const {encrypt, decrypt} = require('helpers/crypt') const mongoose = require("mongoose") const decryptTitles = l => ({notes: l.notes.map(n => ({ ...n, title: decrypt(n.title)}))}) diff --git a/pages/_app.js b/pages/_app.js index 0fec9f6..637b096 100644 --- a/pages/_app.js +++ b/pages/_app.js @@ -1,6 +1,8 @@ import { SWRConfig } from 'swr' +import {SettingsProvider} from 'hooks/useSettings' import {PopupProvider} from 'hooks/usePopup' -import fetchJson from 'lib/fetchJson' +import {AppsProvider} from 'hooks/useApps' +import fetchJson from 'helpers/fetchJson' import '/styles/global.scss' function MyApp({Component, pageProps}) { @@ -13,9 +15,13 @@ function MyApp({Component, pageProps}) { }, }} > - <PopupProvider> - <Component {...pageProps} /> - </PopupProvider> + <SettingsProvider> + <PopupProvider> + <AppsProvider> + <Component {...pageProps} /> + </AppsProvider> + </PopupProvider> + </SettingsProvider> </SWRConfig> ) } diff --git a/pages/api/login.js b/pages/api/login.js index db39bda..038097c 100644 --- a/pages/api/login.js +++ b/pages/api/login.js @@ -1,5 +1,5 @@ -import withSession from 'lib/withSession' -import dbConnect from 'lib/dbConnect' +import withSession from 'hocs/withSession' +import dbConnect from 'configs/dbConnect' import User from 'models/User' export default withSession(async (req, res) => { diff --git a/pages/api/logout.js b/pages/api/logout.js index 8c7a766..04e5160 100644 --- a/pages/api/logout.js +++ b/pages/api/logout.js @@ -1,4 +1,4 @@ -import withSession from 'lib/withSession' +import withSession from 'hocs/withSession' export default withSession(async (req, res) => { req.session.destroy() diff --git a/pages/api/note/[id].js b/pages/api/note/[id].js index 46278c8..1781210 100644 --- a/pages/api/note/[id].js +++ b/pages/api/note/[id].js @@ -1,5 +1,5 @@ -import dbConnect from 'lib/dbConnect' -import withSession from 'lib/withSession' +import dbConnect from 'configs/dbConnect' +import withSession from 'hocs/withSession' import Note from 'models/Note' export default withSession(async (req, res) => { diff --git a/pages/api/notes.js b/pages/api/notes.js index 79ab281..0439a7a 100644 --- a/pages/api/notes.js +++ b/pages/api/notes.js @@ -1,5 +1,5 @@ -import dbConnect from 'lib/dbConnect' -import withSession from 'lib/withSession' +import dbConnect from 'configs/dbConnect' +import withSession from 'hocs/withSession' import NoteList from 'models/NoteList' import Note from 'models/Note' diff --git a/pages/api/register.js b/pages/api/register.js index 1407146..e14044e 100644 --- a/pages/api/register.js +++ b/pages/api/register.js @@ -1,6 +1,6 @@ -import dbConnect from 'lib/dbConnect' -import withSession from 'lib/withSession' -import sendMail from 'lib/sendMail' +import dbConnect from 'configs/dbConnect' +import sendMail from 'configs/sendMail' +import withSession from 'hocs/withSession' import {subject, text, html} from 'helpers/email' import User from 'models/User' import NoteList from 'models/NoteList' @@ -16,14 +16,13 @@ export default withSession(async (req, res) => { 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)) + sendMail(req.body.email, subject(language), text(language, key), html(language, key)) 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) } catch (error) { - console.log(error) res.status(400).json({isLoggedIn: false}) } break diff --git a/pages/api/settings.js b/pages/api/settings.js index e70868e..0de5e73 100644 --- a/pages/api/settings.js +++ b/pages/api/settings.js @@ -1,6 +1,6 @@ -import withSession from 'lib/withSession' +import dbConnect from 'configs/dbConnect' +import withSession from 'hocs/withSession' import User from 'models/User' -import dbConnect from 'lib/dbConnect' export default withSession(async (req, res) => { await dbConnect() diff --git a/pages/api/user.js b/pages/api/user.js index 59db3b6..44c7fc3 100644 --- a/pages/api/user.js +++ b/pages/api/user.js @@ -1,5 +1,5 @@ -import dbConnect from 'lib/dbConnect' -import withSession from 'lib/withSession' +import dbConnect from 'configs/dbConnect' +import withSession from 'hocs/withSession' import User from 'models/User' export default withSession(async (req, res) => { diff --git a/pages/api/verify.js b/pages/api/verify.js index 1606dbc..bdad434 100644 --- a/pages/api/verify.js +++ b/pages/api/verify.js @@ -1,7 +1,7 @@ -import withSession from 'lib/withSession' -import dbConnect from 'lib/dbConnect' -import sendMail from 'lib/sendMail' +import dbConnect from 'configs/dbConnect' +import sendMail from 'configs/sendMail' import User from 'models/User' +import withSession from 'hocs/withSession' import {subject, text, html} from 'helpers/email' export default withSession(async (req, res) => { @@ -10,13 +10,13 @@ export default withSession(async (req, res) => { switch (req.method) { case 'GET': try { - const {email} = req.session.get('user') + const {email, language: l} = req.session.get('user') if (!email) { throw new Error('Something went wrong') } const key = await User.getVerificationKey(email) if (!key) { throw new Error('Something went wrong') } - const response = await sendMail(email, subject, text(key), html(key)) + const response = await sendMail(email, subject(l), text(l, key), html(l, key)) if (!response?.accepted?.length) { throw new Error('Something went wrong') } res.status(204).send() @@ -33,7 +33,6 @@ export default withSession(async (req, res) => { res.status(200).json(user) } } catch (error) { - console.log(error) res.status(400).send() } break diff --git a/pages/index.js b/pages/index.js index ee1c277..b441362 100644 --- a/pages/index.js +++ b/pages/index.js @@ -1,26 +1,21 @@ import styles from 'styles/Main.module.scss' -import React, {useState, useEffect} from 'react' -import useUser from 'lib/useUser' +import React from 'react' +import useUser from 'hooks/useUser' +import useSettings from 'hooks/useSettings' +import useApps from 'hooks/useApps' import {Layout, App, Splash} from 'components' import {open} from 'helpers/windowActions' -import appList from 'helpers/appList' -import Context from '../context'; +import appList from 'configs/appList' const Home = () => { - const [settings, setSettings] = useState() - const [apps, setApps] = useState([]) + const {t} = useSettings() + const {apps, setApps} = useApps() const {user} = useUser({ redirectToLogin: true, redirectToVerify: true, redirectToApps: true, }) - useEffect(() => { - if (user) { - setSettings({theme: user.theme, language: user.language}) - } - }, [user]) - if (!user) return ( <Layout><Splash fixed /></Layout> ) @@ -44,39 +39,37 @@ const Home = () => { } return ( - <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 + <Layout apps={apps} setApps={setApps}> + <> + { + 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>{t(a[0])}</p> + </div> + )) + } + {apps && apps.map(app => { + const AppComponent = appList[app.name].component - return ( - <App - key={`${app.name}_app`} - app={app} - setApps={setApps} - > - <AppComponent /> - </App> - ); - })} - </> - </Layout> - </Context.Provider> + return ( + <App + key={`${app.name}_app`} + app={app} + setApps={setApps} + > + <AppComponent /> + </App> + ); + })} + </> + </Layout> ) } diff --git a/pages/login.js b/pages/login.js index 3fb2df6..533e8e8 100644 --- a/pages/login.js +++ b/pages/login.js @@ -1,24 +1,33 @@ -import { useState } from 'react' -import useUser from 'lib/useUser' +import {useState} from 'react' +import useUser from 'hooks/useUser' import submitForm from 'helpers/submitForm' -import {Layout, Form} from 'components' +import {Layout, Form, Splash} from 'components' const Login = () => { + const [errorMsg, setErrorMsg] = useState('') + const [loading, setLoading] = useState(false) const {mutateUser} = useUser({ redirectToVerify: true, redirectToApps: true, }) - const [errorMsg, setErrorMsg] = useState('') - const handleSubmit = e => submitForm(e, '/api/login', mutateUser, setErrorMsg) + const handleSubmit = async e => { + setLoading(true) + await submitForm(e, '/api/login', mutateUser, setErrorMsg) + setLoading(false) + } return ( <Layout> - <Form - isLogin - errorMessage={errorMsg} - onSubmit={handleSubmit} - /> + {loading + ? <Splash fixed /> + : ( + <Form + isLogin + errorMessage={errorMsg} + onSubmit={handleSubmit} + /> + )} </Layout> ) } diff --git a/pages/register.js b/pages/register.js index 3a00504..5405432 100644 --- a/pages/register.js +++ b/pages/register.js @@ -1,22 +1,31 @@ -import {useState } from 'react' -import useUser from 'lib/useUser' +import {useState} from 'react' +import useUser from 'hooks/useUser' import submitForm from 'helpers/submitForm' -import {Layout, Form} from 'components' +import {Layout, Form, Splash} from 'components' const Register = () => { - const { mutateUser } = useUser({ + const [errorMsg, setErrorMsg] = useState('') + const [loading, setLoading] = useState(false) + const {mutateUser} = useUser({ redirectToVerify: true, }) - const [errorMsg, setErrorMsg] = useState('') - const handleSubmit = e => submitForm(e, '/api/register', mutateUser, setErrorMsg) + const handleSubmit = async e => { + setLoading(true) + await submitForm(e, '/api/register', mutateUser, setErrorMsg) + setLoading(false) + } return ( <Layout> - <Form - errorMessage={errorMsg} - onSubmit={handleSubmit} - /> + {loading + ? <Splash fixed /> + : ( + <Form + errorMessage={errorMsg} + onSubmit={handleSubmit} + /> + )} </Layout> ) } diff --git a/pages/verify.js b/pages/verify.js index 5991035..2f7c863 100644 --- a/pages/verify.js +++ b/pages/verify.js @@ -1,18 +1,21 @@ import styles from 'styles/Main.module.scss' import {useState} from 'react' -import useUser from 'lib/useUser' -import fetchJson from 'lib/fetchJson' +import useUser from 'hooks/useUser' +import useSettings from 'hooks/useSettings' +import fetchJson from 'helpers/fetchJson' import {Layout} from 'components' import Splash from 'components/Splash' const Verify = () => { + const {t} = useSettings() + const [errorMsg, setErrorMsg] = useState('') + const [loading, setLoading] = useState(false) + const [sending, setSending] = useState(false) + const [resent, setResent] = useState(false) const {user, mutateUser} = useUser({ redirectToLogin: true, redirectToApps: true, }) - const [errorMsg, setErrorMsg] = useState('') - const [sending, setSending] = useState(false) - const [resent, setResent] = useState(false) const handleSendMail = async e => { e.preventDefault() @@ -22,7 +25,7 @@ const Verify = () => { await fetch('/api/verify') setResent(true) } catch (error) { - setErrorMsg('Could not send verification email') + setErrorMsg(t('verification_mail_error')) } finally { setSending(false) } @@ -30,6 +33,7 @@ const Verify = () => { const handleKey = async e => { e.preventDefault() + setLoading(true) const key = e.currentTarget.key.value try { mutateUser( @@ -40,36 +44,36 @@ const Verify = () => { }) ) } catch (err) { - setErrorMsg('Could not verify user') + setErrorMsg(t('verification_error')) + } finally { + setLoading(false) } } return ( <Layout> - {!user + {!user || loading ? ( - <div className="window window--popup"> - <Splash /> - </div> + <Splash fixed /> ) : ( <div className={`window window--popup ${styles.verify}`}> - <p>One last step missing</p> - <p>{`To start using Notes App type the verification code we sent to your email (${user.email}):`}</p> + <p>{t('verification_title')}</p> + <p>{`${t('verification_text')} ${user.email}`}</p> <form onSubmit={handleKey}> - <input type="text" placeholder="Verification key" name="key" /> - <button className="window__button" type="submit">Verify</button> + <input type="text" placeholder={t('verification_key')} name="key" /> + <button className="window__button" type="submit">{t('verify')}</button> </form> { sending ? ( - <p>Sending...</p> + <p>{t('sending')}</p> ) : ( resent ? ( - <p>Mail was successfully sent again, check your mailbox!</p> + <p>{t('verification_sent_again')}</p> ) : ( - <p>If you didn't get verification email - <span className={styles.email} onClick={handleSendMail}>send it again</span>. + <p>{t('verification_not_received')} + <span className={styles.email} onClick={handleSendMail}>{t('verification_send_again')}</span>. </p> ) ) diff --git a/styles/Main.module.scss b/styles/Main.module.scss index 3f0230c..bac6a97 100644 --- a/styles/Main.module.scss +++ b/styles/Main.module.scss @@ -9,8 +9,8 @@ .icon { text-decoration: none; display: inline-block; - width: 4.5em; padding: .5em; + text-align: center; img { width: 3em; @@ -19,7 +19,6 @@ p { margin-top: .25em; padding: .25em; - text-align: center; transition: .2s background; border-radius: .5em; } @@ -30,6 +29,8 @@ } .userForm { + padding: 2em; + label, & { display: flex; @@ -50,11 +51,77 @@ border-radius: .5px; } + + input[type=password]:nth-of-type(2) { + margin-top: 1em; + } + p { text-align: center; color: var(--color-error); margin: 1rem 0 0; } + + legend { + font-size: .8em; + padding: 2em 0 1.5em; + } + + fieldset:nth-of-type(1) { + input { + display: none; + } + + label { + padding: 0 .75em; + transition: .3s color; + display: inline-block; + color: var(--color-decor); + + &:hover { + color: #666; + } + } + + input:checked + label { + font-weight: 600; + color: var(--color-text); + } + } + + fieldset:nth-of-type(2) { + input { + display: none; + } + + label { + 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:checked + label { + outline: 3px solid var(--color-window-buttons); + } + } + + input[type=submit] { + margin-top: 1.5em; + } } .header { @@ -70,7 +137,7 @@ overflow: auto; } - ul { + & > ul { display: block; & > li { @@ -119,21 +186,34 @@ color: var(--color-text-alt); background-color: var(--color-selected); } - } .submenu { position: absolute; right: 4px; top: 2.1em; - width: 8em; - padding: .5em; + padding: .75em; background-color: var(--color-window-content); border-bottom: 1px solid var(--color-window-border-bottom); border-radius: .5em; + display: flex; + flex-direction: column; - & > li > span { + & > li > span, + & > li > a { + display: block; + color: var(--color-text); + padding: .5em; + margin: .25em; + white-space: nowrap; text-decoration: none; + transition: .3s background, .3s color; + border-radius: .5em; + + &:hover { + color: var(--color-text-alt); + background-color: var(--color-selected); + } } } diff --git a/styles/_window.scss b/styles/_window.scss index a2d0438..08ddfaf 100644 --- a/styles/_window.scss +++ b/styles/_window.scss @@ -26,22 +26,22 @@ } &:not(.window--popup) { - height: 80vh; - width: 80vw; - top: 10vh; - left: 10vw; + height: 100%; + width: 100%; + } + // top: 50%; + // left: 50%; + // @media (min-width: 80em) { + // width: 64em; + // left: calc((100vw - 64em) / 2) + // } - @media (min-width: 80em) { - width: 64em; - left: calc((100vw - 64em) / 2) - } - - @media (min-height: 80em) { - width: 64em; - left: calc((100vh - 64em) / 2) - } - } + // @media (min-height: 80em) { + // width: 64em; + // left: calc((100vh - 64em) / 2) + // } + // } &__title { position: absolute; @@ -181,8 +181,8 @@ right: 0!important; bottom: 0!important; left: 0!important; - width: 100%; - height: 100%; + width: 100%!important; + height: 100%!important; & > .window__content { top: 0; |