From f08f6ca0a9d337efff280d4d1669a41b5d9c31c2 Mon Sep 17 00:00:00 2001 From: piotrruss Date: Thu, 2 Sep 2021 22:28:11 +0200 Subject: finish translations, force maximize --- apps/Notes/components/Export.js | 12 +- apps/Notes/components/Import.js | 15 +- apps/Notes/components/List.js | 18 +- apps/Notes/components/ListItem.js | 8 +- apps/Notes/components/NoteEdit.js | 16 +- apps/Notes/components/NoteView.js | 16 +- apps/Notes/helpers/copyToClipboard.js | 6 +- apps/Notes/helpers/export.js | 2 +- apps/Notes/helpers/import.js | 2 +- apps/Notes/helpers/noteActions.js | 32 ++-- apps/Settings/api.js | 10 ++ apps/Settings/components/Settings.js | 64 +++---- apps/Settings/styles/Settings.module.scss | 18 ++ components/App.js | 18 +- components/Form.js | 74 ++++++-- components/Header.js | 34 ++-- components/Layout.js | 34 ++-- components/Splash.js | 17 +- configs/appList.js | 8 + configs/dbConnect.js | 46 +++++ configs/sendMail.js | 26 +++ configs/translations.js | 284 ++++++++++++++++++++++++++++++ context/index.js | 3 - helpers/appList.js | 8 - helpers/crypt.js | 19 ++ helpers/email.js | 8 +- helpers/fetchJson.js | 23 +++ helpers/submitForm.js | 26 ++- hocs/withSession.js | 14 ++ hooks/useApps.js | 17 ++ hooks/useMediaQuery.js | 19 ++ hooks/useSettings.js | 50 ++++++ hooks/useUser.js | 33 ++++ lib/crypt.js | 19 -- lib/dbConnect.js | 46 ----- lib/fetchJson.js | 23 --- lib/sendMail.js | 26 --- lib/useUser.js | 33 ---- lib/withSession.js | 14 -- models/Note.js | 2 +- models/NoteList.js | 2 +- pages/_app.js | 14 +- pages/api/login.js | 4 +- pages/api/logout.js | 2 +- pages/api/note/[id].js | 4 +- pages/api/notes.js | 4 +- pages/api/register.js | 9 +- pages/api/settings.js | 4 +- pages/api/user.js | 4 +- pages/api/verify.js | 11 +- pages/index.js | 81 ++++----- pages/login.js | 29 +-- pages/register.js | 29 +-- pages/verify.js | 42 +++-- styles/Main.module.scss | 94 +++++++++- styles/_window.scss | 32 ++-- 56 files changed, 1051 insertions(+), 457 deletions(-) create mode 100644 apps/Settings/api.js create mode 100644 configs/appList.js create mode 100644 configs/dbConnect.js create mode 100644 configs/sendMail.js create mode 100644 configs/translations.js delete mode 100644 context/index.js delete mode 100644 helpers/appList.js create mode 100644 helpers/crypt.js create mode 100644 helpers/fetchJson.js create mode 100644 hocs/withSession.js create mode 100644 hooks/useApps.js create mode 100644 hooks/useMediaQuery.js create mode 100644 hooks/useSettings.js create mode 100644 hooks/useUser.js delete mode 100644 lib/crypt.js delete mode 100644 lib/dbConnect.js delete mode 100644 lib/fetchJson.js delete mode 100644 lib/sendMail.js delete mode 100644 lib/useUser.js delete mode 100644 lib/withSession.js 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 (
-
{setAction('')}}>Back
+
{setAction('')}}>{t('back')}
-

Click to export your notes:

+

{t('notes_click_to_export')}

handleExport(e, ids, notes)} /> -

Notes to export:

+

{t('notes_to_export')}

