diff --git a/ui/src/components/AlbumFilter.tsx b/ui/src/components/AlbumFilter.tsx deleted file mode 100644 index d391cded..00000000 --- a/ui/src/components/AlbumFilter.tsx +++ /dev/null @@ -1,104 +0,0 @@ -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 ( - - ) -} - -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) => { - 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 && ( - - )} - {t('album_filter.sort_by', 'Sort by')} - - - - - - ) -} - -export default AlbumFilter diff --git a/ui/src/components/album/AlbumFilter.tsx b/ui/src/components/album/AlbumFilter.tsx new file mode 100644 index 00000000..509115cf --- /dev/null +++ b/ui/src/components/album/AlbumFilter.tsx @@ -0,0 +1,133 @@ +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' +import Checkbox from '../../primitives/form/Checkbox' + +import { ReactComponent as SortingIcon } from './icons/sorting.svg' +import { ReactComponent as DirectionIcon } from './icons/direction-arrow.svg' + +import Dropdown from '../../primitives/form/Dropdown' + +type FavoriteCheckboxProps = { + onlyFavorites: boolean + setOnlyFavorites(favorites: boolean): void +} + +export const FavoritesCheckbox = ({ + onlyFavorites, + setOnlyFavorites, +}: FavoriteCheckboxProps) => { + const { t } = useTranslation() + + return ( + setOnlyFavorites(e.target.checked)} + /> + ) +} + +type SortingOptionsProps = { + ordering?: MediaOrdering + setOrdering?: SetOrderingFn +} + +const SortingOptions = ({ setOrdering, ordering }: SortingOptionsProps) => { + const { t } = useTranslation() + + const changeOrderDirection = () => { + if (setOrdering && ordering) { + setOrdering({ + orderDirection: + ordering.orderDirection == OrderDirection.ASC + ? OrderDirection.DESC + : OrderDirection.ASC, + }) + } + } + + const changeOrderBy = (value: string) => { + if (setOrdering) { + setOrdering({ orderBy: value }) + } + } + + const sortingOptions = [ + { + value: 'date_shot', + label: t('album_filter.sorting_options.date_shot', 'Date shot'), + }, + { + value: 'updated_at', + label: t('album_filter.sorting_options.date_imported', 'Date imported'), + }, + { + value: 'title', + label: t('album_filter.sorting_options.title', 'Title'), + }, + { + value: 'type', + label: t('album_filter.sorting_options.type', 'Kind'), + }, + ] + + return ( +
+ + +
+ + +
+
+ ) +} + +type AlbumFilterProps = { + onlyFavorites: boolean + setOnlyFavorites?(favorites: boolean): void + ordering?: MediaOrdering + setOrdering?: SetOrderingFn +} + +const AlbumFilter = ({ + onlyFavorites, + setOnlyFavorites, + setOrdering, + ordering, +}: AlbumFilterProps) => { + return ( +
+ + {authToken() && setOnlyFavorites && ( + + )} +
+ ) +} + +export default AlbumFilter diff --git a/ui/src/components/AlbumTitle.tsx b/ui/src/components/album/AlbumTitle.tsx similarity index 65% rename from ui/src/components/AlbumTitle.tsx rename to ui/src/components/album/AlbumTitle.tsx index 5c6fd7aa..76b989ac 100644 --- a/ui/src/components/AlbumTitle.tsx +++ b/ui/src/components/album/AlbumTitle.tsx @@ -1,11 +1,14 @@ import React, { useEffect, useContext } from 'react' import { Link } from 'react-router-dom' import styled from 'styled-components' -import { SidebarContext } from './sidebar/Sidebar' -import AlbumSidebar from './sidebar/AlbumSidebar' +import { SidebarContext } from '../sidebar/Sidebar' +import AlbumSidebar from '../sidebar/AlbumSidebar' import { useLazyQuery, gql } from '@apollo/client' -import { authToken } from '../helpers/authentication' +import { authToken } from '../../helpers/authentication' import { albumPathQuery } from './__generated__/albumPathQuery' +import useDelay from '../../hooks/useDelay' + +import { ReactComponent as GearIcon } from './icons/gear.svg' const BreadcrumbList = styled.ol` & li::after { @@ -56,7 +59,21 @@ const AlbumTitle = ({ album, disableLink = false }: AlbumTitleProps) => { } }, [album]) - if (!album) return
+ const delay = useDelay(200, [album]) + console.log('delay', delay) + + if (!album) { + return ( +
+
+
+
+ ) + } let title = {album.title} @@ -76,20 +93,21 @@ const AlbumTitle = ({ album, disableLink = false }: AlbumTitleProps) => { } return ( -
-
+
+
-

{title}

+

{title}

{authToken() && ( )}
diff --git a/ui/src/components/album/__generated__/albumPathQuery.ts b/ui/src/components/album/__generated__/albumPathQuery.ts new file mode 100644 index 00000000..6d141f5d --- /dev/null +++ b/ui/src/components/album/__generated__/albumPathQuery.ts @@ -0,0 +1,32 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: albumPathQuery +// ==================================================== + +export interface albumPathQuery_album_path { + __typename: 'Album' + id: string + title: string +} + +export interface albumPathQuery_album { + __typename: 'Album' + id: string + path: albumPathQuery_album_path[] +} + +export interface albumPathQuery { + /** + * Get album by id, user must own the album or be admin + * If valid tokenCredentials are provided, the album may be retrived without further authentication + */ + album: albumPathQuery_album +} + +export interface albumPathQueryVariables { + id: string +} diff --git a/ui/src/components/album/icons/direction-arrow.svg b/ui/src/components/album/icons/direction-arrow.svg new file mode 100644 index 00000000..1860effe --- /dev/null +++ b/ui/src/components/album/icons/direction-arrow.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/ui/src/components/album/icons/gear.svg b/ui/src/components/album/icons/gear.svg new file mode 100644 index 00000000..8e48f3f5 --- /dev/null +++ b/ui/src/components/album/icons/gear.svg @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/ui/src/components/album/icons/sorting.svg b/ui/src/components/album/icons/sorting.svg new file mode 100644 index 00000000..e2ee5c80 --- /dev/null +++ b/ui/src/components/album/icons/sorting.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/ui/src/components/layout/__generated__/adminQuery.ts b/ui/src/components/layout/__generated__/adminQuery.ts new file mode 100644 index 00000000..2301f231 --- /dev/null +++ b/ui/src/components/layout/__generated__/adminQuery.ts @@ -0,0 +1,20 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: adminQuery +// ==================================================== + +export interface adminQuery_myUser { + __typename: 'User' + admin: boolean +} + +export interface adminQuery { + /** + * Information about the currently logged in user + */ + myUser: adminQuery_myUser +} diff --git a/ui/src/components/layout/__generated__/mapboxEnabledQuery.ts b/ui/src/components/layout/__generated__/mapboxEnabledQuery.ts new file mode 100644 index 00000000..06007845 --- /dev/null +++ b/ui/src/components/layout/__generated__/mapboxEnabledQuery.ts @@ -0,0 +1,15 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: mapboxEnabledQuery +// ==================================================== + +export interface mapboxEnabledQuery { + /** + * Get the mapbox api token, returns null if mapbox is not enabled + */ + mapboxToken: string | null +} diff --git a/ui/src/components/layout/menu-icons/albums.svg b/ui/src/components/layout/menu-icons/albums.svg new file mode 100644 index 00000000..1e93f5e5 --- /dev/null +++ b/ui/src/components/layout/menu-icons/albums.svg @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/ui/src/components/layout/menu-icons/people.svg b/ui/src/components/layout/menu-icons/people.svg new file mode 100644 index 00000000..2af9060c --- /dev/null +++ b/ui/src/components/layout/menu-icons/people.svg @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/ui/src/components/layout/menu-icons/photos.svg b/ui/src/components/layout/menu-icons/photos.svg new file mode 100644 index 00000000..5b801f13 --- /dev/null +++ b/ui/src/components/layout/menu-icons/photos.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/ui/src/components/layout/menu-icons/places.svg b/ui/src/components/layout/menu-icons/places.svg new file mode 100644 index 00000000..3032ea3e --- /dev/null +++ b/ui/src/components/layout/menu-icons/places.svg @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/ui/src/components/layout/menu-icons/settings.svg b/ui/src/components/layout/menu-icons/settings.svg new file mode 100644 index 00000000..709a765b --- /dev/null +++ b/ui/src/components/layout/menu-icons/settings.svg @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/ui/src/hooks/useDelay.ts b/ui/src/hooks/useDelay.ts new file mode 100644 index 00000000..5a45309b --- /dev/null +++ b/ui/src/hooks/useDelay.ts @@ -0,0 +1,23 @@ +import { useLayoutEffect, useState, useRef } from 'react' + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function useDelay(wait: number, deps: any[] = []) { + const [_, triggerUpdate] = useState(false) + const done = useRef(false) + + useLayoutEffect(() => { + const handle = setTimeout(() => { + done.current = true + triggerUpdate(x => !x) + }, wait) + + return () => { + done.current = false + clearTimeout(handle) + } + }, deps) + + return done.current +} + +export default useDelay diff --git a/ui/src/primitives/form/Checkbox.tsx b/ui/src/primitives/form/Checkbox.tsx new file mode 100644 index 00000000..a2b12683 --- /dev/null +++ b/ui/src/primitives/form/Checkbox.tsx @@ -0,0 +1,54 @@ +import React from 'react' +import styled from 'styled-components' + +const CheckboxLabelWrapper = styled.label` + & input[type='checkbox'] { + appearance: none; + outline: none; + } + + & input[type='checkbox'] + span { + cursor: pointer; + } + + & input[type='checkbox'] + span::before { + content: ''; + background-repeat: no-repeat; + background-position: center; + width: 12px; + height: 12px; + background-color: #f9fafb; + border: 1px solid #aaa; + display: inline-block; + border-radius: 3px; + cursor: pointer; + margin-right: 8px; + margin-bottom: -1px; + } + + & input[type='checkbox']:checked + span::before { + background-color: #0072ff; + border-color: #109dff; + background-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='7px' height='4px' viewBox='0 0 7 4' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Ctitle%3EPath 5%3C/title%3E%3Cg id='Page-1' stroke='none' stroke-width='1.5' fill='none' fill-rule='evenodd'%3E%3Cpolyline id='Path-5' stroke='%23FFFFFF' points='1 1.93487039 2.77174339 3.70661378 6.47835718 2.27373675e-13'%3E%3C/polyline%3E%3C/g%3E%3C/svg%3E"); + } + + & input[type='checkbox']:focus + span::before { + outline: 2px solid #86c6ff; + border-color: #109dff; + } +` + +type CheckboxProps = React.InputHTMLAttributes & { + label: string +} + +const Checkbox = ({ label, className, ...props }: CheckboxProps) => { + return ( + + + {label} + + ) +} + +export default Checkbox diff --git a/ui/src/primitives/form/Dropdown.tsx b/ui/src/primitives/form/Dropdown.tsx new file mode 100644 index 00000000..f69e22ae --- /dev/null +++ b/ui/src/primitives/form/Dropdown.tsx @@ -0,0 +1,52 @@ +import React from 'react' +import styled from 'styled-components' + +const DropdownStyledSelect = styled.select` + appearance: none; + + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='9px' height='5px' viewBox='0 0 9 5'%3E%3Cpolygon fill='%23D8D8D8' points='0 0 8.36137659 0 4.1806883 4.1806883'%3E%3C/polygon%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: center right 10px; +` + +type DropdownItem = { + value: string + label: string +} + +type DropdownProps = React.SelectHTMLAttributes & { + items: DropdownItem[] + selected?: string + setSelected(label: string): void +} + +const Dropdown = ({ + items, + selected, + setSelected, + ...otherProps +}: DropdownProps) => { + const onChange = (e: React.ChangeEvent) => { + setSelected(e.target.value) + otherProps.onChange && otherProps.onChange(e) + } + + const options = items.map(({ value, label }) => ( + + )) + + return ( + + {options} + + ) +} + +export default Dropdown