General work towards new design

This commit is contained in:
viktorstrate
2021-05-23 17:39:42 +02:00
parent e5c318258c
commit 40297c069e
19 changed files with 396 additions and 545 deletions

View File

@@ -6,6 +6,9 @@
<!-- <link rel="manifest" href="/manifest.json" />
<link rel="stylesheet" href="/index.css" /> -->
<link rel="icon" href="/src/favicon.ico" />
<link rel="icon" href="/src/favicon.svg" type="image/svg+xml" />
<!-- Apple touch devices -->
<link rel="apple-touch-icon" href="/src/assets/apple-touch-icon.png" />
<meta name="apple-mobile-web-app-title" content="Photoview" />

326
ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,6 +13,9 @@
"@babel/core": "^7.14.0",
"@babel/preset-react": "^7.13.13",
"@babel/preset-typescript": "^7.13.0",
"@rollup/plugin-babel": "^5.3.0",
"@vitejs/plugin-react-refresh": "^1.3.3",
"autoprefixer": "^10.2.5",
"babel-eslint": "^10.1.0",
"babel-jest": "^26.6.3",
"babel-plugin-graphql-tag": "^3.2.0",
@@ -31,6 +34,7 @@
"fs-extra": "^10.0.0",
"i18next": "^20.2.2",
"mapbox-gl": "^2.2.0",
"postcss": "^8.3.0",
"prop-types": "^15.7.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
@@ -44,8 +48,10 @@
"semantic-ui-react": "^2.0.3",
"styled-components": "^5.3.0",
"subscriptions-transport-ws": "^0.9.18",
"tailwindcss": "^2.1.2",
"typescript": "^4.2.4",
"url-join": "^4.0.1"
"url-join": "^4.0.1",
"vite": "^2.3.3"
},
"scripts": {
"start": "vite",
@@ -78,17 +84,12 @@
"@types/url-join": "^4.0.0",
"@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"
"tsc-files": "^1.1.2"
},
"prettier": {
"trailingComma": "es5",

View File

@@ -4,6 +4,7 @@ import Layout from '../../components/layout/Layout'
import { useQuery, gql } from '@apollo/client'
import LazyLoad from '../../helpers/LazyLoad'
import { useTranslation } from 'react-i18next'
import { getMyAlbums } from './__generated__/getMyAlbums'
const getAlbumsQuery = gql`
query getMyAlbums {
@@ -20,8 +21,7 @@ const getAlbumsQuery = gql`
`
const AlbumsPage = () => {
const { t } = useTranslation()
const { loading, error, data } = useQuery(getAlbumsQuery)
const { loading, error, data } = useQuery<getMyAlbums>(getAlbumsQuery)
useEffect(() => {
return () => LazyLoad.disconnect()
@@ -33,14 +33,7 @@ const AlbumsPage = () => {
return (
<Layout title="Albums">
<h1>{t('albums_page.title', 'Albums')}</h1>
{!loading && (
<AlbumBoxes
loading={loading}
error={error}
albums={data && data.myAlbums}
/>
)}
<AlbumBoxes error={error} albums={data?.myAlbums} />
</Layout>
)
}

View File

@@ -66,6 +66,8 @@ const LoginForm = () => {
})
}
console.log('errors', formErrors)
const errorMessage =
data && !data.authorizeUser.success ? data.authorizeUser.status : null
@@ -78,14 +80,23 @@ const LoginForm = () => {
<TextField
className="w-full"
label={t('login_page.field.username', 'Username')}
{...register('username')}
error={formErrors.username?.message}
{...register('username', { required: true })}
error={
formErrors.username?.type == 'required'
? 'Please enter a username'
: undefined
}
/>
<TextField
className="w-full"
type="password"
label={t('login_page.field.password', 'Password')}
{...register('password')}
{...register('password', { required: true })}
error={
formErrors.password?.type == 'required'
? 'Please enter a password'
: undefined
}
/>
<input
type="submit"

View File

@@ -1,136 +0,0 @@
import React from 'react'
import { authToken } from '../helpers/authentication'
import { Checkbox, Dropdown, Button, Icon } from 'semantic-ui-react'
import styled from 'styled-components'
import PropTypes from 'prop-types'
import { useTranslation } from 'react-i18next'
const FavoritesCheckboxStyle = styled(Checkbox)`
margin-bottom: 16px;
margin-right: 10px;
&.ui.toggle.checkbox label {
padding-left: 0;
padding-right: 4em;
font-weight: bold;
}
&.ui.checkbox input,
&.ui.toggle.checkbox label:before {
left: auto;
right: 0;
}
&.ui.toggle.checkbox label:after {
left: auto;
right: 1.75em;
transition: background 0.3s ease 0s, right 0.3s ease 0s;
}
&.ui.toggle.checkbox input:checked + label:after {
left: auto;
right: 0.08em;
transition: background 0.3s ease 0s, right 0.3s ease 0s;
}
`
export const FavoritesCheckbox = ({ onlyFavorites, setOnlyFavorites }) => {
const { t } = useTranslation()
return (
<FavoritesCheckboxStyle
toggle
label={t('album_filter.only_favorites', 'Show only favorites')}
checked={onlyFavorites}
onChange={(e, result) => setOnlyFavorites(result.checked)}
/>
)
}
FavoritesCheckbox.propTypes = {
onlyFavorites: PropTypes.bool.isRequired,
setOnlyFavorites: PropTypes.func.isRequired,
}
const OrderDirectionButton = styled(Button)`
padding: 0.88em;
margin-left: 10px !important;
`
const SortByLabel = styled.strong`
margin-left: 4px;
margin-right: 6px;
`
const AlbumFilter = ({
onlyFavorites,
setOnlyFavorites,
setOrdering,
ordering,
}) => {
const { t } = useTranslation()
const onChangeOrderDirection = (e, data) => {
const direction = data.children.props.name === 'arrow up' ? 'DESC' : 'ASC'
setOrdering({ orderDirection: direction })
}
const sortingOptions = [
{
key: 'date_shot',
value: 'date_shot',
text: t('album_filter.sorting_options.date_shot', 'Date shot'),
},
{
key: 'updated_at',
value: 'updated_at',
text: t('album_filter.sorting_options.date_imported', 'Date imported'),
},
{
key: 'title',
value: 'title',
text: t('album_filter.sorting_options.title', 'Title'),
},
{
key: 'type',
value: 'type',
text: t('album_filter.sorting_options.type', 'Kind'),
},
]
return (
<>
{authToken() && setOnlyFavorites && (
<FavoritesCheckbox
onlyFavorites={onlyFavorites}
setOnlyFavorites={setOnlyFavorites}
/>
)}
<SortByLabel>{t('album_filter.sort_by', 'Sort by')}</SortByLabel>
<Dropdown
selection
options={sortingOptions}
defaultValue={
sortingOptions.find(e => e.value === ordering.orderBy)?.value ||
sortingOptions[0].value
}
onChange={(e, data) => {
setOrdering({ orderBy: data.value })
}}
/>
<OrderDirectionButton icon basic onClick={onChangeOrderDirection}>
<Icon
name={'arrow ' + (ordering.orderDirection === 'ASC' ? 'up' : 'down')}
/>
</OrderDirectionButton>
</>
)
}
AlbumFilter.propTypes = {
onlyFavorites: PropTypes.bool,
setOnlyFavorites: PropTypes.func,
setOrdering: PropTypes.func,
ordering: PropTypes.object,
}
export default AlbumFilter

View File

@@ -0,0 +1,104 @@
import React from 'react'
import { authToken } from '../helpers/authentication'
import { useTranslation } from 'react-i18next'
import { OrderDirection } from '../../__generated__/globalTypes'
import { MediaOrdering, SetOrderingFn } from '../hooks/useOrderingParams'
type FavoriteCheckboxProps = {
onlyFavorites: boolean
setOnlyFavorites(favorites: boolean): void
}
export const FavoritesCheckbox = ({
onlyFavorites,
setOnlyFavorites,
}: FavoriteCheckboxProps) => {
const { t } = useTranslation()
return (
<label>
<input
type="checkbox"
checked={onlyFavorites}
onChange={e => setOnlyFavorites(e.target.checked)}
/>
<span>{t('album_filter.only_favorites', 'Show only favorites')}</span>
</label>
)
}
type AlbumFilterProps = {
onlyFavorites: boolean
setOnlyFavorites?(favorites: boolean): void
ordering?: MediaOrdering
setOrdering?: SetOrderingFn
}
const AlbumFilter = ({
onlyFavorites,
setOnlyFavorites,
setOrdering,
ordering,
}: AlbumFilterProps) => {
const { t } = useTranslation()
const changeOrderDirection = () => {
if (setOrdering && ordering) {
setOrdering({
orderDirection:
ordering.orderDirection == OrderDirection.ASC
? OrderDirection.DESC
: OrderDirection.ASC,
})
}
}
const changeOrderBy = (e: React.ChangeEvent<HTMLSelectElement>) => {
if (setOrdering) {
setOrdering({ orderBy: e.target.value })
}
}
const sortingOptions = [
{
value: 'date_shot',
text: t('album_filter.sorting_options.date_shot', 'Date shot'),
},
{
value: 'updated_at',
text: t('album_filter.sorting_options.date_imported', 'Date imported'),
},
{
value: 'title',
text: t('album_filter.sorting_options.title', 'Title'),
},
{
value: 'type',
text: t('album_filter.sorting_options.type', 'Kind'),
},
]
return (
<>
{authToken() && setOnlyFavorites && (
<FavoritesCheckbox
onlyFavorites={onlyFavorites}
setOnlyFavorites={setOnlyFavorites}
/>
)}
<span>{t('album_filter.sort_by', 'Sort by')}</span>
<select onChange={changeOrderBy} value={ordering?.orderBy || undefined}>
{sortingOptions.map(x => (
<option key={x.value} value={x.value}>
{x.text}
</option>
))}
</select>
<button onClick={changeOrderDirection}>{ordering?.orderDirection}</button>
</>
)
}
export default AlbumFilter

View File

@@ -1,42 +1,24 @@
import React, { useEffect, useContext } from 'react'
import PropTypes from 'prop-types'
import { Breadcrumb, IconProps } from 'semantic-ui-react'
import { Link } from 'react-router-dom'
import styled from 'styled-components'
import { Icon } from 'semantic-ui-react'
import { SidebarContext } from './sidebar/Sidebar'
import AlbumSidebar from './sidebar/AlbumSidebar'
import { useLazyQuery, gql } from '@apollo/client'
import { authToken } from '../helpers/authentication'
import { albumPathQuery } from './__generated__/albumPathQuery'
const Header = styled.h1`
margin: 24px 0 8px 0 !important;
& a {
color: black;
&:hover {
text-decoration: underline;
}
const BreadcrumbList = styled.ol`
& li::after {
content: '';
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='5px' height='6px' viewBox='0 0 5 6'%3E%3Cpolyline fill='none' stroke='%23979797' points='0.74 0.167710644 3.57228936 3 0.74 5.83228936' /%3E%3C/svg%3E");
width: 5px;
height: 6px;
display: inline-block;
margin: 6px;
vertical-align: middle;
}
`
const StyledIcon = styled(Icon)`
margin-left: 8px !important;
display: inline-block;
color: #888;
cursor: pointer;
&:hover {
color: #1e70bf;
}
`
const SettingsIcon = (props: IconProps) => {
return <StyledIcon name="settings" size="small" {...props} />
}
const ALBUM_PATH_QUERY = gql`
query albumPathQuery($id: ID!) {
album(id: $id) {
@@ -58,9 +40,8 @@ type AlbumTitleProps = {
}
const AlbumTitle = ({ album, disableLink = false }: AlbumTitleProps) => {
const [fetchPath, { data: pathData }] = useLazyQuery<albumPathQuery>(
ALBUM_PATH_QUERY
)
const [fetchPath, { data: pathData }] =
useLazyQuery<albumPathQuery>(ALBUM_PATH_QUERY)
const { updateSidebar } = useContext(SidebarContext)
useEffect(() => {
@@ -85,12 +66,9 @@ const AlbumTitle = ({ album, disableLink = false }: AlbumTitleProps) => {
.slice()
.reverse()
.map(x => (
<span key={x.id}>
<Breadcrumb.Section as={Link} to={`/album/${x.id}`}>
{x.title}
</Breadcrumb.Section>
<Breadcrumb.Divider icon="right angle" />
</span>
<li key={x.id} className="inline-block hover:underline">
<Link to={`/album/${x.id}`}>{x.title}</Link>
</li>
))
if (!disableLink) {
@@ -98,25 +76,24 @@ const AlbumTitle = ({ album, disableLink = false }: AlbumTitleProps) => {
}
return (
<>
<Header>
<Breadcrumb>{breadcrumbSections}</Breadcrumb>
{title}
{authToken() && (
<SettingsIcon
onClick={() => {
updateSidebar(<AlbumSidebar albumId={album.id} />)
}}
/>
)}
</Header>
</>
<div className="flex">
<div>
<nav aria-label="Album breadcrumb">
<BreadcrumbList className="">{breadcrumbSections}</BreadcrumbList>
</nav>
<h1 className="text-2xl">{title}</h1>
</div>
{authToken() && (
<button
onClick={() => {
updateSidebar(<AlbumSidebar albumId={album.id} />)
}}
>
More
</button>
)}
</div>
)
}
AlbumTitle.propTypes = {
album: PropTypes.object,
disableLink: PropTypes.bool,
}
export default AlbumTitle

View File

@@ -1,52 +1,8 @@
import React, { useState } from 'react'
import styled from 'styled-components'
import { Link } from 'react-router-dom'
import { ProtectedImage } from '../photoGallery/ProtectedMedia'
import { albumQuery_album_subAlbums } from '../../Pages/AlbumPage/__generated__/albumQuery'
const AlbumBoxLink = styled(Link)`
width: 240px;
height: 240px;
display: inline-block;
text-align: center;
color: #222;
`
const ImageWrapper = styled.div`
width: 240px;
height: 220px;
padding: 0 10px;
position: relative;
`
const Image = styled(ProtectedImage)`
width: 220px;
height: 220px;
margin: auto;
border-radius: 4%;
object-fit: cover;
object-position: center;
`
const Placeholder = styled.div<{ overlap?: boolean; loaded?: boolean }>`
width: 220px;
height: 220px;
border-radius: 4%;
margin: auto;
background: linear-gradient(#f7f7f7 0%, #eee 100%);
${({ overlap, loaded }) =>
overlap &&
`
position: absolute;
top: 0;
left: 10px;
opacity: ${loaded ? 0 : 1};
transition: opacity 200ms;
`}
`
interface AlbumBoxImageProps {
src?: string
}
@@ -54,16 +10,31 @@ interface AlbumBoxImageProps {
const AlbumBoxImage = ({ src, ...props }: AlbumBoxImageProps) => {
const [loaded, setLoaded] = useState(false)
let image = null
if (src) {
return (
<ImageWrapper>
<Image {...props} onLoad={() => setLoaded(true)} src={src} />
<Placeholder overlap loaded={loaded} />
</ImageWrapper>
image = (
<ProtectedImage
className="object-cover object-center w-full h-full rounded-lg"
{...props}
onLoad={() => setLoaded(true)}
src={src}
/>
)
}
return <Placeholder />
let placeholder = null
if (!loaded) {
placeholder = (
<div className="bg-gray-100 animate-pulse w-full h-full rounded-lg absolute"></div>
)
}
return (
<div className="w-[220px] h-[220px] relative rounded-lg">
{image}
{placeholder}
</div>
)
}
type AlbumBoxProps = {
@@ -72,20 +43,24 @@ type AlbumBoxProps = {
}
export const AlbumBox = ({ album, customLink, ...props }: AlbumBoxProps) => {
if (!album) {
const wrapperClasses = 'inline-block text-center text-gray-900 mx-3 my-2 h-60'
if (album) {
return (
<AlbumBoxLink {...props} to="#">
<AlbumBoxImage />
</AlbumBoxLink>
<Link
to={customLink || `/album/${album.id}`}
className={wrapperClasses}
{...props}
>
<AlbumBoxImage src={album.thumbnail?.thumbnail?.url} />
<p>{album.title}</p>
</Link>
)
}
const thumbnail = album.thumbnail?.thumbnail?.url
return (
<AlbumBoxLink {...props} to={customLink || `/album/${album.id}`}>
<AlbumBoxImage src={thumbnail} />
<p>{album.title}</p>
</AlbumBoxLink>
<div className={wrapperClasses} {...props}>
<AlbumBoxImage />
</div>
)
}

View File

@@ -1,15 +1,8 @@
import React from 'react'
import styled from 'styled-components'
import { albumQuery_album_subAlbums } from '../../Pages/AlbumPage/__generated__/albumQuery'
import { AlbumBox } from './AlbumBox'
const Container = styled.div`
margin: 20px -10px;
position: relative;
`
type AlbumBoxesProps = {
loading: boolean
error?: Error
albums?: albumQuery_album_subAlbums[]
getCustomLink?(albumID: string): string
@@ -20,7 +13,7 @@ const AlbumBoxes = ({ error, albums, getCustomLink }: AlbumBoxesProps) => {
let albumElements = []
if (albums) {
if (albums !== undefined) {
albumElements = albums.map(album => (
<AlbumBox
key={album.id}
@@ -34,7 +27,7 @@ const AlbumBoxes = ({ error, albums, getCustomLink }: AlbumBoxesProps) => {
}
}
return <Container>{albumElements}</Container>
return <div className="-mx-3 my-6">{albumElements}</div>
}
export default AlbumBoxes

View File

@@ -4,11 +4,11 @@ import PhotoGallery from '../photoGallery/PhotoGallery'
import AlbumBoxes from './AlbumBoxes'
import AlbumFilter from '../AlbumFilter'
import { albumQuery_album } from '../../Pages/AlbumPage/__generated__/albumQuery'
import { OrderDirection } from '../../../__generated__/globalTypes'
import {
photoGalleryReducer,
urlPresentModeSetupHook,
} from '../photoGallery/photoGalleryReducer'
import { MediaOrdering, SetOrderingFn } from '../../hooks/useOrderingParams'
type AlbumGalleryProps = {
album?: albumQuery_album
@@ -16,8 +16,8 @@ type AlbumGalleryProps = {
customAlbumLink?(albumID: string): string
showFilter?: boolean
setOnlyFavorites?(favorites: boolean): void
setOrdering?(ordering: { orderBy: string }): void
ordering?: { orderBy: string | null; orderDirection: OrderDirection | null }
setOrdering?: SetOrderingFn
ordering?: MediaOrdering
onlyFavorites?: boolean
onFavorite?(): void
}
@@ -61,19 +61,17 @@ const AlbumGallery = React.forwardRef(
if (album.subAlbums.length > 0) {
subAlbumElement = (
<AlbumBoxes
loading={loading}
albums={album.subAlbums}
getCustomLink={customAlbumLink}
/>
)
}
} else {
subAlbumElement = <AlbumBoxes loading={loading} />
subAlbumElement = <AlbumBoxes />
}
return (
<div ref={ref}>
<AlbumTitle album={album} disableLink />
{showFilter && (
<AlbumFilter
onlyFavorites={onlyFavorites}
@@ -82,17 +80,8 @@ const AlbumGallery = React.forwardRef(
ordering={ordering}
/>
)}
<AlbumTitle album={album} disableLink />
{subAlbumElement}
{
<h2
style={{
opacity: loading ? 0 : 1,
display: album && album.subAlbums.length > 0 ? 'block' : 'none',
}}
>
Images
</h2>
}
<PhotoGallery
loading={loading}
mediaState={mediaState}

View File

@@ -60,7 +60,7 @@ export const MainMenu = () => {
const mapboxEnabled = !!mapboxQuery?.data?.mapboxToken
return (
<div className="absolute lg:relative w-full bottom-0 bg-white shadow-separator lg:shadow-none lg:w-[240px] lg:mx-8 lg:my-4">
<div className="absolute lg:relative w-full bottom-0 z-10 bg-white shadow-separator lg:shadow-none lg:w-[240px] lg:mx-8 lg:my-4">
<ul className="flex justify-around py-2 px-2 z-10 max-w-lg mx-auto lg:flex-col lg:p-0">
<MenuButton
to="/photos"

View File

@@ -181,7 +181,7 @@ export const MediaThumbnail = ({
)
}
export const PhotoThumbnail = styled.div`
export const MediaPlaceholder = styled.div`
flex-grow: 1;
height: 200px;
width: 300px;

View File

@@ -1,7 +1,6 @@
import React, { useContext, useEffect } from 'react'
import styled from 'styled-components'
import { Loader } from 'semantic-ui-react'
import { MediaThumbnail, PhotoThumbnail } from './MediaThumbnail'
import { MediaThumbnail, MediaPlaceholder } from './MediaThumbnail'
import PresentView from './presentView/PresentView'
import { useTranslation } from 'react-i18next'
import { PresentMediaProps_Media } from './presentView/PresentMedia'
@@ -37,10 +36,6 @@ const PhotoFiller = styled.div`
flex-grow: 999999;
`
const ClearWrap = styled.div`
clear: both;
`
export interface PhotoGalleryProps_Media extends PresentMediaProps_Media {
thumbnail: sidebarPhoto_media_thumbnail | null
favorite?: boolean
@@ -109,16 +104,13 @@ const PhotoGallery = ({
})
} else {
for (let i = 0; i < 6; i++) {
photoElements.push(<PhotoThumbnail key={i} />)
photoElements.push(<MediaPlaceholder key={i} />)
}
}
return (
<ClearWrap>
<>
<Gallery data-testid="photo-gallery-wrapper">
<Loader active={loading}>
{t('general.loading.media', 'Loading media')}
</Loader>
{photoElements}
<PhotoFiller />
</Gallery>
@@ -128,7 +120,7 @@ const PhotoGallery = ({
dispatchMedia={dispatchMedia}
/>
)}
</ClearWrap>
</>
)
}

View File

@@ -173,9 +173,6 @@ const TimelineGallery = () => {
return (
<>
<Loader active={loading}>
{t('general.loading.timeline', 'Loading timeline')}
</Loader>
<FavoritesCheckbox
onlyFavorites={onlyFavorites}
setOnlyFavorites={setOnlyFavorites}

1
ui/src/favicon.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -2,18 +2,23 @@ import { useCallback } from 'react'
import { OrderDirection } from '../../__generated__/globalTypes'
import { UrlKeyValuePair, UrlParams } from './useURLParameters'
export type MediaOrdering = {
orderBy: string | null
orderDirection: OrderDirection | null
}
export type SetOrderingFn = (args: {
orderBy?: string
orderDirection?: OrderDirection
}) => void
function useOrderingParams({ getParam, setParams }: UrlParams) {
const orderBy = getParam('orderBy', 'date_shot')
const orderDirStr = getParam('orderDirection', 'ASC') || 'hello'
const orderDirection = orderDirStr as OrderDirection
type setOrderingFn = (args: {
orderBy?: string
orderDirection?: OrderDirection
}) => void
const setOrdering: setOrderingFn = useCallback(
const setOrdering: SetOrderingFn = useCallback(
({ orderBy, orderDirection }) => {
const updatedParams: UrlKeyValuePair[] = []
if (orderBy !== undefined) {

View File

@@ -1,45 +1,51 @@
import React from 'react'
import React, { forwardRef } 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}
/>
)
export const TextField = forwardRef(
(
{ label, error, className, ...inputProps }: TextFieldProps,
ref: React.ForwardedRef<HTMLInputElement>
) => {
let variant = 'bg-white border-gray-200 focus:border-blue-400'
if (error)
variant =
'bg-red-50 border-red-200 focus:border-red-400 focus:ring-red-100'
let errorElm = null
if (error) errorElm = <div>{error}</div>
const input = (
<input
className={`block border rounded-md h-10 w-full focus:ring-2 focus:outline-none px-2 ${variant}`}
{...inputProps}
ref={ref}
/>
)
let errorElm = null
if (error) errorElm = <div className="text-red-800">{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>
)
}
if (label) {
return (
<label className={`block my-4 ${className}`}>
<span className="block text-xs uppercase font-semibold mb-1">
{label}
</span>
<div className={className}>
{input}
{errorElm}
</label>
</div>
)
}
return (
<div className={className}>
{input}
{errorElm}
</div>
)
}
)
export const Submit = (props: React.InputHTMLAttributes<HTMLInputElement>) => {
return (

View File

@@ -1,9 +1,17 @@
import { defineConfig } from 'vite'
import reactRefresh from '@vitejs/plugin-react-refresh'
import { babel } from '@rollup/plugin-babel'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [reactRefresh()],
plugins: [
reactRefresh(),
// babel({
// babelHelpers: 'bundled',
// exclude: 'node_modules/**',
// extensions: ['.js', '.jsx', '.ts', '.tsx'],
// }),
],
server: {
port: 1234,
},