mirror of
https://git.vectorsigma.ru/public/photoview.git
synced 2026-08-03 20:59:03 +00:00
Add tailwind + redesign login page
This commit is contained in:
@@ -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
4732
ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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
6
ui/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
@@ -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 />
|
||||
</>
|
||||
|
||||
@@ -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
17
ui/src/index.css
Normal 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;
|
||||
@@ -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 = () => (
|
||||
13
ui/src/primitives/Loader.tsx
Normal file
13
ui/src/primitives/Loader.tsx
Normal 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
|
||||
52
ui/src/primitives/form/Input.tsx
Normal file
52
ui/src/primitives/form/Input.tsx
Normal 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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
19
ui/src/primitives/form/MessageBox.tsx
Normal file
19
ui/src/primitives/form/MessageBox.tsx
Normal 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
12
ui/tailwind.config.js
Normal 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: [],
|
||||
}
|
||||
@@ -4,4 +4,7 @@ import reactRefresh from '@vitejs/plugin-react-refresh'
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [reactRefresh()],
|
||||
server: {
|
||||
port: 1234,
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user