diff --git a/app/auth/page.tsx b/app/auth/page.tsx new file mode 100644 index 0000000..260389e --- /dev/null +++ b/app/auth/page.tsx @@ -0,0 +1,89 @@ +'use client'; +import React, { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { Button, Layout, Input, Form, theme } from 'antd'; +import Title from 'antd/es/typography/Title'; +import { request } from '@/utils'; + +const Auth: React.FC = () => { + const [email, setEmail] = useState(''); + const [submitted, setSubmitted] = useState(false); + const router = useRouter(); + const { + token: { colorBgContainer, borderRadiusLG }, + } = theme.useToken(); + + const onFinish = async (values: any) => { + await request({ + path: '/users/auth', + method: 'POST', + data: values, + }); + + setEmail(values.email); + setSubmitted(true); + }; + + return ( + +
+ {submitted ? ( + <> + Check your inbox. +

+ Click the link we sent to {email} to sign in. +

+ + + ) : ( + <> + Sign in with email +

+ Enter the email address and we’ll send a magic link to your inbox. +

+
+ + + + +
+ + )} +
+
+ ); +}; + +export default Auth; diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index 2f57aa9..3135539 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -1,75 +1,29 @@ 'use client'; -import React, { useState } from 'react'; -import { - DesktopOutlined, - FileOutlined, - PieChartOutlined, - TeamOutlined, - UserOutlined, -} from '@ant-design/icons'; -import type { MenuProps } from 'antd'; -import { Breadcrumb, Layout, Menu, theme } from 'antd'; +import React from 'react'; -const { Header, Content, Footer, Sider } = Layout; - -type MenuItem = Required['items'][number]; +import { Breadcrumb, Layout, theme } from 'antd'; +import Profile from '@/components/Profile'; -function getItem( - label: React.ReactNode, - key: React.Key, - icon?: React.ReactNode, - children?: MenuItem[] -): MenuItem { - return { - key, - icon, - children, - label, - } as MenuItem; -} - -const items: MenuItem[] = [ - getItem('Option 1', '1', ), - getItem('Option 2', '2', ), - getItem('User', 'sub1', , [ - getItem('Tom', '3'), - getItem('Bill', '4'), - getItem('Alex', '5'), - ]), - getItem('Team', 'sub2', , [ - getItem('Team 1', '6'), - getItem('Team 2', '8'), - ]), - getItem('Files', '9', ), -]; +const { Header, Content, Footer, Sider } = Layout; const Dashboard: React.FC = () => { - const [collapsed, setCollapsed] = useState(false); const { token: { colorBgContainer, borderRadiusLG }, } = theme.useToken(); return ( - setCollapsed(value)} - > +
- -
+
+ +
User - Bill + Sites
{ borderRadius: borderRadiusLG, }} > - Bill is a cat. + No added site yet.
diff --git a/app/layout.tsx b/app/layout.tsx index a61ce4e..4373466 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,4 +1,5 @@ import type { Metadata } from 'next'; + import { AntdRegistry } from '@ant-design/nextjs-registry'; import './globals.css'; diff --git a/app/page.tsx b/app/page.tsx index 18b0d13..fbc01a6 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,4 +1,4 @@ -import Builder from '@/components/Builder'; +import Example from '@/components/Example'; import { Metadata } from 'next'; import Title from 'antd/es/typography/Title'; import { Tag } from 'antd'; @@ -12,7 +12,7 @@ export const metadata: Metadata = { export default function Home() { return ( -
+
</div> - <Builder /> + <Example lng="en" theme="light" /> </main> ); } diff --git a/components/Builder.tsx b/components/Builder.tsx deleted file mode 100644 index 9550b20..0000000 --- a/components/Builder.tsx +++ /dev/null @@ -1,70 +0,0 @@ -'use client'; - -import { useState } from 'react'; -import Example from './Example'; -import { Select, Button } from 'antd'; - -export default function Builder() { - const [theme, setTheme] = useState('light'); - const [lng, setLng] = useState('en'); - - return ( - <> - <div className="px-5"> - <pre className=" overflow-auto mb-10 text-sm text-left items-center bg-gray-800 text-white rounded-md px-2 w-full"> - {` - <div - id="zoomment" - data-theme="${theme}" - data-language="${lng}" - data-emotions="❤️,😀,🪄,🥸,💡,🤔,💩,😢" - ></div> - <script src='https://cdn.zoomment.com/zoomment.min.js'></script> - `} - </pre> - - <Example lng={lng} theme={theme} key={lng + theme} /> - - <div className=" px-4"> - {/* <div className="mb-5"> - <label - htmlFor="theme" - className="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-400" - > - Select Theme - </label> - <Select - id="theme" - value={theme} - onChange={(value) => setTheme(value)} - options={[ - { value: 'light', label: 'Light' }, - { value: 'dark', label: 'Dark' }, - ]} - /> - </div> - <div className="mb-5"> - <label - htmlFor="language" - className="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-400" - > - Select Language - </label> - <Select - id="language" - value={lng} - onChange={(value) => setLng(value)} - options={[ - { value: 'en', label: 'English' }, - { value: 'hy', label: 'Armenian' }, - { value: 'hyw', label: 'Armenian (classic)' }, - { value: 'ru', label: 'Russian' }, - { value: 'zh', label: 'Simplified Chinese' }, - ]} - /> - </div> */} - </div> - </div> - </> - ); -} diff --git a/components/Example.tsx b/components/Example.tsx deleted file mode 100644 index 876ea08..0000000 --- a/components/Example.tsx +++ /dev/null @@ -1,46 +0,0 @@ -'use client'; -import Script from 'next/script'; - -export default function Example({ - lng, - theme, -}: { - lng: string; - theme: string; -}) { - return ( - // <iframe - // key={lng + theme} - // style={{ height: '400px' }} - // className="w-full" - // srcDoc={` - // <html> - // <head> - // <link rel="preconnect" href="https://fonts.googleapis.com"> - // <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> - // <link href="https://fonts.googleapis.com/css2?family=Inter:wght@100&family=Noto+Sans:ital@0;1&display=swap" rel="stylesheet"> - // <style> - // * { - // font-family: 'Noto Sans'; - // } - // </style> - // </head> - // <body> - - // </body> - // `} - // frameBorder="0" - // ></iframe> - - <> - <div - id="zoomment" - className="w-full" - data-theme={theme} - data-language={lng} - data-emotions="❤️,😀,🪄,🥸,💡,🤔,💩,😢" - ></div> - <Script src="https://cdn.zoomment.com/zoomment.min.js"></Script> - </> - ); -} diff --git a/components/Example/index.tsx b/components/Example/index.tsx new file mode 100644 index 0000000..6f42241 --- /dev/null +++ b/components/Example/index.tsx @@ -0,0 +1,35 @@ +'use client'; +import Script from 'next/script'; + +export default function Example({ + lng, + theme, +}: { + lng: string; + theme: string; +}) { + return ( + <div className="px-5"> + <pre className=" overflow-auto mb-10 text-sm text-left items-center bg-gray-800 text-white rounded-md px-2 w-full"> + {` + <div + id="zoomment" + data-theme="${theme}" + data-language="${lng}" + data-emotions="❤️,😀,🪄,🥸,💡,🤔,💩,😢" + ></div> + <script src='https://cdn.zoomment.com/zoomment.min.js'></script> + `} + </pre> + + <div + id="zoomment" + className="w-full" + data-theme={theme} + data-language={lng} + data-emotions="❤️,😀,🪄,🥸,💡,🤔,💩,😢" + ></div> + <Script src="https://cdn.zoomment.com/zoomment.min.js"></Script> + </div> + ); +} diff --git a/components/Profile/index.tsx b/components/Profile/index.tsx new file mode 100644 index 0000000..28af40d --- /dev/null +++ b/components/Profile/index.tsx @@ -0,0 +1,34 @@ +'use client'; +import { Avatar, Dropdown } from 'antd'; +import { useCookies } from 'react-cookie'; +import { useRouter } from 'next/navigation'; +import { UserOutlined } from '@ant-design/icons'; + +export default function Profile() { + const [cookies, setCookies] = useCookies(['token']); + const router = useRouter(); + + const onLogout = () => { + setCookies('token', ''); + router.refresh(); + }; + + return ( + <div className=""> + <Dropdown + menu={{ + items: [ + { + key: '4', + danger: true, + onClick: onLogout, + label: 'Logout', + }, + ], + }} + > + <Avatar size={28} icon={<UserOutlined />} /> + </Dropdown> + </div> + ); +} diff --git a/middleware.ts b/middleware.ts new file mode 100644 index 0000000..d9641c7 --- /dev/null +++ b/middleware.ts @@ -0,0 +1,30 @@ +import { NextRequest, NextResponse } from 'next/server'; + +export function middleware(request: NextRequest) { + const token = request.cookies.get('token')?.value; + const url = request.nextUrl; + const newToken = url.searchParams.get('token'); + + //TODO add token validation + + if (newToken) { + let res = NextResponse.redirect(new URL('/dashboard', url)); + + url.searchParams.delete('token'); + res.cookies.set('token', newToken); + + return res; + } + + if (token && url.pathname.startsWith('/auth')) { + return Response.redirect(new URL('/dashboard', url)); + } + + if (!token && url.pathname.startsWith('/dashboard')) { + return Response.redirect(new URL('/auth', url)); + } +} + +export const config = { + matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'], +}; diff --git a/next.config.mjs b/next.config.mjs index bf221d3..4678774 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,6 +1,4 @@ /** @type {import('next').NextConfig} */ -const nextConfig = { - output: 'export', -}; +const nextConfig = {}; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index 583a09f..844d74e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "antd": "^5.14.2", "next": "14.1.0", "react": "^18", + "react-cookie": "^7.1.0", "react-dom": "^18" }, "devDependencies": { @@ -654,6 +655,20 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==" + }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz", + "integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==", + "dependencies": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -672,14 +687,12 @@ "node_modules/@types/prop-types": { "version": "15.7.11", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", - "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==", - "dev": true + "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==" }, "node_modules/@types/react": { "version": "18.2.59", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.59.tgz", "integrity": "sha512-DE+F6BYEC8VtajY85Qr7mmhTd/79rJKIHCg99MU9SWPB4xvLb6D1za2vYflgZfmPqQVEr6UqJTnLXEwzpVPuOg==", - "dev": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -698,8 +711,7 @@ "node_modules/@types/scheduler": { "version": "0.16.8", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", - "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", - "dev": true + "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==" }, "node_modules/@typescript-eslint/parser": { "version": "6.21.0", @@ -1481,6 +1493,14 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/copy-to-clipboard": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", @@ -2712,6 +2732,14 @@ "node": ">= 0.4" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, "node_modules/ignore": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", @@ -4614,6 +4642,19 @@ "node": ">=0.10.0" } }, + "node_modules/react-cookie": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-7.1.0.tgz", + "integrity": "sha512-n2+Gt07/xxuShXary+SImk1sw5l7a1UguQOQEN55YewEW5LoA0opbR4nbeo8sY6OYwR37iCFJtqJ0AGEywqAtg==", + "dependencies": { + "@types/hoist-non-react-statics": "^3.3.5", + "hoist-non-react-statics": "^3.3.2", + "universal-cookie": "^7.0.0" + }, + "peerDependencies": { + "react": ">= 16.3.0" + } + }, "node_modules/react-dom": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", @@ -4629,8 +4670,7 @@ "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/read-cache": { "version": "1.0.0", @@ -5509,6 +5549,15 @@ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "dev": true }, + "node_modules/universal-cookie": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-7.1.0.tgz", + "integrity": "sha512-LCLHwP0whxTqkBYMptW1dzNS0xxIVJmU6c51N5CfPNheVxuJW7fVxPa6MUGX7boUSyOlpMveBO96hMs5Gee6Fg==", + "dependencies": { + "@types/cookie": "^0.6.0", + "cookie": "^0.6.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.0.13", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", diff --git a/package.json b/package.json index 0d86644..28d2567 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "antd": "^5.14.2", "next": "14.1.0", "react": "^18", + "react-cookie": "^7.1.0", "react-dom": "^18" }, "devDependencies": { diff --git a/utils/index.ts b/utils/index.ts new file mode 100644 index 0000000..56e4b05 --- /dev/null +++ b/utils/index.ts @@ -0,0 +1 @@ +export * from './request'; diff --git a/utils/request.ts b/utils/request.ts new file mode 100644 index 0000000..2bc8632 --- /dev/null +++ b/utils/request.ts @@ -0,0 +1,22 @@ +export const request = async ({ + path = '', + method = 'GET', + data = null, + headers = {}, +}: { + path?: string; + method?: string; + data?: object | null; + headers?: object; +}) => { + const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}${path}`, { + method: method, + body: JSON.stringify(data), + headers: { + 'Content-Type': 'application/json', + ...headers, + }, + }); + + return response.json(); +};