diff --git a/components/LoginPage/LoginPage.module.scss b/components/LoginPage/LoginPage.module.scss index e69de29..e81e9e2 100644 --- a/components/LoginPage/LoginPage.module.scss +++ b/components/LoginPage/LoginPage.module.scss @@ -0,0 +1,33 @@ +.wrapper { + width: 100vw; + height: 100vh; + overflow: hidden; + + .loginBox { + max-width: 480px; + min-height: 240px; + gap: 2rem; + background-color: #fff; + margin: 0 auto; + + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + margin-top: 4%; + + h1 { + margin-top: 0; + text-align: center; + } + + .loginButton { + display: flex; + padding: 1rem; + background: #00aaff; + color: #000; + font-weight: bold; + border-radius: 8px; + } + } +} diff --git a/components/LoginPage/LoginPage.tsx b/components/LoginPage/LoginPage.tsx index faad930..808062b 100644 --- a/components/LoginPage/LoginPage.tsx +++ b/components/LoginPage/LoginPage.tsx @@ -1,10 +1,15 @@ import Link from 'next/link'; +import styles from './LoginPage.module.scss'; export const LoginPage = () => { return ( - <> -

Log in

- Log in with Icy Network - +
+
+

Icy Network Administration

+ + Log in with Icy Network + +
+
); }; diff --git a/components/OAuth2Page/OAuth2Page.module.scss b/components/OAuth2Page/OAuth2Page.module.scss new file mode 100644 index 0000000..f531465 --- /dev/null +++ b/components/OAuth2Page/OAuth2Page.module.scss @@ -0,0 +1,29 @@ +.clientCard { + display: flex; + flex-direction: row; + gap: 1rem; + padding: 0.5rem; + box-shadow: 0 0 8px rgba(0, 0, 0, 0.25); + .pictureWrapper { + overflow: hidden; + } + + .clientInfo { + display: flex; + flex-direction: column; + + h2 { + padding: 0; + margin: 0; + .clientname { + font-size: 0.875rem; + } + } + } +} + +.clientList { + display: flex; + flex-direction: column; + gap: 1rem; +} diff --git a/components/OAuth2Page/OAuth2Page.tsx b/components/OAuth2Page/OAuth2Page.tsx new file mode 100644 index 0000000..b8d16a9 --- /dev/null +++ b/components/OAuth2Page/OAuth2Page.tsx @@ -0,0 +1,120 @@ +import useSWR from 'swr'; +import Image from 'next/image'; +import { OAuth2ClientListItem } from '../../lib/types/oauth2-client.interface'; +import { PaginatedResponse } from '../../lib/types/paginated-response.interface'; +import { Paginator } from '../common/Paginator/Paginator'; +import styles from './OAuth2Page.module.scss'; +import { UPLOADS_URL } from '../../lib/constants'; +import application from '../../public/application.png'; +import { useState } from 'react'; +import useUser from '../../lib/hooks/useUser'; +import { Container } from '../common/Container/Container'; +import { Header } from '../common/Header/Header'; +import Modal from '../common/Modal/Modal/Modal'; +import ModalHeader from '../common/Modal/ModalHeader/ModalHeader'; +import ModalBody from '../common/Modal/ModalBody/ModalBody'; +import ModalFooter from '../common/Modal/ModalFooter/ModalFooter'; +import ModalService from '../common/Modal/services/ModalService'; + +const TestModal = ({ close }: { close: (...args: any[]) => void }) => { + return ( + + +

Test!

+
+ +

This is only a test

+
+ + + +
+ ); +}; + +const OAuth2ClientCard = ({ client }: { client: OAuth2ClientListItem }) => ( +
+
+ {client.picture ? ( + + ) : ( + + )} +
+
+

{client.title}

+ {client.description} +
+
Client ID
+
{client.client_id}
+
Allowed scopes
+
{client.scope?.split(' ').join(', ')}
+
Allowed grant types
+
{client.grants?.split(' ').join(', ')}
+
Activated
+
{client.activated ? 'Yes' : NOT ACTIVATED}
+
Verified
+
{client.verified ? 'Yes' : 'No'}
+
Created
+
{new Date(client.created_at).toDateString()}
+
+
+
+); + +const OAuth2ClientList = ({ + pageIndex, + searchTerm, + setPage, +}: { + pageIndex: number; + searchTerm: string; + setPage: (page: number) => void; +}) => { + const { data } = useSWR>( + `/api/admin/oauth2/clients?page=${pageIndex}${ + searchTerm ? `&q=${searchTerm}` : '' + }` + ); + + return data ? ( + <> + +
+ {data.list.map((client) => ( + + ))} + {data?.pagination && ( + + )} +
+ + ) : ( + Nothing found + ); +}; + +export const OAuth2Page = () => { + const { user } = useUser({ redirectTo: '/login' }); + const [pageIndex, setPageIndex] = useState(1); + const [searchTerm, setSearchTerm] = useState(''); + + return ( + <> +
+ +

OAuth2 clients

+ +
+ + ); +}; diff --git a/components/common/Header/Header.tsx b/components/common/Header/Header.tsx index d6d2e2f..fbfcf41 100644 --- a/components/common/Header/Header.tsx +++ b/components/common/Header/Header.tsx @@ -16,16 +16,16 @@ const navItems = [ title: 'Users', privileges: ['admin', 'admin:user'], }, - { - path: '/privileges', - title: 'Privileges', - privileges: ['admin', 'admin:user:privilege'], - }, { path: '/oauth2', title: 'OAuth2', privileges: [['admin', 'admin:oauth2'], 'self:oauth2'], }, + { + path: '/privileges', + title: 'Privileges', + privileges: ['admin', 'admin:user:privilege'], + }, { path: '/documents', title: 'Documents', diff --git a/components/common/Modal/Modal/Modal.module.scss b/components/common/Modal/Modal/Modal.module.scss new file mode 100644 index 0000000..fe28cb0 --- /dev/null +++ b/components/common/Modal/Modal/Modal.module.scss @@ -0,0 +1,32 @@ +.modal { + display: flex; + flex-direction: column; + max-width: 480px; + margin: 0 auto; + background-color: #fff; + margin-top: 8%; + + .header { + padding: 1rem; + border-bottom: 1px solid #ddd; + display: flex; + justify-content: space-between; + + h1, + h2, + h3 { + margin: 0; + } + } + + .body { + padding: 1rem; + } + + .footer { + padding: 1rem; + border-top: 1px solid #ddd; + display: flex; + justify-content: flex-end; + } +} diff --git a/components/common/Modal/Modal/Modal.tsx b/components/common/Modal/Modal/Modal.tsx new file mode 100644 index 0000000..6a11321 --- /dev/null +++ b/components/common/Modal/Modal/Modal.tsx @@ -0,0 +1,5 @@ +import styles from './Modal.module.scss'; + +export default function Modal({ children }: { children: JSX.Element[] }) { + return
{children}
; +} diff --git a/components/common/Modal/ModalBody/ModalBody.tsx b/components/common/Modal/ModalBody/ModalBody.tsx new file mode 100644 index 0000000..a60f008 --- /dev/null +++ b/components/common/Modal/ModalBody/ModalBody.tsx @@ -0,0 +1,9 @@ +import styles from '../Modal/Modal.module.scss'; + +export default function ModalBody({ + children, +}: { + children: JSX.Element | []; +}) { + return
{children}
; +} diff --git a/components/common/Modal/ModalFooter/ModalFooter.tsx b/components/common/Modal/ModalFooter/ModalFooter.tsx new file mode 100644 index 0000000..a3804d2 --- /dev/null +++ b/components/common/Modal/ModalFooter/ModalFooter.tsx @@ -0,0 +1,9 @@ +import styles from '../Modal/Modal.module.scss'; + +export default function ModalFooter({ + children, +}: { + children: JSX.Element | []; +}) { + return
{children}
; +} diff --git a/components/common/Modal/ModalHeader/ModalHeader.tsx b/components/common/Modal/ModalHeader/ModalHeader.tsx new file mode 100644 index 0000000..873f447 --- /dev/null +++ b/components/common/Modal/ModalHeader/ModalHeader.tsx @@ -0,0 +1,9 @@ +import styles from '../Modal/Modal.module.scss'; + +export default function ModalHeader({ + children, +}: { + children: JSX.Element | []; +}) { + return
{children}
; +} diff --git a/components/common/Modal/ModalRoot/ModalRoot.module.scss b/components/common/Modal/ModalRoot/ModalRoot.module.scss new file mode 100644 index 0000000..3a2982e --- /dev/null +++ b/components/common/Modal/ModalRoot/ModalRoot.module.scss @@ -0,0 +1,8 @@ +.modalRoot { + position: fixed; + top: 0; + z-index: 100; + width: 100vw; + height: 100vh; + background-color: rgba(1, 1, 1, 0.5); +} diff --git a/components/common/Modal/ModalRoot/ModalRoot.tsx b/components/common/Modal/ModalRoot/ModalRoot.tsx new file mode 100644 index 0000000..4e53720 --- /dev/null +++ b/components/common/Modal/ModalRoot/ModalRoot.tsx @@ -0,0 +1,53 @@ +import { useState, useEffect } from 'react'; +import { ModalDetail } from '../../../../lib/types/modal.interface'; +import ModalService from '../services/ModalService'; +import styles from './ModalRoot.module.scss'; + +export default function ModalRoot() { + const [modal, setModal] = useState | null>(null); + + useEffect(() => { + ModalService.on('open', ({ component, props, target, resolve }) => { + setModal({ + component, + props, + close: (...args) => { + setModal(null); + resolve?.call(null, ...args); + target?.focus(); + }, + }); + }); + }, []); + + useEffect(() => { + const handler = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + modal?.close?.call(null, false); + } + }; + + if (!modal) { + return; + } + + window.addEventListener('keyup', handler); + return () => { + window.removeEventListener('keyup', handler); + }; + }, [modal]); + + const ModalComponent = modal?.component ? modal.component : null; + + return ( +
+ {ModalComponent && ( + + )} +
+ ); +} diff --git a/components/common/Modal/services/ModalService.ts b/components/common/Modal/services/ModalService.ts new file mode 100644 index 0000000..751d972 --- /dev/null +++ b/components/common/Modal/services/ModalService.ts @@ -0,0 +1,25 @@ +import { ModalType, ModalDetail } from '../../../../lib/types/modal.interface'; + +const ModalService = { + on(event: string, callback: (props: ModalDetail) => void) { + document.addEventListener(event, (e: Event) => + callback((e as CustomEvent>).detail) + ); + }, + open(component: ModalType, props: any = {}) { + return new Promise((resolve, _) => { + document.dispatchEvent( + new CustomEvent>('open', { + detail: { + component, + props, + resolve, + target: (document.activeElement as HTMLElement) || undefined, + }, + }) + ); + }); + }, +}; + +export default ModalService; diff --git a/lib/types/modal.interface.ts b/lib/types/modal.interface.ts new file mode 100644 index 0000000..2780c30 --- /dev/null +++ b/lib/types/modal.interface.ts @@ -0,0 +1,13 @@ +import React from 'react'; + +export type ModalType = React.ElementType< + T & { close: (...args: any[]) => void } +>; + +export interface ModalDetail { + component: ModalType; + props: any; + target?: HTMLElement; + resolve?: (...args: any[]) => void; + close?: (...args: any[]) => void; +} diff --git a/lib/types/oauth2-client.interface.ts b/lib/types/oauth2-client.interface.ts new file mode 100644 index 0000000..65701eb --- /dev/null +++ b/lib/types/oauth2-client.interface.ts @@ -0,0 +1,37 @@ +import { Picture } from './picture.interface'; + +export enum OAuth2ClientURLType { + REDIRECT_URI = 'redirect_uri', + TERMS = 'terms', + PRIVACY = 'privacy', + WEBSITE = 'website', +} + +export interface OAuth2ClientURL { + id: number; + url: string; + type: OAuth2ClientURLType; + created_at: Date; + updated_at: Date; +} + +export interface OAuth2ClientListItem { + id: number; + client_id: string; + client_secret: string; + title: string; + description: string; + scope: string; + grants: string; + activated: boolean; + verified: boolean; + created_at: string; + updated_at: string; + urls?: OAuth2ClientURL[]; + picture?: Picture; + owner?: { + id: number; + uuid: string; + username: string; + }; +} diff --git a/lib/types/picture.interface.ts b/lib/types/picture.interface.ts new file mode 100644 index 0000000..7c3250c --- /dev/null +++ b/lib/types/picture.interface.ts @@ -0,0 +1,5 @@ +export interface Picture { + id: number; + file: number; + mimetype: string; +} diff --git a/lib/types/users.interfaces.ts b/lib/types/users.interfaces.ts index 191c1c6..b90c6d7 100644 --- a/lib/types/users.interfaces.ts +++ b/lib/types/users.interfaces.ts @@ -1,11 +1,6 @@ +import { Picture } from './picture.interface'; import { Privilege } from './privilege.interface'; -export interface UserPicture { - id: number; - file: number; - mimetype: string; -} - export interface UserListItem { id: number; uuid: string; @@ -16,6 +11,6 @@ export interface UserListItem { updated_at: string; activity_at: string; activated: boolean; - picture?: UserPicture; + picture?: Picture; privileges?: Privilege[]; } diff --git a/pages/_app.tsx b/pages/_app.tsx index fa9bf14..7f7c4c2 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -2,6 +2,7 @@ import '../styles/globals.scss'; import type { AppProps } from 'next/app'; import { SWRConfig } from 'swr'; import fetchJson from '../lib/utils/swr-fetcher'; +import ModalRoot from '../components/common/Modal/ModalRoot/ModalRoot'; function MyApp({ Component, pageProps }: AppProps) { return ( @@ -14,6 +15,7 @@ function MyApp({ Component, pageProps }: AppProps) { }} > + ); } diff --git a/pages/api/login.ts b/pages/api/login.ts index 1b0e59d..c424908 100644 --- a/pages/api/login.ts +++ b/pages/api/login.ts @@ -18,6 +18,7 @@ export default function handler(req: NextApiRequest, res: NextApiResponse) { state: stateToken, }) ); + const params = new URLSearchParams({ client_id: CLIENT_ID, response_type: 'code', diff --git a/pages/oauth2.tsx b/pages/oauth2.tsx new file mode 100644 index 0000000..3c358fb --- /dev/null +++ b/pages/oauth2.tsx @@ -0,0 +1,3 @@ +import { OAuth2Page } from '../components/OAuth2Page/OAuth2Page'; + +export default OAuth2Page; diff --git a/public/application.png b/public/application.png new file mode 100644 index 0000000..6ad5ce6 Binary files /dev/null and b/public/application.png differ diff --git a/styles/_breakpoint.scss b/styles/_breakpoint.scss new file mode 100644 index 0000000..2a43723 --- /dev/null +++ b/styles/_breakpoint.scss @@ -0,0 +1,28 @@ +$breakpoints: ( + xxs: 480px, + xs: 720px, + sm: 1024px, + md: 1280px, + lg: 1440px, + xl: 1920px, + xxl: 2580px, +); + +@mixin break-on($breakpoint, $type) { + @if map-has-key($breakpoints, $breakpoint) { + $mediaValue: map-get($breakpoints, $breakpoint); + @if $type == up { + @media (min-width: $mediaValue) { + @content; + } + } @else if ($type == down) { + @media (max-width: $mediaValue) { + @content; + } + } @else { + @warn "Unknown `#{$type}` in $media query type"; + } + } @else { + @warn "Unknown `#{$breakpoint}` in $breakpoints"; + } +} diff --git a/styles/globals.scss b/styles/globals.scss index 2ceac67..0e7092e 100644 --- a/styles/globals.scss +++ b/styles/globals.scss @@ -1,3 +1,5 @@ +@import 'breakpoint'; + html, body { padding: 0; @@ -16,6 +18,14 @@ a { box-sizing: border-box; } +h1, +h2, +h3, +h4, +h5 { + margin-top: 0; +} + @media (prefers-color-scheme: dark) { html { color-scheme: dark;