{ onChange={e => handleSelectAll(e, notes, setIds)} checked={notes.length === ids.length} /> - +
    {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 (
    -
    {setAction('')}}>Back
    +
    {setAction('')}}>{t('back')}
    handleImport(e, files, mutateNotes, setDone)}> - Import new notes: + {t('notes_import')}
    {files && ( <> -

    Notes to import:

    +

    {t('notes_to_import')}

      {[...files].map((f, i) =>
    • {f.name} {state(done[i])}
    • )}
    { done.length === 0 - ? - :

    Import finished.

    Go back to notes list or choose other notes to import.

    + ? + :

    {t('import_finished')}

    {t('notes_import_go_back')}

    } )} 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 === '' ? ( <>
    -
    setAction('addNote')}>New note
    -
    setAction('importNotes')}>Import
    -
    setAction('exportNotes')}>Export
    +
    setAction('addNote')}>{t('notes_new')}
    +
    setAction('importNotes')}>{t('import')}
    +
    setAction('exportNotes')}>{t('export')}
    - - - + + + @@ -57,7 +59,7 @@ const List = () => { /> ))) : ( - + )} 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}) => { {e.stopPropagation(); exportNote(note)}}> - removeNote(e, note._id, mutateNotes, setPopup, setAction)}> + removeNote(e, note._id, mutateNotes, t, setPopup, setAction)}> 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 (
    -

    {fetchedNote ? 'Edit note:' : 'Add new note:'}

    +

    {fetchedNote ? t('notes_edit') : t('notes_add_new')}

    { className='window__button' onClick={() => {setAction(fetchedNote ? 'showNote' : '')}} > - Cancel + {t('cancel')}
    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 (
    -
    {setFetchedNote(); setAction('')}}>Back
    -
    copyToClipboard(content, setPopup)}>Copy
    -
    {setAction('editNote')}}>Edit
    -
    exportNote(fetchedNote)}>Export
    -
    {removeNote(e, _id, mutateNotes, setPopup, setAction)}}>Remove
    +
    {setFetchedNote(); setAction('')}}>{t('back')}
    +
    copyToClipboard(content, t, setPopup)}>{t('copy')}
    +
    {setAction('editNote')}}>{t('edit')}
    +
    exportNote(fetchedNote)}>{t('export')}
    +
    {removeNote(e, _id, mutateNotes, t, setPopup, setAction)}}>{t('remove')}

    {title}

    {content}

    -
    ) } 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 (
    -
    Language:
    +
    {t('language')}
    - English + {['en', 'pl', 'es', 'de'].map(l => ( + {setSettings(prev => ({...prev, language: l}))}} + >{l.toUpperCase()} + ))}
    -
    Theme:
    +
    {t('color_theme')}
    - {setSettings({language, theme:'green'})}} - className={theme === 'green' ? styles.settings__active : ''} - /> - {setSettings({language, theme:'blue'})}} - className={theme === 'blue' ? styles.settings__active : ''} - /> - {setSettings({language, theme:'black'})}} - className={theme === 'black' ? styles.settings__active : ''} - /> + {['green', 'blue', 'black'].map(c => ( + {setSettings(prev => ({...prev, theme: c}))}} + className={theme === c ? styles.settings__active : ''} + /> + ))}
    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)`, } }} > -

    {app.name}

    +

    {t(app.name)}

    {children}
    { app.buttons.includes('min') && ( @@ -38,7 +42,7 @@ const App = ({children, app, setApps}) => { )} - { app.buttons.includes('max') && ( + { app.buttons.includes('max') && !forceMax && ( toggleMax(app.name, setApps)}> 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}) => ( -
    -
    - {isLogin ? 'Login to access your notes' : 'Register new user'} -
    - - +const Form = ({errorMessage, onSubmit, isLogin}) => { + const {settings, setSettings, t} = useSettings() - + const themeChange = c => { + setSettings(prev => ({...prev, theme: c})) + } - {errorMessage &&

    {errorMessage}

    } - -) + const languageChange = l => { + setSettings(prev => ({...prev, language: l})) + } + + return ( +
    +
    + {isLogin ? t('log_in') : t('register_user')} +
    + + + {!isLogin && ( + <> + +
    + {t('language')} + {['en', 'pl', 'es', 'de'].map(l => ( + <> + languageChange(l)} + /> + + + ))} +
    +
    + {t('color_theme')} + {['green', 'blue', 'black'].map(c => ( + <> + themeChange(c)} + /> +
    + + )} + + + + {errorMessage &&

    {t(errorMessage)}

    } + + ) +} 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)} )) @@ -56,14 +58,14 @@ const Header = ({apps, setApps}) => { {!user?.isLoggedIn && (
  • - Register + {t('register')}
  • )} {!user?.isLoggedIn && (
  • - Login + {t('login')}
  • )} @@ -79,17 +81,19 @@ const Header = ({apps, setApps}) => { <>
    setUserMenu(false)} />
      -
    • - { - open({appName: 'Settings', ...appList.Settings}, setApps) - setUserMenu() - }}> - Settings - -
    • + {user.isVerified && ( +
    • + { + open({appName: 'Settings', ...appList.Settings}, setApps) + setUserMenu() + }}> + {t('Settings')} + +
    • + )}
    • - Logout + {t('logout')}
    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, -}) => ( -
    - - My Apps - -
    -
    {children}
    -
    -
    - -
    -) +}) => { + const {settings} = useSettings() + + if (!settings || !settings.theme || !settings.language) return + + return ( +
    + + My Apps + +
    +
    {children}
    +
    +
    + +
    + ) +} 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}) => ( -
    - -

    {type === 'connection' ? 'No connection' : 'Loading...'}

    -
    -) +const Splash = ({type, fixed = false}) => { + const {t} = useSettings() + + return ( +
    + +

    {type === 'connection' ? t('no_connection') : t('loading')}

    +
    + ) +} export default Splash diff --git a/configs/appList.js b/configs/appList.js new file mode 100644 index 0000000..a114a70 --- /dev/null +++ b/configs/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/configs/dbConnect.js b/configs/dbConnect.js new file mode 100644 index 0000000..0a0d200 --- /dev/null +++ b/configs/dbConnect.js @@ -0,0 +1,46 @@ +import mongoose from 'mongoose' + +const MONGODB_URI = process.env.MYAPPS_MONGODB_URI +// const MONGODB_URI = 'mongodb://localhost:27017/myapps' + +if (!MONGODB_URI) { + throw new Error( + 'MONGODB_URI missing in dbConnect' + ) +} + +/** + * Global is used here to maintain a cached connection across hot reloads + * in development. This prevents connections growing exponentially + * during API Route usage. + */ +let cached = global.mongoose + +if (!cached) { + cached = global.mongoose = {conn: null, promise: null} +} + +async function dbConnect() { + if (cached.conn) { + return cached.conn + } + + if (!cached.promise) { + const opts = { + useNewUrlParser: true, + useUnifiedTopology: true, + bufferCommands: false, + bufferMaxEntries: 0, + useFindAndModify: false, + useCreateIndex: true, + } + + cached.promise = mongoose.connect(MONGODB_URI, opts).then((mongoose) => { + return mongoose + }) + } + cached.conn = await cached.promise + return cached.conn +} + +export default dbConnect diff --git a/configs/sendMail.js b/configs/sendMail.js new file mode 100644 index 0000000..f50eaf1 --- /dev/null +++ b/configs/sendMail.js @@ -0,0 +1,26 @@ +import nodemailer from 'nodemailer' + +const sendMail = (to, subject, text, html) => { + + const transporter = nodemailer.createTransport({ + port: 465, + host: process.env.MYAPPS_MAIL_SMTP_SERVER, + auth: { + user: process.env.MYAPPS_MAIL_ADDRESS, + pass: process.env.MYAPPS_MAIL_PASSWORD, + }, + secure: true, + }) + + const mailData = { + from: `"Notes App" <${process.env.MYAPPS_MAIL_ADDRESS}>`, + to, + subject, + text, + html, + } + + return transporter.sendMail(mailData) +} + +export default sendMail 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/helpers/appList.js b/helpers/appList.js deleted file mode 100644 index a114a70..0000000 --- a/helpers/appList.js +++ /dev/null @@ -1,8 +0,0 @@ -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/crypt.js b/helpers/crypt.js new file mode 100644 index 0000000..5d3d79e --- /dev/null +++ b/helpers/crypt.js @@ -0,0 +1,19 @@ +const crypto = require('crypto') +const algorithm = 'aes-256-ctr' +const secretKey = process.env.MYAPPS_NOTES_KEY + +export const encrypt = (text) => { + const iv = crypto.randomBytes(16) + const cipher = crypto.createCipheriv(algorithm, secretKey, iv) + const encrypted = Buffer.concat([cipher.update(text), cipher.final()]) + + return iv.toString('hex') + '::' + encrypted.toString('hex') +} + +export const decrypt = (hash) => { + const [iv, content] = hash.split('::') + const decipher = crypto.createDecipheriv(algorithm, secretKey, Buffer.from(iv, 'hex')) + const decrpyted = Buffer.concat([decipher.update(Buffer.from(content, 'hex')), decipher.final()]) + + return decrpyted.toString(); +} 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 => `

    Thank you for creating an account in Notes App.
    We are sending you the verification code:

    ${key}

    To finish verification log in and paste this code.


    ` +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) => `

    ${t(l, 'mail_ver_t1')}
    ${t(l, 'mail_ver_t2')}

    ${key}

    ${t(l, 'mail_ver_t3')}


    ` diff --git a/helpers/fetchJson.js b/helpers/fetchJson.js new file mode 100644 index 0000000..5db80b5 --- /dev/null +++ b/helpers/fetchJson.js @@ -0,0 +1,23 @@ +export default async function fetcher(...args) { + try { + const response = await fetch(...args) + + // if the server replies, there's always some data in json + // if there's a network error, it will throw at the previous line + const data = await response.json() + + if (response.ok) { + return data + } + + const error = new Error(response.statusText) + error.response = response + error.data = data + throw error + } catch (error) { + if (!error.data) { + error.data = {message: error.message} + } + throw error + } +} 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/hocs/withSession.js b/hocs/withSession.js new file mode 100644 index 0000000..0361db3 --- /dev/null +++ b/hocs/withSession.js @@ -0,0 +1,14 @@ +// this file is a wrapper with defaults to be used in both API routes and `getServerSideProps` functions +import {withIronSession} from 'next-iron-session' + +export default function withSession(handler) { + return withIronSession(handler, { + password: process.env.MYAPPS_SECRET_COOKIE_PASSWORD, + cookieName: 'myapps_session', + cookieOptions: { + // the next line allows to use the session in non-https environments like + // Next.js dev mode (http://localhost:3000) + secure: process.env.NODE_ENV === 'production' ? true : false, + }, + }) +} 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 ( + + {children} + + ) +} + +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 ( + + {children} + + ) +} + +const useSettings = () => useContext(SettingsContext) + +export default useSettings diff --git a/hooks/useUser.js b/hooks/useUser.js new file mode 100644 index 0000000..16833aa --- /dev/null +++ b/hooks/useUser.js @@ -0,0 +1,33 @@ +import { useEffect } from 'react' +import Router from 'next/router' +import useSWR from 'swr' + +export default function useUser({ + redirectToLogin = false, + redirectToVerify = false, + redirectToApps = false, +} = {}) { + const { data: user, mutate: mutateUser } = useSWR('/api/user') + + useEffect(() => { + if (!user || (!redirectToLogin && !redirectToVerify && !redirectToApps)) return + + if (redirectToLogin && !user?.isLoggedIn) { + Router.push('/login') + } + + if (redirectToVerify && user?.isLoggedIn && !user?.isVerified) { + Router.push('/verify') + } + + if ( + (redirectToApps && user?.isLoggedIn && user?.isVerified) + ) { + Router.push('/') + } + + return + }, [user, redirectToLogin, redirectToVerify, redirectToApps]) + + return {user, mutateUser} +} diff --git a/lib/crypt.js b/lib/crypt.js deleted file mode 100644 index 5d3d79e..0000000 --- a/lib/crypt.js +++ /dev/null @@ -1,19 +0,0 @@ -const crypto = require('crypto') -const algorithm = 'aes-256-ctr' -const secretKey = process.env.MYAPPS_NOTES_KEY - -export const encrypt = (text) => { - const iv = crypto.randomBytes(16) - const cipher = crypto.createCipheriv(algorithm, secretKey, iv) - const encrypted = Buffer.concat([cipher.update(text), cipher.final()]) - - return iv.toString('hex') + '::' + encrypted.toString('hex') -} - -export const decrypt = (hash) => { - const [iv, content] = hash.split('::') - const decipher = crypto.createDecipheriv(algorithm, secretKey, Buffer.from(iv, 'hex')) - const decrpyted = Buffer.concat([decipher.update(Buffer.from(content, 'hex')), decipher.final()]) - - return decrpyted.toString(); -} diff --git a/lib/dbConnect.js b/lib/dbConnect.js deleted file mode 100644 index a58ef79..0000000 --- a/lib/dbConnect.js +++ /dev/null @@ -1,46 +0,0 @@ -import mongoose from 'mongoose' - -// 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' - ) -} - -/** - * Global is used here to maintain a cached connection across hot reloads - * in development. This prevents connections growing exponentially - * during API Route usage. - */ -let cached = global.mongoose - -if (!cached) { - cached = global.mongoose = {conn: null, promise: null} -} - -async function dbConnect() { - if (cached.conn) { - return cached.conn - } - - if (!cached.promise) { - const opts = { - useNewUrlParser: true, - useUnifiedTopology: true, - bufferCommands: false, - bufferMaxEntries: 0, - useFindAndModify: false, - useCreateIndex: true, - } - - cached.promise = mongoose.connect(MONGODB_URI, opts).then((mongoose) => { - return mongoose - }) - } - cached.conn = await cached.promise - return cached.conn -} - -export default dbConnect diff --git a/lib/fetchJson.js b/lib/fetchJson.js deleted file mode 100644 index 5db80b5..0000000 --- a/lib/fetchJson.js +++ /dev/null @@ -1,23 +0,0 @@ -export default async function fetcher(...args) { - try { - const response = await fetch(...args) - - // if the server replies, there's always some data in json - // if there's a network error, it will throw at the previous line - const data = await response.json() - - if (response.ok) { - return data - } - - const error = new Error(response.statusText) - error.response = response - error.data = data - throw error - } catch (error) { - if (!error.data) { - error.data = {message: error.message} - } - throw error - } -} diff --git a/lib/sendMail.js b/lib/sendMail.js deleted file mode 100644 index f50eaf1..0000000 --- a/lib/sendMail.js +++ /dev/null @@ -1,26 +0,0 @@ -import nodemailer from 'nodemailer' - -const sendMail = (to, subject, text, html) => { - - const transporter = nodemailer.createTransport({ - port: 465, - host: process.env.MYAPPS_MAIL_SMTP_SERVER, - auth: { - user: process.env.MYAPPS_MAIL_ADDRESS, - pass: process.env.MYAPPS_MAIL_PASSWORD, - }, - secure: true, - }) - - const mailData = { - from: `"Notes App" <${process.env.MYAPPS_MAIL_ADDRESS}>`, - to, - subject, - text, - html, - } - - return transporter.sendMail(mailData) -} - -export default sendMail diff --git a/lib/useUser.js b/lib/useUser.js deleted file mode 100644 index 16833aa..0000000 --- a/lib/useUser.js +++ /dev/null @@ -1,33 +0,0 @@ -import { useEffect } from 'react' -import Router from 'next/router' -import useSWR from 'swr' - -export default function useUser({ - redirectToLogin = false, - redirectToVerify = false, - redirectToApps = false, -} = {}) { - const { data: user, mutate: mutateUser } = useSWR('/api/user') - - useEffect(() => { - if (!user || (!redirectToLogin && !redirectToVerify && !redirectToApps)) return - - if (redirectToLogin && !user?.isLoggedIn) { - Router.push('/login') - } - - if (redirectToVerify && user?.isLoggedIn && !user?.isVerified) { - Router.push('/verify') - } - - if ( - (redirectToApps && user?.isLoggedIn && user?.isVerified) - ) { - Router.push('/') - } - - return - }, [user, redirectToLogin, redirectToVerify, redirectToApps]) - - return {user, mutateUser} -} diff --git a/lib/withSession.js b/lib/withSession.js deleted file mode 100644 index 0361db3..0000000 --- a/lib/withSession.js +++ /dev/null @@ -1,14 +0,0 @@ -// this file is a wrapper with defaults to be used in both API routes and `getServerSideProps` functions -import {withIronSession} from 'next-iron-session' - -export default function withSession(handler) { - return withIronSession(handler, { - password: process.env.MYAPPS_SECRET_COOKIE_PASSWORD, - cookieName: 'myapps_session', - cookieOptions: { - // the next line allows to use the session in non-https environments like - // Next.js dev mode (http://localhost:3000) - secure: process.env.NODE_ENV === 'production' ? true : false, - }, - }) -} 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}) { }, }} > - - - + + + + + + + ) } 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 ( ) @@ -44,39 +39,37 @@ const Home = () => { } return ( - - - <> - { - Object.entries(appList).filter(a => a[1].icon).map(a => ( -
    handleClick(e, {appName: a[0], ...a[1]})} - onKeyDown={e => handleKey(e, {appName: a[0], ...a[1]})} - tabIndex="0" - > - {`${a[0]} -

    {a[0]}

    -
    - )) - } - {apps && apps.map(app => { - const AppComponent = appList[app.name].component + + <> + { + Object.entries(appList).filter(a => a[1].icon).map(a => ( +
    handleClick(e, {appName: a[0], ...a[1]})} + onKeyDown={e => handleKey(e, {appName: a[0], ...a[1]})} + tabIndex="0" + > + {`${a[0]} +

    {t(a[0])}

    +
    + )) + } + {apps && apps.map(app => { + const AppComponent = appList[app.name].component - return ( - - - - ); - })} - -
    -
    + return ( + + + + ); + })} + + ) } 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 ( -
    + {loading + ? + : ( + + )}
    ) } 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 ( - + {loading + ? + : ( + + )} ) } 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 ( - {!user + {!user || loading ? ( -
    - -
    + ) : (
    -

    One last step missing

    -

    {`To start using Notes App type the verification code we sent to your email (${user.email}):`}

    +

    {t('verification_title')}

    +

    {`${t('verification_text')} ${user.email}`}

    - - + + { sending ? ( -

    Sending...

    +

    {t('sending')}

    ) : ( resent ? ( -

    Mail was successfully sent again, check your mailbox!

    +

    {t('verification_sent_again')}

    ) : ( -

    If you didn't get verification email  - send it again. +

    {t('verification_not_received')}  + {t('verification_send_again')}.

    ) ) 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; -- cgit v1.2.3
    sortBy(1)}>Title {sortedBy(1)} sortBy(2)}>Created {sortedBy(2)} sortBy(3)}>Modified {sortedBy(3)} sortBy(1)}>{t('title')} {sortedBy(1)} sortBy(2)}>{t('created')} {sortedBy(2)} sortBy(3)}>{t('modified')} {sortedBy(3)}
    Your notes list is empty.{t('notes_list_empty')}