Add tailwind + redesign login page

This commit is contained in:
viktorstrate
2021-05-21 12:15:00 +02:00
parent 8ec0fd7076
commit d5ea5e4d2e
13 changed files with 1574 additions and 3510 deletions

View File

@@ -14,6 +14,6 @@
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

4732
ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -19,10 +19,8 @@
"babel-plugin-i18next-extract": "^0.8.3",
"babel-plugin-styled-components": "^1.12.0",
"babel-plugin-transform-semantic-ui-react-imports": "^1.4.1",
"browser-sync": "^2.26.14",
"connect-history-api-fallback": "^1.6.0",
"copy-to-clipboard": "^3.3.1",
"dotenv": "^9.0.2",
"esbuild": "^0.11.20",
"esbuild-plugin-babel": "^0.2.3",
"eslint": "^7.26.0",
@@ -37,18 +35,17 @@
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-helmet": "^6.1.0",
"react-hook-form": "^7.6.3",
"react-i18next": "^11.8.15",
"react-router-dom": "^5.2.0",
"react-router-prop-types": "^1.0.5",
"react-spring": "^8.0.27",
"react-test-renderer": "^17.0.2",
"semantic-ui-css": "^2.4.1",
"semantic-ui-react": "^2.0.3",
"styled-components": "^5.3.0",
"subscriptions-transport-ws": "^0.9.18",
"typescript": "^4.2.4",
"url-join": "^4.0.1",
"workbox-build": "^6.1.5"
"url-join": "^4.0.1"
},
"scripts": {
"start": "vite",
@@ -82,17 +79,17 @@
"@typescript-eslint/eslint-plugin": "^4.23.0",
"@typescript-eslint/parser": "^4.23.0",
"@vitejs/plugin-react-refresh": "^1.3.3",
"autoprefixer": "^10.2.5",
"eslint-config-prettier": "^8.3.0",
"husky": "^6.0.0",
"jest": "^26.6.3",
"lint-staged": "^11.0.0",
"postcss": "^8.3.0",
"prettier": "^2.3.0",
"tailwindcss": "^2.1.2",
"tsc-files": "^1.1.2",
"vite": "^2.3.3"
},
"cache": {
"swDest": "service-worker.js"
},
"prettier": {
"trailingComma": "es5",
"tabWidth": 2,

6
ui/postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@@ -1,35 +1,10 @@
import React from 'react'
import { createGlobalStyle } from 'styled-components'
import { Helmet } from 'react-helmet'
import Routes from './components/routes/Routes'
import Messages from './components/messages/Messages'
import { useTranslation } from 'react-i18next'
import { loadTranslations } from './localization'
const GlobalStyle = createGlobalStyle`
* {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
html {
font-size: 0.85rem;
}
#root, body {
height: 100%;
margin: 0;
font-size: inherit;
}
/* Make dimmer lighter */
.ui.dimmer {
background-color: rgba(0, 0, 0, 0.5);
}
`
import 'semantic-ui-css/semantic.min.css'
const App = () => {
const { t } = useTranslation()
loadTranslations()
@@ -45,7 +20,6 @@ const App = () => {
)}
/>
</Helmet>
<GlobalStyle />
<Routes />
<Messages />
</>

View File

@@ -1,14 +1,17 @@
import React, { useState, useCallback } from 'react'
import React from 'react'
import { useQuery, gql, useMutation } from '@apollo/client'
import { Redirect } from 'react-router-dom'
import styled from 'styled-components'
import { Button, Form, Message, Header, HeaderProps } from 'semantic-ui-react'
import { checkInitialSetupQuery, login, Container } from './loginUtilities'
import { useForm, SubmitHandler } from 'react-hook-form'
import { checkInitialSetupQuery, login } from './loginUtilities'
import { authToken } from '../../helpers/authentication'
import logoPath from '../../assets/photoview-logo.svg'
import { useTranslation } from 'react-i18next'
import { Helmet } from 'react-helmet'
import { Redirect } from 'react-router'
import { Submit, TextField } from '../../primitives/form/Input'
import MessageBox from '../../primitives/form/MessageBox'
import { CheckInitialSetup } from './__generated__/CheckInitialSetup'
import { Authorize, AuthorizeVariables } from './__generated__/Authorize'
const authorizeMutation = gql`
mutation Authorize($username: String!, $password: String!) {
@@ -20,123 +23,117 @@ const authorizeMutation = gql`
}
`
const StyledLogo = styled.img`
max-height: 128px;
`
const LogoHeader = (props: HeaderProps) => {
const LogoHeader = () => {
const { t } = useTranslation()
return (
<Header {...props} as="h1" textAlign="center">
<StyledLogo src={logoPath} alt="photoview logo" />
<p style={{ fontWeight: 400 }}>
<div className="flex justify-center flex-col mb-20 mt-40">
<img className="h-24" src={logoPath} alt="photoview logo" />
<h1 className="text-3xl text-center mt-4">
{t('login_page.welcome', 'Welcome to Photoview')}
</p>
</Header>
</h1>
</div>
)
}
const LogoHeaderStyled = styled(LogoHeader)`
margin-bottom: 72px !important;
`
const LoginPage = () => {
const LoginForm = () => {
const { t } = useTranslation()
const {
register,
handleSubmit,
formState: { errors: formErrors },
} = useForm()
const [credentials, setCredentials] = useState({
username: '',
password: '',
})
const handleChange = useCallback(
(event, key) => {
const value = event.target.value
setCredentials(credentials => {
return {
...credentials,
[key]: value,
}
})
},
[setCredentials]
)
const signIn = useCallback(
(event, authorize) => {
event.preventDefault()
authorize({
variables: {
username: credentials.username,
password: credentials.password,
},
})
},
[credentials]
)
const { data: initialSetupData } = useQuery(checkInitialSetupQuery)
const [authorize, { loading, data }] = useMutation(authorizeMutation, {
const [authorize, { loading, data }] = useMutation<
Authorize,
AuthorizeVariables
>(authorizeMutation, {
onCompleted: data => {
const { success, token } = data.authorizeUser
if (success) {
if (success && token) {
login(token)
}
},
})
const onSubmit: SubmitHandler<LoginInputs> = data => {
authorize({
variables: {
username: data.username,
password: data.password,
},
})
}
const errorMessage =
data && !data.authorizeUser.success ? data.authorizeUser.status : null
return (
<form
className="mx-auto w-[450px]"
onSubmit={handleSubmit(onSubmit)}
// loading={loading || (data && data.authorizeUser.success)}
>
<TextField
className="w-full"
label={t('login_page.field.username', 'Username')}
{...register('username')}
error={formErrors.username?.message}
/>
<TextField
className="w-full"
type="password"
label={t('login_page.field.password', 'Password')}
{...register('password')}
/>
<input
type="submit"
value={t('login_page.field.submit', 'Sign in') as string}
className="rounded-md px-8 py-2 focus:outline-none hover:cursor-pointer bg-gradient-to-bl from-[#FF8246] to-[#D6264D] text-white font-semibold focus:ring-2 focus:ring-red-200"
/>
<MessageBox
message={errorMessage}
show={!!errorMessage}
type="negative"
/>
</form>
)
}
// background-image: radial-gradient(0% 100%, #FF8246 0%, #D6264D 100%);
// border: 2px solid rgba(255,51,0,0.29);
// border-radius: 6px;
type LoginInputs = {
username: string
password: string
}
const LoginPage = () => {
const { t } = useTranslation()
const { data: initialSetupData } = useQuery<CheckInitialSetup>(
checkInitialSetupQuery
)
if (authToken()) {
return <Redirect to="/" />
}
return (
<div>
<>
<Helmet>
<title>{t('title.login', 'Login')} - Photoview</title>
</Helmet>
<Container>
<LogoHeaderStyled />
{initialSetupData?.siteInfo?.initialSetup && (
<Redirect to="/initialSetup" />
)}
<Form
style={{ width: 500, margin: 'auto' }}
error={!!errorMessage}
onSubmit={e => signIn(e, authorize)}
loading={loading || (data && data.authorizeUser.success)}
>
<Form.Field>
<label htmlFor="username_field">
{t('login_page.field.username', 'Username')}
</label>
<input
id="username_field"
onChange={e => handleChange(e, 'username')}
/>
</Form.Field>
<Form.Field>
<label htmlFor="password_field">
{t('login_page.field.password', 'Password')}
</label>
<input
type="password"
id="password_field"
onChange={e => handleChange(e, 'password')}
/>
</Form.Field>
<Message error content={errorMessage} />
<Button type="submit">
{t('login_page.field.submit', 'Sign in')}
</Button>
</Form>
</Container>
</div>
{initialSetupData?.siteInfo?.initialSetup && (
<Redirect to="/initialSetup" />
)}
<div>
<LogoHeader />
<LoginForm />
</div>
</>
)
}

17
ui/src/index.css Normal file
View File

@@ -0,0 +1,17 @@
* {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
#root,
body,
html {
height: 100%;
margin: 0;
font-size: inherit;
font-family: 'Source Sans Pro', sans-serif;
}
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -1,4 +1,4 @@
import 'regenerator-runtime/runtime'
// import 'regenerator-runtime/runtime'
import React from 'react'
import ReactDOM from 'react-dom'
@@ -9,6 +9,8 @@ import { ApolloProvider } from '@apollo/client'
import { BrowserRouter as Router } from 'react-router-dom'
import { setupLocalization } from './localization'
import './index.css'
setupLocalization()
const Main = () => (

View File

@@ -0,0 +1,13 @@
import React from 'react'
type LoaderProps = {
active: boolean
message?: string
}
const Loader = ({ active, message }: LoaderProps) => {
if (!active) return null
return <div>{message ?? 'Loading...'}</div>
}
export default Loader

View File

@@ -0,0 +1,52 @@
import React from 'react'
type TextFieldProps = {
label?: string
error?: string
} & React.InputHTMLAttributes<HTMLInputElement>
export const TextField = ({
label,
error,
className,
...inputProps
}: TextFieldProps) => {
const input = (
<input
className="block bg-white border border-gray-200 rounded-md h-10 w-full focus:border-blue-400 focus:ring-2 focus:outline-none px-2"
{...inputProps}
/>
)
let errorElm = null
if (error) errorElm = <div>{error}</div>
if (label) {
return (
<label className={`block my-4 ${className}`}>
<span className="block text-xs uppercase font-semibold mb-1">
{label}
</span>
{input}
{errorElm}
</label>
)
}
return (
<div className={className}>
{input}
{errorElm}
</div>
)
}
export const Submit = (props: React.InputHTMLAttributes<HTMLInputElement>) => {
return (
<input
className={`rounded-md px-8 py-2 focus:outline-none hover:cursor-pointer bg-[#eee] hover:bg-[#e6e6e6] focus:bg-[#e6e6e6] ${props.className}`}
type="submit"
{...props}
/>
)
}

View File

@@ -0,0 +1,19 @@
import React from 'react'
type MessageBoxProps = {
message?: string | null
show?: boolean
type?: 'neutral' | 'positive' | 'negative'
}
const MessageBox = ({ message, show, type }: MessageBoxProps) => {
if (!show) return null
let variant = 'bg-gray-100'
if (type == 'positive') variant = 'bg-green-200 text-green-900'
if (type == 'negative') variant = 'bg-red-200 text-red-900'
return <div className={`py-2 px-3 my-4 rounded-md ${variant}`}>{message}</div>
}
export default MessageBox

12
ui/tailwind.config.js Normal file
View File

@@ -0,0 +1,12 @@
module.exports = {
mode: 'jit',
purge: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [],
}

View File

@@ -4,4 +4,7 @@ import reactRefresh from '@vitejs/plugin-react-refresh'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [reactRefresh()],
server: {
port: 1234,
},
})