├── README.md ├── .eslintignore ├── src ├── constants │ ├── table.js │ ├── productionStatusTypes.js │ ├── statusTypes.js │ └── queryTypes.js ├── styles │ ├── components │ │ ├── _ItemTitleBlock.scss │ │ ├── _HistoryList.scss │ │ ├── _LinkButton.scss │ │ ├── _SearchList.scss │ │ ├── _SidebarItem.scss │ │ ├── _ItemView.scss │ │ ├── _ItemRefreshButton.scss │ │ ├── _ItemThumbnail.scss │ │ ├── _SelectBar.scss │ │ ├── _ProviderTest.scss │ │ ├── _AutosaveInput.scss │ │ ├── _StorageEdit.scss │ │ ├── _Provider.scss │ │ ├── _Header.scss │ │ └── _Floating.scss │ ├── containers │ │ ├── _Popup.scss │ │ ├── _Sidebar.scss │ │ ├── _Storage.scss │ │ ├── _History.scss │ │ ├── _Search.scss │ │ ├── _Providers.scss │ │ └── _Items.scss │ ├── _functions.scss │ ├── modules │ │ ├── _Utils.scss │ │ ├── _ItemTags.scss │ │ └── _CompactTable.scss │ ├── _breakpoints.scss │ ├── _theme.scss │ └── main.scss ├── images │ ├── icon.xcf │ ├── icon16.png │ ├── icon19.png │ ├── icon32.png │ ├── icon38.png │ ├── icon48.png │ └── icon128.png ├── utils │ ├── function.js │ ├── string.js │ ├── math.js │ ├── array.js │ ├── date.js │ └── object.js ├── api │ ├── tabs.js │ └── permissions.js ├── containers │ ├── Popup.js │ ├── Storage.js │ ├── Items.js │ ├── Sidebar.js │ ├── Search.js │ ├── Providers.js │ ├── History.js │ └── Header.js ├── components │ ├── ItemListCreator.js │ ├── ItemListNotes.js │ ├── ItemListGenres.js │ ├── ItemListLength.js │ ├── ItemListThumbnail.js │ ├── ItemListCharacters.js │ ├── ItemListStatusDate.js │ ├── SidebarInfo.js │ ├── FullDate.js │ ├── ItemListTitle.js │ ├── ItemThumbnail.js │ ├── ItemTitleBlock.js │ ├── SidebarHistory.js │ ├── LinkButton.js │ ├── ItemListProductionStatus.js │ ├── ItemTags.js │ ├── ItemNotes.js │ ├── HistoryRaw.js │ ├── SelectBar.js │ ├── Markdown.js │ ├── ItemGenres.js │ ├── ItemLength.js │ ├── ItemCharacters.js │ ├── Floating.js │ ├── ItemList.js │ ├── ProviderList.js │ ├── ItemInfo.js │ ├── SidebarItem.js │ ├── Provider.js │ ├── ItemStatus.js │ ├── ItemHistory.js │ ├── Item.js │ ├── ItemProductionStatus.js │ ├── ProviderTest.js │ ├── AutosaveInput.js │ ├── ItemRefreshButton.js │ ├── SearchList.js │ ├── HistoryList.js │ ├── PopupInfo.js │ ├── ItemView.js │ └── StorageEdit.js ├── index.html ├── manifest.json ├── data │ ├── history.js │ ├── provider.js │ ├── queries.js │ └── database.js └── index.entry.js ├── .gitignore ├── .editorconfig ├── deploy.js ├── .github └── workflows │ └── ci.yml ├── package.json ├── .sass-lint.yml ├── webpack.config.js ├── .eslintrc.json └── LICENSE /README.md: -------------------------------------------------------------------------------- 1 | # media-db 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Build output 2 | dist 3 | -------------------------------------------------------------------------------- /src/constants/table.js: -------------------------------------------------------------------------------- 1 | export const ROW_LIMIT = 32; 2 | -------------------------------------------------------------------------------- /src/styles/components/_ItemTitleBlock.scss: -------------------------------------------------------------------------------- 1 | .ItemTitleBlock { 2 | text-align: center; 3 | } 4 | -------------------------------------------------------------------------------- /src/images/icon.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erikdesjardins/media-db/master/src/images/icon.xcf -------------------------------------------------------------------------------- /src/images/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erikdesjardins/media-db/master/src/images/icon16.png -------------------------------------------------------------------------------- /src/images/icon19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erikdesjardins/media-db/master/src/images/icon19.png -------------------------------------------------------------------------------- /src/images/icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erikdesjardins/media-db/master/src/images/icon32.png -------------------------------------------------------------------------------- /src/images/icon38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erikdesjardins/media-db/master/src/images/icon38.png -------------------------------------------------------------------------------- /src/images/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erikdesjardins/media-db/master/src/images/icon48.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /bower_components 2 | /node_modules 3 | /dist 4 | /.idea 5 | *.iml 6 | mediadb-backup-*.json 7 | -------------------------------------------------------------------------------- /src/images/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erikdesjardins/media-db/master/src/images/icon128.png -------------------------------------------------------------------------------- /src/styles/components/_HistoryList.scss: -------------------------------------------------------------------------------- 1 | .HistoryList { 2 | &-legend { 3 | text-align: center; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/styles/components/_LinkButton.scss: -------------------------------------------------------------------------------- 1 | .LinkButton { 2 | &:not([href]) { 3 | cursor: default; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/styles/components/_SearchList.scss: -------------------------------------------------------------------------------- 1 | .SearchList { 2 | &-legend { 3 | text-align: center; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/styles/components/_SidebarItem.scss: -------------------------------------------------------------------------------- 1 | .SidebarItem { 2 | &-legend { 3 | text-align: center; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/function.js: -------------------------------------------------------------------------------- 1 | export function pipe(init, ...fs) { 2 | return fs.reduce((x, f) => f(x), init); 3 | } 4 | -------------------------------------------------------------------------------- /src/styles/containers/_Popup.scss: -------------------------------------------------------------------------------- 1 | @import '../theme'; 2 | 3 | .Popup { 4 | padding: $Theme-spacing--medium; 5 | } 6 | -------------------------------------------------------------------------------- /src/styles/_functions.scss: -------------------------------------------------------------------------------- 1 | @function fade-in($color) { 2 | @return linear-gradient(to right, rgba($color, 0), rgba($color, 1)); 3 | } 4 | -------------------------------------------------------------------------------- /src/styles/components/_ItemView.scss: -------------------------------------------------------------------------------- 1 | @import '../theme'; 2 | 3 | .ItemView { 4 | &-controls { 5 | text-align: center; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/styles/containers/_Sidebar.scss: -------------------------------------------------------------------------------- 1 | @import '../theme'; 2 | 3 | .Sidebar { 4 | position: sticky; 5 | top: $Theme-spacing--medium; 6 | } 7 | -------------------------------------------------------------------------------- /src/styles/containers/_Storage.scss: -------------------------------------------------------------------------------- 1 | @import '../theme'; 2 | 3 | .Storage { 4 | width: 100%; 5 | padding: $Theme-spacing--medium; 6 | } 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = tab 3 | end_of_line = lf 4 | charset = utf-8 5 | trim_trailing_whitespace = true 6 | insert_final_newline = true 7 | -------------------------------------------------------------------------------- /src/styles/components/_ItemRefreshButton.scss: -------------------------------------------------------------------------------- 1 | .ItemRefreshButton { 2 | line-height: 0; // avoid emoji making the line taller 3 | text-decoration: none; 4 | } 5 | -------------------------------------------------------------------------------- /src/api/tabs.js: -------------------------------------------------------------------------------- 1 | export async function activeTab() { 2 | const [tab] = await chrome.tabs.query({ active: true, lastFocusedWindow: true }); 3 | return tab; 4 | } 5 | -------------------------------------------------------------------------------- /src/styles/components/_ItemThumbnail.scss: -------------------------------------------------------------------------------- 1 | .ItemThumbnail { 2 | display: flex; 3 | 4 | &-img { 5 | max-width: 100%; 6 | max-height: 50vh; 7 | margin: auto; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/constants/productionStatusTypes.js: -------------------------------------------------------------------------------- 1 | export const INCOMPLETE = 'INCOMPLETE'; 2 | export const HIATUS = 'HIATUS'; 3 | export const COMPLETE = 'COMPLETE'; 4 | export const CANCELLED = 'CANCELLED'; 5 | -------------------------------------------------------------------------------- /src/utils/string.js: -------------------------------------------------------------------------------- 1 | export function randomId(length = 16) { 2 | let id = ''; 3 | while (id.length < length) { 4 | id += String(Math.random()).slice(2); 5 | } 6 | return id.slice(0, length); 7 | } 8 | -------------------------------------------------------------------------------- /src/containers/Popup.js: -------------------------------------------------------------------------------- 1 | import PopupInfo from '../components/PopupInfo'; 2 | 3 | export default function Popup() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/constants/statusTypes.js: -------------------------------------------------------------------------------- 1 | export const WAITING = 'WAITING'; 2 | export const PENDING = 'PENDING'; 3 | export const IN_PROGRESS = 'IN_PROGRESS'; 4 | export const COMPLETE = 'COMPLETE'; 5 | export const REJECTED = 'REJECTED'; 6 | -------------------------------------------------------------------------------- /src/containers/Storage.js: -------------------------------------------------------------------------------- 1 | import StorageEdit from '../components/StorageEdit'; 2 | 3 | export default function Storage() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/styles/modules/_Utils.scss: -------------------------------------------------------------------------------- 1 | @import '../theme'; 2 | 3 | .Utils { 4 | &-flexSpacer { 5 | flex-grow: 1; 6 | } 7 | 8 | &-fieldset { 9 | &--noPadding { 10 | padding: $Theme-spacing--small 0 0 0; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/styles/components/_SelectBar.scss: -------------------------------------------------------------------------------- 1 | @import '../theme'; 2 | 3 | .SelectBar { 4 | &-button { 5 | border-radius: $Theme-borderRadius--small; 6 | 7 | &--active { 8 | outline: $Theme-borderWidth solid; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/styles/components/_ProviderTest.scss: -------------------------------------------------------------------------------- 1 | @import '../theme'; 2 | 3 | .ProviderTest { 4 | &-url { 5 | display: block; 6 | width: 100%; 7 | } 8 | 9 | &-output { 10 | &--stale { 11 | opacity: $Theme-stale; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/styles/components/_AutosaveInput.scss: -------------------------------------------------------------------------------- 1 | @import '../theme'; 2 | 3 | .AutosaveInput { 4 | display: block; 5 | width: 100%; 6 | resize: vertical; 7 | 8 | &--dirty { 9 | border-color: $Theme-warning; 10 | outline-color: $Theme-warning; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/utils/math.js: -------------------------------------------------------------------------------- 1 | export function pmod(x, n) { 2 | // "positive modulus" (there's probably a proper name for this...maybe this is what euclidean modulo is) 3 | return ((x % n) + n) % n; 4 | } 5 | 6 | export function roundDownToMultiple(x, n) { 7 | return x - (x % n); 8 | } 9 | -------------------------------------------------------------------------------- /src/components/ItemListCreator.js: -------------------------------------------------------------------------------- 1 | import Markdown from './Markdown'; 2 | 3 | export default function ItemListCreator({ item }) { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/constants/queryTypes.js: -------------------------------------------------------------------------------- 1 | export const ACTIVE_TAB = 'ACTIVE_TAB'; 2 | export const ITEM = 'ITEM'; 3 | export const ITEMS = 'ITEMS'; 4 | export const FROM_PROVIDER = 'FROM_PROVIDER'; 5 | export const PROVIDER = 'PROVIDER'; 6 | export const PROVIDERS = 'PROVIDERS'; 7 | export const RAW_DATA = 'RAW_DATA'; 8 | -------------------------------------------------------------------------------- /src/components/ItemListNotes.js: -------------------------------------------------------------------------------- 1 | import Markdown from './Markdown'; 2 | 3 | export default function ItemListNotes({ item }) { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/components/ItemListGenres.js: -------------------------------------------------------------------------------- 1 | import Markdown from './Markdown'; 2 | 3 | export default function ItemListGenres({ item }) { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/components/ItemListLength.js: -------------------------------------------------------------------------------- 1 | import numeral from 'numeral'; 2 | 3 | export default function ItemListLength({ item }) { 4 | return ( 5 |
6 | 7 | {numeral(item.length).format('0a')} 8 | 9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | media-db 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /src/components/ItemListThumbnail.js: -------------------------------------------------------------------------------- 1 | 2 | export default function ItemListThumbnail({ item }) { 3 | return ( 4 |
5 | {item.tinyThumbnail ? 6 | : 7 | null 8 | } 9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /src/styles/modules/_ItemTags.scss: -------------------------------------------------------------------------------- 1 | @import '../theme'; 2 | 3 | a[href='#info'] { 4 | float: right; 5 | color: inherit; 6 | font-size: x-small; 7 | text-decoration: none; 8 | cursor: default; 9 | 10 | &:visited { 11 | color: inherit; 12 | } 13 | 14 | &:not(:last-of-type)::before { 15 | content: ', '; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/components/ItemListCharacters.js: -------------------------------------------------------------------------------- 1 | import Markdown from './Markdown'; 2 | 3 | export default function ItemListCharacters({ item }) { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/components/ItemListStatusDate.js: -------------------------------------------------------------------------------- 1 | import FullDate from './FullDate'; 2 | 3 | export default function ItemListStatusDate({ item }) { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/styles/_breakpoints.scss: -------------------------------------------------------------------------------- 1 | $breakpoints: ( 2 | xs: 0, 3 | sm: 768px, 4 | md: 992px, 5 | lg: 1200px, 6 | ); 7 | 8 | @mixin breakpoint($name) { 9 | $min: map-get($breakpoints, $name); 10 | @if not $min { 11 | @error 'breakpoint `#{$name}` not found in `#{$breakpoints}`'; 12 | } 13 | @media screen and (min-width: $min) { 14 | @content 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/styles/components/_StorageEdit.scss: -------------------------------------------------------------------------------- 1 | @import '../theme'; 2 | 3 | .StorageEdit { 4 | &-showTextarea { 5 | border-radius: $Theme-borderRadius--small; 6 | 7 | &--active { 8 | outline: $Theme-borderWidth solid; 9 | } 10 | } 11 | 12 | &-textarea { 13 | width: 100%; 14 | font-family: monospace; 15 | overflow-y: scroll; 16 | resize: vertical; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/components/SidebarInfo.js: -------------------------------------------------------------------------------- 1 | import { useQueryItem } from '../data/queries'; 2 | import ItemInfo from './ItemInfo'; 3 | 4 | export default function SidebarInfo({ itemId: id }) { 5 | const { isLoading, data: item } = useQueryItem(id, { keepPreviousData: true }); 6 | 7 | if (isLoading) { 8 | return null; 9 | } 10 | 11 | return ( 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/components/FullDate.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { formatFullDate } from '../utils/date'; 3 | 4 | export default React.memo(function FullDate({ date }) { 5 | const seconds = new Date(date).getSeconds(); 6 | return ( 7 | 11 | {formatFullDate(date)} 12 | 13 | ); 14 | }); 15 | -------------------------------------------------------------------------------- /src/styles/components/_Provider.scss: -------------------------------------------------------------------------------- 1 | @import '../theme'; 2 | 3 | .Provider { 4 | position: relative; 5 | 6 | &:not(:first-of-type) { 7 | margin-top: $Theme-spacing--medium; 8 | } 9 | 10 | &-removeButton { 11 | position: absolute; 12 | top: $Theme-spacing--small; 13 | right: #{$Theme-spacing--small + $Theme-scrollbarWidth}; 14 | } 15 | 16 | &-textarea { 17 | font-family: monospace; 18 | overflow-y: scroll; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/components/ItemListTitle.js: -------------------------------------------------------------------------------- 1 | import Markdown from './Markdown'; 2 | 3 | export default function ItemListTitle({ item }) { 4 | return ( 5 |
6 |
7 |

8 | {item.title} 9 | {item.tags ? : null} 10 |

11 |
12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/components/ItemThumbnail.js: -------------------------------------------------------------------------------- 1 | import ItemRefreshButton from './ItemRefreshButton'; 2 | 3 | export default function ItemThumbnail({ item }) { 4 | return ( 5 |
6 | {item.thumbnail ? 7 | : 8 | null 9 | } 10 | 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/components/ItemTitleBlock.js: -------------------------------------------------------------------------------- 1 | import ItemRefreshButton from './ItemRefreshButton'; 2 | 3 | export default function ItemTitleBlock({ item }) { 4 | return ( 5 |

6 | {item.title} 7 | 8 | {' by '}{item.creator} 9 | {' '} 10 | 15 | 16 |

17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/components/SidebarHistory.js: -------------------------------------------------------------------------------- 1 | import { useQueryItemHistory } from '../data/queries'; 2 | import ItemHistory from './ItemHistory'; 3 | 4 | export default function SidebarHistory({ itemId: id, onClickItemHistory }) { 5 | const { isLoading, data: history } = useQueryItemHistory(id, { keepPreviousData: true }); 6 | 7 | if (isLoading) { 8 | return null; 9 | } 10 | 11 | return ( 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/api/permissions.js: -------------------------------------------------------------------------------- 1 | // may have to wait for https://groups.google.com/a/chromium.org/g/chromium-extensions/c/EnUmtHWOI9o to enable this, 2 | // since you currently can't have optional host permissions 3 | // "host_permissions": [ 4 | // "https://*/*" 5 | // ], 6 | export async function requestPermissionForUrl(url) { 7 | const perms = { origins: [url] }; 8 | 9 | const alreadyHave = await chrome.permissions.contains(perms); 10 | if (alreadyHave) { 11 | return; 12 | } 13 | 14 | await chrome.permissions.request(perms); 15 | } 16 | -------------------------------------------------------------------------------- /src/components/LinkButton.js: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import React from 'react'; 3 | 4 | export default React.memo(function LinkButton({ className, title, disabled, onClick, children }) { 5 | const handleClick = e => { 6 | e.preventDefault(); 7 | if (!disabled) { 8 | onClick(); 9 | } 10 | }; 11 | 12 | return ( 13 | 19 | {children} 20 | 21 | ); 22 | }); 23 | -------------------------------------------------------------------------------- /src/components/ItemListProductionStatus.js: -------------------------------------------------------------------------------- 1 | import * as productionStatusTypes from '../constants/productionStatusTypes'; 2 | 3 | const statusIcons = { 4 | [productionStatusTypes.INCOMPLETE]: '✏️', 5 | [productionStatusTypes.COMPLETE]: '✔️', 6 | [productionStatusTypes.HIATUS]: '⏸️', 7 | [productionStatusTypes.CANCELLED]: '🚫', 8 | }; 9 | 10 | export default function ItemListProductionStatus({ item }) { 11 | return ( 12 |
13 | {statusIcons[item.productionStatus]} 14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/components/ItemTags.js: -------------------------------------------------------------------------------- 1 | import { useMutationUpdateItem } from '../data/queries'; 2 | import AutosaveInput from './AutosaveInput'; 3 | 4 | export default function ItemTags({ item }) { 5 | const mutation = useMutationUpdateItem(item.id); 6 | 7 | const handleSave = value => { 8 | mutation.mutate({ tags: value }); 9 | }; 10 | 11 | return ( 12 |
13 |

14 | {'Tags'} 15 |

16 | 21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/components/ItemNotes.js: -------------------------------------------------------------------------------- 1 | import { useMutationUpdateItem } from '../data/queries'; 2 | import AutosaveInput from './AutosaveInput'; 3 | 4 | export default function ItemNotes({ item }) { 5 | const mutation = useMutationUpdateItem(item.id); 6 | 7 | const handleSave = value => { 8 | mutation.mutate({ notes: value }); 9 | }; 10 | 11 | return ( 12 |
13 |

14 | {'Notes'} 15 |

16 | 22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/components/HistoryRaw.js: -------------------------------------------------------------------------------- 1 | import { useQueryItemHistoryAt } from '../data/queries'; 2 | import FullDate from './FullDate'; 3 | 4 | export default function HistoryRaw({ itemId: id, date }) { 5 | const { isLoading, data: historyItem } = useQueryItemHistoryAt(id, date, { keepPreviousData: true }); 6 | 7 | if (isLoading) { 8 | return null; 9 | } 10 | 11 | return ( 12 |
13 | 14 | 15 | 16 | 17 | {JSON.stringify(historyItem, null, 2)} 18 | 19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/containers/Items.js: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import { Outlet, useNavigate } from 'react-router-dom'; 3 | import ItemView from '../components/ItemView'; 4 | 5 | export default function Items() { 6 | const navigate = useNavigate(); 7 | 8 | const handleClickItem = useCallback(item => { 9 | navigate(`/items/${btoa(item.id)}`); 10 | }, [navigate]); 11 | 12 | return ( 13 |
14 |
15 | 16 |
17 |
18 | 19 |
20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/containers/Sidebar.js: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import { useNavigate, useParams } from 'react-router-dom'; 3 | import SidebarItem from '../components/SidebarItem'; 4 | 5 | export default function Sidebar() { 6 | const params = useParams(); 7 | const id = atob(params.id); 8 | 9 | const navigate = useNavigate(); 10 | 11 | const handleClickItemHistory = useCallback(date => { 12 | navigate(`/history/${btoa(id)}/${date}`); 13 | }, [navigate, id]); 14 | 15 | return ( 16 |
17 | 18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /deploy.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-commonjs */ 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const deploy = require('chrome-extension-deploy'); 5 | 6 | deploy({ 7 | clientId: process.env.CHROME_CLIENT_ID, 8 | clientSecret: process.env.CHROME_CLIENT_SECRET, 9 | refreshToken: process.env.CHROME_REFRESH_TOKEN, 10 | id: 'jhneopniogogjaodebilaefmllbffcmf', 11 | zip: fs.readFileSync(path.join(__dirname, 'dist/media-db.zip')), 12 | }).then(() => { 13 | console.log('Deploy complete!'); // eslint-disable-line no-console 14 | }, err => { 15 | process.exitCode = 1; 16 | console.error(err); // eslint-disable-line no-console 17 | }); 18 | -------------------------------------------------------------------------------- /src/components/SelectBar.js: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import React from 'react'; 3 | import { intersperse } from '../utils/array'; 4 | import LinkButton from './LinkButton'; 5 | 6 | export default React.memo(function SelectBar({ options, selected, onSelect }) { 7 | return ( 8 | 9 | {intersperse(options.map(({ value, name }) => ( 10 | onSelect(value)} 14 | > 15 | {name || value} 16 | 17 | )), () => ' ')} 18 | 19 | ); 20 | }); 21 | -------------------------------------------------------------------------------- /src/components/Markdown.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { markdown } from 'snudown-js'; 3 | 4 | const isMarkdown = /[*_~^[&#\n]/; 5 | 6 | export default React.memo(function Markdown({ source, inline = false }) { 7 | if (!isMarkdown.test(source)) { 8 | return (inline ? 9 | {source} : 10 |

{source}

11 | ); 12 | } 13 | 14 | /* eslint-disable react/no-danger */ 15 | return (inline ? 16 | '.length, -'

\n'.length) }}/> : 17 |
18 | ); 19 | }); 20 | -------------------------------------------------------------------------------- /src/components/ItemGenres.js: -------------------------------------------------------------------------------- 1 | import { useMutationUpdateItem } from '../data/queries'; 2 | import AutosaveInput from './AutosaveInput'; 3 | import ItemRefreshButton from './ItemRefreshButton'; 4 | 5 | export default function ItemGenres({ item }) { 6 | const mutation = useMutationUpdateItem(item.id); 7 | 8 | const handleSave = value => { 9 | mutation.mutate({ genres: value }); 10 | }; 11 | 12 | return ( 13 |
14 |

15 | {'Genres'} 16 | {' '} 17 | 21 |

22 | 27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/components/ItemLength.js: -------------------------------------------------------------------------------- 1 | import { useMutationUpdateItem } from '../data/queries'; 2 | import AutosaveInput from './AutosaveInput'; 3 | import ItemRefreshButton from './ItemRefreshButton'; 4 | 5 | export default function ItemLength({ item }) { 6 | const mutation = useMutationUpdateItem(item.id); 7 | 8 | const handleSave = value => { 9 | mutation.mutate({ length: Number(value) }); 10 | }; 11 | 12 | return ( 13 |
14 |

15 | {'Length'} 16 | {' '} 17 | 21 |

22 | 27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/containers/Search.js: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import { Outlet, useNavigate, useParams } from 'react-router-dom'; 3 | import SearchList from '../components/SearchList'; 4 | 5 | export default function Search() { 6 | const params = useParams(); 7 | const query = atob(params.query); 8 | 9 | const navigate = useNavigate(); 10 | 11 | const handleClickItem = useCallback(item => { 12 | navigate(`/search/${btoa(query)}/${btoa(item.id)}`); 13 | }, [navigate, query]); 14 | 15 | return ( 16 |
17 |
18 | 19 |
20 |
21 | 22 |
23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/components/ItemCharacters.js: -------------------------------------------------------------------------------- 1 | import { useMutationUpdateItem } from '../data/queries'; 2 | import AutosaveInput from './AutosaveInput'; 3 | import ItemRefreshButton from './ItemRefreshButton'; 4 | 5 | export default function ItemCharacters({ item }) { 6 | const mutation = useMutationUpdateItem(item.id); 7 | 8 | const handleSave = value => { 9 | mutation.mutate({ characters: value }); 10 | }; 11 | 12 | return ( 13 |
14 |

15 | {'Characters'} 16 | {' '} 17 | 21 |

22 | 27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/components/Floating.js: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | 3 | export default function Floating({ top, right, bottom, left, onBlur, children }) { 4 | const triangleClass = classNames('Floating-triangle', { 5 | 'Floating-triangle--top': top, 6 | 'Floating-triangle--bottom': bottom, 7 | }); 8 | const contentClass = classNames('Floating-content', { 9 | 'Floating-content--top': top, 10 | 'Floating-content--right': right, 11 | 'Floating-content--bottom': bottom, 12 | 'Floating-content--left': left, 13 | }); 14 | return ( 15 |
16 |
17 |
18 | {children} 19 |
20 |
21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/components/ItemList.js: -------------------------------------------------------------------------------- 1 | import Item from './Item'; 2 | 3 | export default function ItemList({ items, onClickItem }) { 4 | return ( 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {items.map(item => ( 21 | 26 | ))} 27 | 28 |
{''}{'Title'}{'Creator'}{'Genres'}{'Characters'}{'Notes'}{'Date'}{'Len.'}{''}
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/containers/Providers.js: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import { useNavigate, useParams } from 'react-router-dom'; 3 | import ProviderList from '../components/ProviderList'; 4 | import ProviderTest from '../components/ProviderTest'; 5 | 6 | export default function Providers() { 7 | const params = useParams(); 8 | const url = atob(params.url || ''); 9 | 10 | const navigate = useNavigate(); 11 | 12 | const handleChangeUrl = useCallback(url => { 13 | navigate(`/providers/${btoa(url)}`); 14 | }, [navigate]); 15 | 16 | return ( 17 |
18 |
19 | 20 |
21 |
22 | 23 |
24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/components/ProviderList.js: -------------------------------------------------------------------------------- 1 | import { useMutationAddProvider, useQueryProviders } from '../data/queries'; 2 | import LinkButton from './LinkButton'; 3 | import Provider from './Provider'; 4 | 5 | export default function ProviderList() { 6 | const { isLoading, data: providers } = useQueryProviders(); 7 | 8 | const mutation = useMutationAddProvider(); 9 | 10 | if (isLoading) { 11 | return null; 12 | } 13 | 14 | const handleAddProvider = () => { 15 | mutation.mutate(); 16 | }; 17 | 18 | return ( 19 |
20 | 21 | 22 | {'Add new provider'} 23 | 24 | 25 | {providers.map(provider => ( 26 | 30 | ))} 31 |
32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "{{prop-loader?name!../package.json}}", 4 | "short_name": "mdb", 5 | "description": "{{prop-loader?description!../package.json}}", 6 | "version": "{{prop-loader?version!../package.json}}", 7 | "minimum_chrome_version": "92", 8 | "permissions": [ 9 | "activeTab", 10 | "unlimitedStorage" 11 | ], 12 | "content_security_policy": { 13 | "extension_pages": "default-src 'self'; connect-src 'self' https:; img-src 'self' https:" 14 | }, 15 | "icons": { 16 | "16": "{{./images/icon16.png}}", 17 | "32": "{{./images/icon32.png}}", 18 | "48": "{{./images/icon48.png}}", 19 | "128": "{{./images/icon128.png}}" 20 | }, 21 | "action": { 22 | "default_icon": { 23 | "19": "{{./images/icon19.png}}", 24 | "38": "{{./images/icon38.png}}" 25 | }, 26 | "default_popup": "{{./index.html}}#/popup" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/components/ItemInfo.js: -------------------------------------------------------------------------------- 1 | import ItemCharacters from './ItemCharacters'; 2 | import ItemGenres from './ItemGenres'; 3 | import ItemLength from './ItemLength'; 4 | import ItemNotes from './ItemNotes'; 5 | import ItemProductionStatus from './ItemProductionStatus'; 6 | import ItemStatus from './ItemStatus'; 7 | import ItemTags from './ItemTags'; 8 | import ItemThumbnail from './ItemThumbnail'; 9 | import ItemTitleBlock from './ItemTitleBlock'; 10 | 11 | export default function ItemInfo({ item }) { 12 | return ( 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/styles/components/_Header.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:math'; 2 | @import '../breakpoints'; 3 | @import '../theme'; 4 | 5 | .Header { 6 | width: 100%; 7 | padding: $Theme-spacing--small; 8 | background: $Theme-background--grey; 9 | 10 | &-header { 11 | display: flex; 12 | align-items: center; 13 | 14 | @include breakpoint(xs) { 15 | width: percentage(math.div(12, 12)); 16 | } 17 | @include breakpoint(sm) { 18 | width: percentage(math.div(10, 12)); 19 | margin-left: percentage(math.div(1, 12)); 20 | } 21 | @include breakpoint(md) { 22 | width: percentage(math.div(8, 12)); 23 | margin-left: percentage(math.div(2, 12)); 24 | } 25 | @include breakpoint(lg) { 26 | width: percentage(math.div(6, 12)); 27 | margin-left: percentage(math.div(3, 12)); 28 | } 29 | } 30 | 31 | &-img { 32 | vertical-align: middle; 33 | } 34 | 35 | &-form { 36 | position: relative; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/containers/History.js: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import { useNavigate, useParams } from 'react-router-dom'; 3 | import HistoryList from '../components/HistoryList'; 4 | import HistoryRaw from '../components/HistoryRaw'; 5 | 6 | export default function History() { 7 | const params = useParams(); 8 | const id = atob(params.id); 9 | const date = parseInt(params.date, 10); 10 | 11 | const navigate = useNavigate(); 12 | 13 | const handleClickItemHistory = useCallback(date => { 14 | navigate(`/history/${btoa(id)}/${date}`, { replace: true }); 15 | }, [navigate, id]); 16 | 17 | return ( 18 |
19 |
20 | 21 |
22 |
23 | 24 |
25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /src/styles/_theme.scss: -------------------------------------------------------------------------------- 1 | // Toplevel config (unlikely to be used elsewhere) 2 | 3 | $Theme-fontFamily: Arial, sans-serif; 4 | $Theme-fontSize: 12px; 5 | $Theme-fontSize--larger: 13px; 6 | $Theme-lineHeight: 1.5; 7 | 8 | $Theme-minBodyWidth: 400px; 9 | 10 | $Theme-scrollbarWidth: 20px; 11 | 12 | // Colors 13 | 14 | $Theme-background: #fff; 15 | $Theme-background--grey: #eee; 16 | $Theme-background--highlight: #ddd; 17 | 18 | $Theme-borderColor: #777; 19 | $Theme-borderColor--light: #e5e3da; 20 | 21 | $Theme-link: #00e; 22 | 23 | $Theme-error: #bc3131; 24 | $Theme-warning: #f7a616; 25 | $Theme-indeterminate: #bd7b40; 26 | $Theme-okay: #63bd40; 27 | 28 | // Opacity 29 | 30 | $Theme-stale: 25%; 31 | 32 | // Border sizes 33 | 34 | $Theme-borderWidth: 1px; 35 | 36 | $Theme-borderRadius--small: 2px; 37 | $Theme-borderRadius--medium: 4px; 38 | 39 | // Margin/padding sizes 40 | 41 | $Theme-spacing--small: 5px; 42 | $Theme-spacing--medium: 10px; 43 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - v*.*.* 9 | pull_request: 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: actions/setup-node@v3 17 | with: 18 | node-version: 16 19 | - run: yarn install 20 | 21 | - run: yarn test 22 | - run: yarn build 23 | 24 | - uses: softprops/action-gh-release@v1 25 | if: startsWith(github.ref, 'refs/tags/') 26 | with: 27 | files: dist/media-db.zip 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | - run: node deploy.js 31 | if: startsWith(github.ref, 'refs/tags/') 32 | env: 33 | CHROME_CLIENT_ID: ${{ secrets.CHROME_CLIENT_ID }} 34 | CHROME_CLIENT_SECRET: ${{ secrets.CHROME_CLIENT_SECRET }} 35 | CHROME_REFRESH_TOKEN: ${{ secrets.CHROME_REFRESH_TOKEN }} 36 | -------------------------------------------------------------------------------- /src/components/SidebarItem.js: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import { useState } from 'react'; 3 | import SelectBar from './SelectBar'; 4 | import SidebarHistory from './SidebarHistory'; 5 | import SidebarInfo from './SidebarInfo'; 6 | 7 | export default function SidebarItem({ itemId: id, onClickItemHistory }) { 8 | const [showInfo, setShowInfo] = useState(true); 9 | 10 | return ( 11 |
12 | 13 | 24 | 25 | {showInfo ? 26 | : 27 | 28 | } 29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/components/Provider.js: -------------------------------------------------------------------------------- 1 | import { useMutationRemoveProvider, useMutationUpdateProvider } from '../data/queries'; 2 | import AutosaveInput from './AutosaveInput'; 3 | import LinkButton from './LinkButton'; 4 | 5 | export default function Provider({ provider }) { 6 | const updateMutation = useMutationUpdateProvider(provider.id); 7 | const removeMutation = useMutationRemoveProvider(provider.id); 8 | 9 | const handleSave = value => { 10 | updateMutation.mutate({ infoCallback: value }); 11 | }; 12 | const handleRemove = () => { 13 | removeMutation.mutate(); 14 | }; 15 | 16 | return ( 17 |
18 | 22 | {'Remove'} 23 | 24 | 31 |
32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /src/styles/containers/_History.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:math'; 2 | @import '../breakpoints'; 3 | @import '../theme'; 4 | 5 | .History { 6 | display: flex; 7 | flex-wrap: wrap; 8 | width: 100%; 9 | 10 | &-list { 11 | padding: $Theme-spacing--medium; 12 | 13 | @include breakpoint(xs) { 14 | width: percentage(math.div(6, 12)); 15 | } 16 | @include breakpoint(sm) { 17 | width: percentage(math.div(6, 12)); 18 | } 19 | @include breakpoint(md) { 20 | width: percentage(math.div(6, 12)); 21 | } 22 | @include breakpoint(lg) { 23 | width: percentage(math.div(6, 12)); 24 | } 25 | } 26 | 27 | &-raw { 28 | padding: $Theme-spacing--medium; 29 | 30 | @include breakpoint(xs) { 31 | width: percentage(math.div(6, 12)); 32 | } 33 | @include breakpoint(sm) { 34 | width: percentage(math.div(6, 12)); 35 | } 36 | @include breakpoint(md) { 37 | width: percentage(math.div(6, 12)); 38 | } 39 | @include breakpoint(lg) { 40 | width: percentage(math.div(6, 12)); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/styles/containers/_Search.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:math'; 2 | @import '../breakpoints'; 3 | @import '../theme'; 4 | 5 | .Search { 6 | display: flex; 7 | flex-wrap: wrap; 8 | width: 100%; 9 | 10 | &-list { 11 | padding: $Theme-spacing--medium; 12 | 13 | @include breakpoint(xs) { 14 | width: percentage(math.div(6, 12)); 15 | } 16 | @include breakpoint(sm) { 17 | width: percentage(math.div(7, 12)); 18 | } 19 | @include breakpoint(md) { 20 | width: percentage(math.div(8, 12)); 21 | } 22 | @include breakpoint(lg) { 23 | width: percentage(math.div(9, 12)); 24 | } 25 | } 26 | 27 | &-sidebar { 28 | padding: $Theme-spacing--medium; 29 | 30 | @include breakpoint(xs) { 31 | width: percentage(math.div(6, 12)); 32 | } 33 | @include breakpoint(sm) { 34 | width: percentage(math.div(5, 12)); 35 | } 36 | @include breakpoint(md) { 37 | width: percentage(math.div(4, 12)); 38 | } 39 | @include breakpoint(lg) { 40 | width: percentage(math.div(3, 12)); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/styles/containers/_Providers.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:math'; 2 | @import '../breakpoints'; 3 | @import '../theme'; 4 | 5 | .Providers { 6 | display: flex; 7 | flex-wrap: wrap; 8 | width: 100%; 9 | 10 | &-list { 11 | padding: $Theme-spacing--medium; 12 | 13 | @include breakpoint(xs) { 14 | width: percentage(math.div(6, 12)); 15 | } 16 | @include breakpoint(sm) { 17 | width: percentage(math.div(6, 12)); 18 | } 19 | @include breakpoint(md) { 20 | width: percentage(math.div(6, 12)); 21 | } 22 | @include breakpoint(lg) { 23 | width: percentage(math.div(5, 12)); 24 | margin-left: percentage(math.div(1, 12)); 25 | } 26 | } 27 | 28 | &-test { 29 | padding: $Theme-spacing--medium; 30 | 31 | @include breakpoint(xs) { 32 | width: percentage(math.div(6, 12)); 33 | } 34 | @include breakpoint(sm) { 35 | width: percentage(math.div(6, 12)); 36 | } 37 | @include breakpoint(md) { 38 | width: percentage(math.div(6, 12)); 39 | } 40 | @include breakpoint(lg) { 41 | width: percentage(math.div(5, 12)); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/components/ItemStatus.js: -------------------------------------------------------------------------------- 1 | import * as statusTypes from '../constants/statusTypes'; 2 | import { useMutationUpdateItem } from '../data/queries'; 3 | import SelectBar from './SelectBar'; 4 | 5 | export default function ItemStatus({ item }) { 6 | const mutation = useMutationUpdateItem(item.id); 7 | 8 | const handleSave = value => { 9 | mutation.mutate({ status: value }); 10 | }; 11 | 12 | return ( 13 |
14 |

15 | {'Status'} 16 |

17 |
18 | 38 |
39 |
40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/styles/containers/_Items.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:math'; 2 | @import '../breakpoints'; 3 | @import '../theme'; 4 | 5 | .Items { 6 | display: flex; 7 | flex-wrap: wrap; 8 | width: 100%; 9 | 10 | &-list { 11 | padding: $Theme-spacing--medium; 12 | 13 | @include breakpoint(xs) { 14 | width: percentage(math.div(12, 12)); 15 | } 16 | @include breakpoint(sm) { 17 | width: percentage(math.div(12, 12)); 18 | } 19 | @include breakpoint(md) { 20 | width: percentage(math.div(8, 12)); 21 | } 22 | @include breakpoint(lg) { 23 | width: percentage(math.div(9, 12)); 24 | } 25 | } 26 | 27 | &-sidebar { 28 | padding: $Theme-spacing--medium; 29 | 30 | @include breakpoint(xs) { 31 | width: percentage(math.div(12, 12)); 32 | } 33 | @include breakpoint(sm) { 34 | width: percentage(math.div(8, 12)); 35 | margin-left: percentage(math.div(2, 12)); 36 | } 37 | @include breakpoint(md) { 38 | width: percentage(math.div(4, 12)); 39 | margin-left: percentage(math.div(0, 12)); 40 | } 41 | @include breakpoint(lg) { 42 | width: percentage(math.div(3, 12)); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/components/ItemHistory.js: -------------------------------------------------------------------------------- 1 | import { Fragment } from 'react'; 2 | import { diffsFromItemHistory } from '../data/history'; 3 | import FullDate from './FullDate'; 4 | 5 | export default function ItemHistory({ history, onClickItemHistory }) { 6 | const diffs = diffsFromItemHistory(history); 7 | 8 | return ( 9 | 10 | 11 | {diffs.map(({ id, date, changes }) => ( 12 | 13 | {changes.map(({ path, desc }) => ( 14 | onClickItemHistory(date)}> 15 | 20 | 25 | 26 | ))} 27 | 28 | ))} 29 | 30 |
16 |
17 |

{desc}

18 |
19 |
21 |
22 |

23 |
24 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/components/Item.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ItemListCharacters from './ItemListCharacters'; 3 | import ItemListCreator from './ItemListCreator'; 4 | import ItemListGenres from './ItemListGenres'; 5 | import ItemListLength from './ItemListLength'; 6 | import ItemListNotes from './ItemListNotes'; 7 | import ItemListProductionStatus from './ItemListProductionStatus'; 8 | import ItemListStatusDate from './ItemListStatusDate'; 9 | import ItemListThumbnail from './ItemListThumbnail'; 10 | import ItemListTitle from './ItemListTitle'; 11 | 12 | export default React.memo(function Item({ item, onClickItem }) { 13 | const handleClick = () => { 14 | onClickItem(item); 15 | }; 16 | 17 | return ( 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ); 30 | }); 31 | -------------------------------------------------------------------------------- /src/components/ItemProductionStatus.js: -------------------------------------------------------------------------------- 1 | import * as productionStatusTypes from '../constants/productionStatusTypes'; 2 | import { useMutationUpdateItem } from '../data/queries'; 3 | import ItemRefreshButton from './ItemRefreshButton'; 4 | import SelectBar from './SelectBar'; 5 | 6 | export default function ItemProductionStatus({ item }) { 7 | const mutation = useMutationUpdateItem(item.id); 8 | 9 | const handleSave = value => { 10 | mutation.mutate({ productionStatus: value }); 11 | }; 12 | 13 | return ( 14 |
15 |

16 | {'Production Status'} 17 | {' '} 18 | 22 |

23 | 40 |
41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /src/components/ProviderTest.js: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import { useQueryItemFromProvider } from '../data/queries'; 3 | 4 | export default function ProviderTest({ url, onChangeUrl }) { 5 | const { isLoading, isError, error, isPreviousData, isRefetching, data: itemFromProvider } = useQueryItemFromProvider(url, { keepPreviousData: true }); 6 | 7 | const handleChange = e => { 8 | onChangeUrl(e.target.value); 9 | }; 10 | 11 | let output; 12 | if (isLoading) { 13 | output = null; 14 | } else if (isError) { 15 | output = ( 16 | <> 17 |

{'Error'}

18 | {error.message} 19 | 20 | ); 21 | } else { 22 | output = ( 23 | <> 24 |

{'Output'}

25 | {JSON.stringify(itemFromProvider, null, 2)} 26 | 27 | ); 28 | } 29 | 30 | return ( 31 |
32 | 33 | {'Test providers'} 34 | 35 | 42 |
43 | {output} 44 |
45 |
46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /src/components/AutosaveInput.js: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import React, { useState } from 'react'; 3 | 4 | export default React.memo(function AutosaveInput({ type, rows, className, defaultValue, onSave }) { 5 | const [value, setValue] = useState(defaultValue); 6 | const [prevDefaultValue, setPrevDefaultValue] = useState(defaultValue); 7 | // if the default value is changed, `value` should follow `defaultValue` to avoid dirtying the component 8 | if (defaultValue !== prevDefaultValue) { 9 | setValue(defaultValue); 10 | setPrevDefaultValue(defaultValue); 11 | } 12 | 13 | const isDirty = String(value) !== String(defaultValue); 14 | 15 | const handleBlur = () => { 16 | if (isDirty) { 17 | onSave(value); 18 | } 19 | }; 20 | 21 | const handleChange = e => { 22 | setValue(e.target.value); 23 | }; 24 | 25 | const class_ = classNames('AutosaveInput', { 'AutosaveInput--dirty': isDirty }, className); 26 | 27 | return type === 'textarea' ? ( 28 |