diff --git a/components/Account.js b/components/Account.js index 7b9e0dda..08ebe815 100644 --- a/components/Account.js +++ b/components/Account.js @@ -1,6 +1,6 @@ import React from 'react'; import { useSelector } from 'react-redux'; -import Page from './Page'; +import Page from './layout/Page'; import styles from './Account.module.css'; export default function Account() { diff --git a/components/Footer.js b/components/Footer.js deleted file mode 100644 index 47f9d4d9..00000000 --- a/components/Footer.js +++ /dev/null @@ -1,5 +0,0 @@ -import React from 'react'; - -export default function Footer() { - return ; -} diff --git a/components/Settings.js b/components/Settings.js index 23e218b5..267f3d60 100644 --- a/components/Settings.js +++ b/components/Settings.js @@ -1,30 +1,36 @@ import React, { useState, useEffect } from 'react'; -import Page from './Page'; -import Table from './Table'; -import Button from './Button'; -import Icon from './Icon'; -import PageHeader from './PageHeader'; +import Page from './layout/Page'; +import Table from './common/Table'; +import Button from './interface/Button'; +import PageHeader from './layout/PageHeader'; import Pen from 'assets/pen.svg'; import Trash from 'assets/trash.svg'; import Plus from 'assets/plus.svg'; import { get } from 'lib/web'; +import Modal from './common/Modal'; +import WebsiteForm from './forms/WebsiteForm'; +import styles from './Settings.module.css'; export default function Settings() { const [data, setData] = useState(); + const [edit, setEdit] = useState(); + const [del, setDelete] = useState(); + const [saved, setSaved] = useState(0); const columns = [ { key: 'name', label: 'Name' }, { key: 'domain', label: 'Domain' }, { key: 'action', - label: '', - style: { flex: 0 }, - render: ({ website_id }) => ( + cell: { + className: styles.buttons, + }, + render: row => ( <> - - @@ -32,13 +38,23 @@ export default function Settings() { }, ]; + function handleSave() { + setSaved(state => state + 1); + handleClose(); + } + + function handleClose() { + setEdit(null); + setDelete(null); + } + async function loadData() { setData(await get(`/api/website`)); } useEffect(() => { loadData(); - }, []); + }, [saved]); if (!data) { return null; @@ -47,13 +63,17 @@ export default function Settings() { return ( -
Settings
-
- +
+ {edit && ( + + + + )} ); } diff --git a/components/Settings.module.css b/components/Settings.module.css new file mode 100644 index 00000000..91af3246 --- /dev/null +++ b/components/Settings.module.css @@ -0,0 +1,4 @@ +.buttons { + display: flex; + justify-content: flex-end; +} diff --git a/components/WebsiteDetails.js b/components/WebsiteDetails.js index 96ed4c74..a5c27f35 100644 --- a/components/WebsiteDetails.js +++ b/components/WebsiteDetails.js @@ -1,14 +1,14 @@ import React, { useEffect, useState } from 'react'; import classNames from 'classnames'; -import WebsiteChart from './WebsiteChart'; -import RankingsChart from './RankingsChart'; -import WorldMap from './WorldMap'; -import Page from './Page'; +import WebsiteChart from './charts/WebsiteChart'; +import RankingsChart from './charts/RankingsChart'; +import WorldMap from './common/WorldMap'; +import Page from './layout/Page'; import { getDateRange } from 'lib/date'; import { get } from 'lib/web'; import { browserFilter, urlFilter, refFilter, deviceFilter, countryFilter } from 'lib/filters'; import styles from './WebsiteDetails.module.css'; -import PageHeader from './PageHeader'; +import PageHeader from './layout/PageHeader'; const pageviewClasses = 'col-md-12 col-lg-6'; const sessionClasses = 'col-12 col-lg-4'; diff --git a/components/WebsiteList.js b/components/WebsiteList.js index 8f449ee7..0e1dfd89 100644 --- a/components/WebsiteList.js +++ b/components/WebsiteList.js @@ -1,12 +1,12 @@ import React, { useState, useEffect } from 'react'; import { useRouter } from 'next/router'; import { get } from 'lib/web'; -import Link from './Link'; -import WebsiteChart from './WebsiteChart'; -import Page from './Page'; -import Icon from './Icon'; -import Button from './Button'; -import PageHeader from './PageHeader'; +import Link from './interface/Link'; +import WebsiteChart from './charts/WebsiteChart'; +import Page from './layout/Page'; +import Icon from './interface/Icon'; +import Button from './interface/Button'; +import PageHeader from './layout/PageHeader'; import Arrow from 'assets/arrow-right.svg'; import styles from './WebsiteList.module.css'; @@ -24,32 +24,31 @@ export default function WebsiteList() { return ( - {data && - data.websites.map(({ website_id, name }) => ( -
- - - {name} - - - - -
- ))} + {data?.map(({ website_id, name }) => ( +
+ + + {name} + + + + +
+ ))}
); } diff --git a/components/MetricCard.js b/components/charts/MetricCard.js similarity index 100% rename from components/MetricCard.js rename to components/charts/MetricCard.js diff --git a/components/MetricCard.module.css b/components/charts/MetricCard.module.css similarity index 100% rename from components/MetricCard.module.css rename to components/charts/MetricCard.module.css diff --git a/components/MetricsBar.js b/components/charts/MetricsBar.js similarity index 100% rename from components/MetricsBar.js rename to components/charts/MetricsBar.js diff --git a/components/MetricsBar.module.css b/components/charts/MetricsBar.module.css similarity index 100% rename from components/MetricsBar.module.css rename to components/charts/MetricsBar.module.css diff --git a/components/PageviewsChart.js b/components/charts/PageviewsChart.js similarity index 100% rename from components/PageviewsChart.js rename to components/charts/PageviewsChart.js diff --git a/components/PageviewsChart.module.css b/components/charts/PageviewsChart.module.css similarity index 100% rename from components/PageviewsChart.module.css rename to components/charts/PageviewsChart.module.css diff --git a/components/QuickButtons.js b/components/charts/QuickButtons.js similarity index 94% rename from components/QuickButtons.js rename to components/charts/QuickButtons.js index de5722e6..831a2c05 100644 --- a/components/QuickButtons.js +++ b/components/charts/QuickButtons.js @@ -1,6 +1,6 @@ import React from 'react'; import classNames from 'classnames'; -import Button from './Button'; +import Button from '../interface/Button'; import { getDateRange } from 'lib/date'; import styles from './QuickButtons.module.css'; diff --git a/components/QuickButtons.module.css b/components/charts/QuickButtons.module.css similarity index 100% rename from components/QuickButtons.module.css rename to components/charts/QuickButtons.module.css diff --git a/components/RankingsChart.js b/components/charts/RankingsChart.js similarity index 97% rename from components/RankingsChart.js rename to components/charts/RankingsChart.js index 6a86dbbc..85acefeb 100644 --- a/components/RankingsChart.js +++ b/components/charts/RankingsChart.js @@ -1,7 +1,7 @@ import React, { useState, useEffect, useMemo } from 'react'; import { useSpring, animated, config } from 'react-spring'; import classNames from 'classnames'; -import CheckVisible from './CheckVisible'; +import CheckVisible from '../helpers/CheckVisible'; import { get } from 'lib/web'; import { percentFilter } from 'lib/filters'; import styles from './RankingsChart.module.css'; diff --git a/components/RankingsChart.module.css b/components/charts/RankingsChart.module.css similarity index 100% rename from components/RankingsChart.module.css rename to components/charts/RankingsChart.module.css diff --git a/components/WebsiteChart.js b/components/charts/WebsiteChart.js similarity index 93% rename from components/WebsiteChart.js rename to components/charts/WebsiteChart.js index 5779258a..837b7548 100644 --- a/components/WebsiteChart.js +++ b/components/charts/WebsiteChart.js @@ -1,11 +1,11 @@ import React, { useState, useEffect, useMemo, useRef } from 'react'; import classNames from 'classnames'; import PageviewsChart from './PageviewsChart'; -import CheckVisible from './CheckVisible'; +import CheckVisible from '../helpers/CheckVisible'; import MetricsBar from './MetricsBar'; import QuickButtons from './QuickButtons'; -import DateFilter from './DateFilter'; -import StickyHeader from './StickyHeader'; +import DateFilter from '../common/DateFilter'; +import StickyHeader from '../helpers/StickyHeader'; import { get } from 'lib/web'; import { getDateArray, getDateRange, getTimezone } from 'lib/date'; import styles from './WebsiteChart.module.css'; diff --git a/components/WebsiteChart.module.css b/components/charts/WebsiteChart.module.css similarity index 100% rename from components/WebsiteChart.module.css rename to components/charts/WebsiteChart.module.css diff --git a/components/DateFilter.js b/components/common/DateFilter.js similarity index 100% rename from components/DateFilter.js rename to components/common/DateFilter.js diff --git a/components/DropDown.js b/components/common/DropDown.js similarity index 93% rename from components/DropDown.js rename to components/common/DropDown.js index 9538d6f5..c3d6f852 100644 --- a/components/DropDown.js +++ b/components/common/DropDown.js @@ -1,10 +1,10 @@ import React, { useState, useRef } from 'react'; import classNames from 'classnames'; -import Menu from './Menu'; +import Menu from '../interface/Menu'; import useDocumentClick from 'hooks/useDocumentClick'; import Chevron from 'assets/chevron-down.svg'; import styles from './Dropdown.module.css'; -import Icon from './Icon'; +import Icon from '../interface/Icon'; export default function DropDown({ value, diff --git a/components/Dropdown.module.css b/components/common/Dropdown.module.css similarity index 100% rename from components/Dropdown.module.css rename to components/common/Dropdown.module.css diff --git a/components/common/Modal.js b/components/common/Modal.js new file mode 100644 index 00000000..c2bca43f --- /dev/null +++ b/components/common/Modal.js @@ -0,0 +1,16 @@ +import React from 'react'; +import { useSpring, animated } from 'react-spring'; +import styles from './Modal.module.css'; + +export default function Modal({ title, children }) { + const props = useSpring({ opacity: 1, from: { opacity: 0 } }); + + return ( + +
+ {title &&
{title}
} +
{children}
+
+
+ ); +} diff --git a/components/common/Modal.module.css b/components/common/Modal.module.css new file mode 100644 index 00000000..450b5820 --- /dev/null +++ b/components/common/Modal.module.css @@ -0,0 +1,45 @@ +.modal { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + margin: auto; +} + +.modal:before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + margin: auto; + background: var(--gray900); + opacity: 0.1; +} + +.content { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: var(--gray50); + min-width: 200px; + min-height: 100px; + z-index: 1; + border: 1px solid var(--gray300); + padding: 30px; + border-radius: 4px; + overflow: hidden; +} + +.header { + font-weight: 600; + margin-bottom: 20px; +} + +.body { + display: flex; + flex-direction: column; +} diff --git a/components/Table.js b/components/common/Table.js similarity index 64% rename from components/Table.js rename to components/common/Table.js index a910f5d1..169bfd22 100644 --- a/components/Table.js +++ b/components/common/Table.js @@ -6,8 +6,12 @@ export default function Table({ columns, rows }) { return (
- {columns.map(({ key, label }) => ( -
+ {columns.map(({ key, label, header }) => ( +
{label}
))} @@ -15,11 +19,11 @@ export default function Table({ columns, rows }) {
{rows.map((row, rowIndex) => (
- {columns.map(({ key, render, className, style }) => ( + {columns.map(({ key, render, cell }) => (
{render ? render(row) : row[key]}
diff --git a/components/Table.module.css b/components/common/Table.module.css similarity index 86% rename from components/Table.module.css rename to components/common/Table.module.css index 75b44feb..3e0f4cbb 100644 --- a/components/Table.module.css +++ b/components/common/Table.module.css @@ -14,6 +14,11 @@ flex: 1; } +.body { + display: flex; + flex-direction: column; +} + .row { display: flex; border-bottom: 1px solid var(--gray300); diff --git a/components/WorldMap.js b/components/common/WorldMap.js similarity index 100% rename from components/WorldMap.js rename to components/common/WorldMap.js diff --git a/components/WorldMap.module.css b/components/common/WorldMap.module.css similarity index 100% rename from components/WorldMap.module.css rename to components/common/WorldMap.module.css diff --git a/components/Login.js b/components/forms/LoginForm.js similarity index 91% rename from components/Login.js rename to components/forms/LoginForm.js index d2da76d8..395319bb 100644 --- a/components/Login.js +++ b/components/forms/LoginForm.js @@ -2,9 +2,9 @@ import React, { useState } from 'react'; import { Formik, Form, Field } from 'formik'; import Router from 'next/router'; import { post } from 'lib/web'; -import Button from './Button'; -import FormLayout, { FormButtons, FormError, FormMessage, FormRow } from './FormLayout'; -import styles from './Login.module.css'; +import Button from '../interface/Button'; +import FormLayout, { FormButtons, FormError, FormMessage, FormRow } from '../layout/FormLayout'; +import styles from './LoginForm.module.css'; const validate = ({ username, password }) => { const errors = {}; @@ -19,7 +19,7 @@ const validate = ({ username, password }) => { return errors; }; -export default function Login() { +export default function LoginForm() { const [message, setMessage] = useState(); const handleSubmit = async ({ username, password }) => { diff --git a/components/Login.module.css b/components/forms/LoginForm.module.css similarity index 100% rename from components/Login.module.css rename to components/forms/LoginForm.module.css diff --git a/components/forms/WebsiteForm.js b/components/forms/WebsiteForm.js new file mode 100644 index 00000000..b131b93c --- /dev/null +++ b/components/forms/WebsiteForm.js @@ -0,0 +1,64 @@ +import React, { useState } from 'react'; +import { Formik, Form, Field } from 'formik'; +import Router from 'next/router'; +import { post } from 'lib/web'; +import Button from 'components/interface/Button'; +import FormLayout, { + FormButtons, + FormError, + FormMessage, + FormRow, +} from 'components/layout/FormLayout'; + +const validate = ({ name, domain }) => { + const errors = {}; + + if (!name) { + errors.name = 'Required'; + } + if (!domain) { + errors.domain = 'Required'; + } + + return errors; +}; + +export default function WebsiteForm({ initialValues, onSave, onClose }) { + const [message, setMessage] = useState(); + + const handleSubmit = async values => { + const response = await post(`/api/website`, values); + + if (response) { + onSave(); + } else { + setMessage('Something went wrong.'); + } + }; + + return ( + + + {() => ( +
+ + + + + + + + + + + + + + + {message} + + )} +
+
+ ); +} diff --git a/components/CheckVisible.js b/components/helpers/CheckVisible.js similarity index 100% rename from components/CheckVisible.js rename to components/helpers/CheckVisible.js diff --git a/components/StickyHeader.js b/components/helpers/StickyHeader.js similarity index 100% rename from components/StickyHeader.js rename to components/helpers/StickyHeader.js diff --git a/components/Button.js b/components/interface/Button.js similarity index 100% rename from components/Button.js rename to components/interface/Button.js diff --git a/components/Button.module.css b/components/interface/Button.module.css similarity index 100% rename from components/Button.module.css rename to components/interface/Button.module.css diff --git a/components/Icon.js b/components/interface/Icon.js similarity index 100% rename from components/Icon.js rename to components/interface/Icon.js diff --git a/components/Icon.module.css b/components/interface/Icon.module.css similarity index 100% rename from components/Icon.module.css rename to components/interface/Icon.module.css diff --git a/components/Link.js b/components/interface/Link.js similarity index 100% rename from components/Link.js rename to components/interface/Link.js diff --git a/components/Link.module.css b/components/interface/Link.module.css similarity index 100% rename from components/Link.module.css rename to components/interface/Link.module.css diff --git a/components/Menu.js b/components/interface/Menu.js similarity index 100% rename from components/Menu.js rename to components/interface/Menu.js diff --git a/components/Menu.module.css b/components/interface/Menu.module.css similarity index 100% rename from components/Menu.module.css rename to components/interface/Menu.module.css diff --git a/components/UserButton.js b/components/interface/UserButton.js similarity index 100% rename from components/UserButton.js rename to components/interface/UserButton.js diff --git a/components/UserButton.module.css b/components/interface/UserButton.module.css similarity index 100% rename from components/UserButton.module.css rename to components/interface/UserButton.module.css diff --git a/components/layout/Footer.js b/components/layout/Footer.js new file mode 100644 index 00000000..c534e685 --- /dev/null +++ b/components/layout/Footer.js @@ -0,0 +1,11 @@ +import React from 'react'; +import classNames from 'classnames'; +import styles from './Footer.module.css'; + +export default function Footer() { + return ( +
+ umami - deliciously simple web stats +
+ ); +} diff --git a/components/layout/Footer.module.css b/components/layout/Footer.module.css new file mode 100644 index 00000000..92bc10c1 --- /dev/null +++ b/components/layout/Footer.module.css @@ -0,0 +1,3 @@ +.footer { + font-size: var(--font-size-small); +} diff --git a/components/FormLayout.js b/components/layout/FormLayout.js similarity index 100% rename from components/FormLayout.js rename to components/layout/FormLayout.js diff --git a/components/FormLayout.module.css b/components/layout/FormLayout.module.css similarity index 74% rename from components/FormLayout.module.css rename to components/layout/FormLayout.module.css index 85c61ac8..9bf5a3de 100644 --- a/components/FormLayout.module.css +++ b/components/layout/FormLayout.module.css @@ -33,11 +33,24 @@ top: 0; left: 100%; bottom: 0; - margin-left: 10px; - padding: 4px 8px; + margin-left: 16px; + padding: 4px 10px; border-radius: 4px; } +.error:after { + content: ''; + position: absolute; + top: 0; + left: -5px; + bottom: 0; + margin: auto; + width: 10px; + height: 10px; + background: var(--color-error); + transform: rotate(45deg); +} + .message { text-align: center; margin: 20px 0; diff --git a/components/Header.js b/components/layout/Header.js similarity index 87% rename from components/Header.js rename to components/layout/Header.js index 1fc62eed..193c3312 100644 --- a/components/Header.js +++ b/components/layout/Header.js @@ -1,9 +1,9 @@ import React from 'react'; import { useSelector } from 'react-redux'; import classNames from 'classnames'; -import Link from 'components/Link'; -import UserButton from './UserButton'; -import Icon from './Icon'; +import Link from 'components/interface/Link'; +import UserButton from '../interface/UserButton'; +import Icon from '../interface/Icon'; import Logo from 'assets/logo.svg'; import styles from './Header.module.css'; diff --git a/components/Header.module.css b/components/layout/Header.module.css similarity index 100% rename from components/Header.module.css rename to components/layout/Header.module.css diff --git a/components/Layout.js b/components/layout/Layout.js similarity index 89% rename from components/Layout.js rename to components/layout/Layout.js index a2d45cc1..91591ccc 100644 --- a/components/Layout.js +++ b/components/layout/Layout.js @@ -1,8 +1,8 @@ import React from 'react'; import classNames from 'classnames'; import Head from 'next/head'; -import Header from 'components/Header'; -import Footer from 'components/Footer'; +import Header from 'components/layout/Header'; +import Footer from 'components/layout/Footer'; import styles from './Layout.module.css'; export default function Layout({ diff --git a/components/Layout.module.css b/components/layout/Layout.module.css similarity index 100% rename from components/Layout.module.css rename to components/layout/Layout.module.css diff --git a/components/Page.js b/components/layout/Page.js similarity index 100% rename from components/Page.js rename to components/layout/Page.js diff --git a/components/Page.module.css b/components/layout/Page.module.css similarity index 100% rename from components/Page.module.css rename to components/layout/Page.module.css diff --git a/components/PageHeader.js b/components/layout/PageHeader.js similarity index 100% rename from components/PageHeader.js rename to components/layout/PageHeader.js diff --git a/components/PageHeader.module.css b/components/layout/PageHeader.module.css similarity index 100% rename from components/PageHeader.module.css rename to components/layout/PageHeader.module.css diff --git a/lib/db.js b/lib/db.js index 8f20b0b5..b047b866 100644 --- a/lib/db.js +++ b/lib/db.js @@ -56,6 +56,20 @@ export async function getWebsites(user_id) { where: { user_id, }, + orderBy: { + name: 'asc', + }, + }), + ); +} + +export async function updateWebsite(website_id, data) { + return runQuery( + prisma.website.update({ + where: { + website_id, + }, + data, }), ); } diff --git a/pages/404.js b/pages/404.js index 5e7a2938..3c0c232e 100644 --- a/pages/404.js +++ b/pages/404.js @@ -1,5 +1,5 @@ import React from 'react'; -import Layout from 'components/Layout'; +import Layout from 'components/layout/Layout'; export default function Custom404() { return ( diff --git a/pages/account.js b/pages/account.js index 0b56dfdc..d97b6325 100644 --- a/pages/account.js +++ b/pages/account.js @@ -1,5 +1,5 @@ import React from 'react'; -import Layout from 'components/Layout'; +import Layout from 'components/layout/Layout'; import Account from 'components/Account'; import useRequireLogin from 'hooks/useRequireLogin'; diff --git a/pages/api/website.js b/pages/api/website.js index cb8f840a..e308c65e 100644 --- a/pages/api/website.js +++ b/pages/api/website.js @@ -1,12 +1,26 @@ -import { getWebsites } from 'lib/db'; +import { getWebsites, updateWebsite } from 'lib/db'; import { useAuth } from 'lib/middleware'; export default async (req, res) => { await useAuth(req, res); const { user_id } = req.auth; + const { website_id } = req.body; - const websites = await getWebsites(user_id); + if (req.method === 'GET') { + const websites = await getWebsites(user_id); - return res.status(200).json({ websites }); + return res.status(200).json(websites); + } + + if (req.method === 'POST') { + if (website_id) { + const { name, domain } = req.body; + const website = await updateWebsite(website_id, { name, domain }); + + return res.status(200).json(website); + } + } + + return res.status(405).end(); }; diff --git a/pages/dashboard.js b/pages/dashboard.js index 63c5be49..aeb9d772 100644 --- a/pages/dashboard.js +++ b/pages/dashboard.js @@ -1,5 +1,5 @@ import React from 'react'; -import Layout from 'components/Layout'; +import Layout from 'components/layout/Layout'; import WebsiteList from 'components/WebsiteList'; import useRequireLogin from 'hooks/useRequireLogin'; diff --git a/pages/login.js b/pages/login.js index 7a91fcba..1d3f2996 100644 --- a/pages/login.js +++ b/pages/login.js @@ -1,14 +1,14 @@ import React from 'react'; -import Layout from 'components/Layout'; -import Login from 'components/Login'; -import Icon from 'components/Icon'; +import Layout from 'components/layout/Layout'; +import LoginForm from 'components/forms/LoginForm'; +import Icon from 'components/interface/Icon'; import Logo from 'assets/logo.svg'; export default function LoginPage() { return ( } size="XL" /> - + ); } diff --git a/pages/settings.js b/pages/settings.js index 6cf411f1..f244529b 100644 --- a/pages/settings.js +++ b/pages/settings.js @@ -1,5 +1,5 @@ import React from 'react'; -import Layout from 'components/Layout'; +import Layout from 'components/layout/Layout'; import Settings from 'components/Settings'; import useRequireLogin from 'hooks/useRequireLogin'; diff --git a/pages/test.js b/pages/test.js index 16bd8b61..3dcc834c 100644 --- a/pages/test.js +++ b/pages/test.js @@ -1,6 +1,6 @@ import Head from 'next/head'; import Link from 'next/link'; -import Layout from 'components/Layout'; +import Layout from 'components/layout/Layout'; export default function Test({ websiteId }) { return ( diff --git a/pages/website/[...id].js b/pages/website/[...id].js index 499f93f0..991d393a 100644 --- a/pages/website/[...id].js +++ b/pages/website/[...id].js @@ -1,6 +1,6 @@ import React from 'react'; import { useRouter } from 'next/router'; -import Layout from 'components/Layout'; +import Layout from 'components/layout/Layout'; import WebsiteDetails from 'components/WebsiteDetails'; import useRequireLogin from 'hooks/useRequireLogin';