mirror of
https://git.vectorsigma.ru/public/photoview.git
synced 2026-08-03 21:09:05 +00:00
General work towards new design
This commit is contained in:
@@ -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
326
ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
104
ui/src/components/AlbumFilter.tsx
Normal file
104
ui/src/components/AlbumFilter.tsx
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -181,7 +181,7 @@ export const MediaThumbnail = ({
|
||||
)
|
||||
}
|
||||
|
||||
export const PhotoThumbnail = styled.div`
|
||||
export const MediaPlaceholder = styled.div`
|
||||
flex-grow: 1;
|
||||
height: 200px;
|
||||
width: 300px;
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
1
ui/src/favicon.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 17 KiB |
@@ -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) {
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user