Library console in typescript (#90221)

## About The Pull Request
Huge jsx UI converted over to typescript, removed uselocalstate, and
some tiny qol changes to boot. This is not a rewrite, it should function
exactly the same

<details>
<summary>photos</summary>

Inventory screen is split into sections so you shouldn't lose the search
controls while scrolling
![Screenshot 2025-03-24
130056](https://github.com/user-attachments/assets/9a5e188f-1960-490d-9b86-884d2ee8636a)

Native tabs for poster selection, candy striped
![Screenshot 2025-03-24
130104](https://github.com/user-attachments/assets/da1aca51-cded-4988-b9cc-bf87a2465867)

Modal gets a bit more padding
![Screenshot 2025-03-24
130113](https://github.com/user-attachments/assets/37410b1e-9fe3-40ff-a87d-e9ffaa01f15b)

</details>

## Why It's Good For The Game
Big win for typescript conversions. The file is made more digestible as
well
## Changelog
🆑
fix: The library console has been refactored. Report any issues in the
github!
/🆑
This commit is contained in:
Jeremiah
2025-04-29 17:07:30 -06:00
committed by Shadow-Quill
parent a6f13664b9
commit 0d3962cff9
16 changed files with 1147 additions and 936 deletions
@@ -2,7 +2,8 @@ import { Box, NoticeBox, Stack } from 'tgui-core/components';
import { useBackend } from '../../backend';
import { Window } from '../../layouts';
import { PageSelect, SearchAndDisplay } from '../LibraryConsole';
import { PageSelect } from '../LibraryConsole/components/PageSelect';
import { SearchAndDisplay } from '../LibraryConsole/components/Search';
import { LibraryAdminData } from './types';
export function BookListing(props) {
@@ -1,934 +0,0 @@
import { map, sortBy } from 'common/collections';
import { useState } from 'react';
import {
Box,
Button,
Dropdown,
Flex,
Input,
LabeledList,
Modal,
NoticeBox,
NumberInput,
Section,
Stack,
Table,
} from 'tgui-core/components';
import { classes } from 'tgui-core/react';
import { useBackend, useLocalState } from '../backend';
import { Window } from '../layouts';
import { sanitizeText } from '../sanitize';
export const LibraryConsole = (props) => {
const { act, data } = useBackend();
const { display_lore } = data;
return (
<Window
theme={display_lore ? 'spookyconsole' : ''}
title="Library Terminal"
width={880}
height={520}
>
<Window.Content m="0">
<Flex height="100%">
<Flex.Item>
<PopoutMenu />
</Flex.Item>
<Flex.Item grow position="relative" pl={1}>
<PageDisplay />
</Flex.Item>
</Flex>
</Window.Content>
</Window>
);
};
export const PopoutMenu = (props) => {
const { act, data } = useBackend();
const { screen_state, show_dropdown, display_lore } = data;
return (
<Section fill maxWidth={show_dropdown ? '150px' : '36px'}>
<Stack vertical fill>
<Stack.Item>
<Button
fluid
fontSize="13px"
onClick={() => act('toggle_dropdown')}
icon={show_dropdown === 1 ? 'chevron-left' : 'chevron-right'}
tooltip={!show_dropdown && 'Expand'}
content={!!show_dropdown && 'Collapse'}
/>
</Stack.Item>
<PopoutEntry id={1} icon="list" text="Inventory" />
<PopoutEntry id={2} icon="calendar" text="Checkout" />
<PopoutEntry id={3} icon="server" text="Archive" />
<PopoutEntry id={4} icon="upload" text="Upload" />
<PopoutEntry id={5} icon="print" text="Print" />
{!!display_lore && (
<PopoutEntry
id={6}
icon="question"
text={screen_state === 6 ? 'Gur Fbeprere' : 'Forbidden Lore'}
color="black"
font="copperplate"
/>
)}
</Stack>
</Section>
);
};
export const PageDisplay = (props) => {
const { act, data } = useBackend();
const { screen_state } = data;
/* eslint-disable indent */
/* eslint-disable operator-linebreak */
return screen_state === 1 ? (
<Inventory />
) : screen_state === 2 ? (
<Checkout />
) : screen_state === 3 ? (
<Archive />
) : screen_state === 4 ? (
<Upload />
) : screen_state === 5 ? (
<Print />
) : screen_state === 6 ? (
<Forbidden />
) : null;
/* eslint-enable indent */
/* eslint-enable operator-linebreak */
};
export const Inventory = (props) => {
const { act, data } = useBackend();
const { inventory_page_count, inventory_page, has_inventory } = data;
if (!has_inventory) {
return (
<NoticeBox>No Book Records detected. Update your inventory!</NoticeBox>
);
}
return (
<Stack vertical justify="space-between" height="100%">
<Stack.Item grow>
<ScrollableSection
header="Library Inventory"
contents={<InventoryDetails />}
/>
</Stack.Item>
<Stack.Item align="center">
<PageSelect
minimum_page_count={1}
page_count={inventory_page_count}
current_page={inventory_page}
call_on_change={(value) =>
act('switch_inventory_page', {
page: value,
})
}
/>
</Stack.Item>
</Stack>
);
};
export const InventoryDetails = (props) => {
const { act, data } = useBackend();
const inventory = sortBy(
map(data.inventory, (book, i) => ({
...book,
// Generate a unique id
key: i,
})),
(book) => book.key,
);
return (
<Section>
<Table>
<Table.Row header>
<Table.Cell>Remove</Table.Cell>
<Table.Cell>Title</Table.Cell>
<Table.Cell>Author</Table.Cell>
</Table.Row>
{inventory.map((book) => (
<Table.Row key={book.key}>
<Table.Cell>
<Button
color="bad"
onClick={() =>
act('inventory_remove', {
book_id: book.ref,
})
}
icon="times"
>
Clear Record
</Button>
</Table.Cell>
<Table.Cell>{book.title}</Table.Cell>
<Table.Cell>{book.author}</Table.Cell>
</Table.Row>
))}
</Table>
</Section>
);
};
export const Checkout = (props) => {
const { act, data } = useBackend();
const { checkout_page, checkout_page_count } = data;
const [checkoutBook, setCheckoutBook] = useLocalState('CheckoutBook', false);
return (
<Stack vertical height="100%" justify="space-between">
<Stack.Item grow>
<Stack vertical height="100%">
<Stack.Item grow>
<ScrollableSection
header="Checked Out Books"
contents={<CheckoutEntries />}
/>
</Stack.Item>
<Stack.Item align="center">
<PageSelect
minimum_page_count={1}
page_count={checkout_page_count}
current_page={checkout_page}
call_on_change={(value) =>
act('switch_checkout_page', {
page: value,
})
}
/>
</Stack.Item>
</Stack>
</Stack.Item>
<Stack.Item>
<Button
fluid
icon="barcode"
content="Check-Out Book"
fontSize="20px"
onClick={() => setCheckoutBook(true)}
/>
</Stack.Item>
{!!checkoutBook && <CheckoutModal />}
</Stack>
);
};
export const CheckoutEntries = (props) => {
const { act, data } = useBackend();
const { checkouts, has_checkout } = data;
if (!has_checkout) {
return null;
}
return (
<Table>
<Table.Row header>
<Table.Cell>Check-In</Table.Cell>
<Table.Cell>Title</Table.Cell>
<Table.Cell>Author</Table.Cell>
<Table.Cell>Borrower</Table.Cell>
<Table.Cell>Time Left</Table.Cell>
</Table.Row>
{checkouts.map((entry) => (
<Table.Row key={entry.id}>
<Table.Cell>
<Button
onClick={() =>
act('checkin', {
checked_out_id: entry.ref,
})
}
icon="box-open"
/>
</Table.Cell>
<Table.Cell>{entry.title}</Table.Cell>
<Table.Cell>{entry.author}</Table.Cell>
<Table.Cell>{entry.borrower}</Table.Cell>
<Table.Cell backgroundColor={entry.overdue ? 'bad' : 'good'}>
{entry.overdue ? 'Overdue' : entry.due_in_minutes + ' Minutes'}
</Table.Cell>
</Table.Row>
))}
</Table>
);
};
const CheckoutModal = (props) => {
const { act, data } = useBackend();
const inventory = sortBy(
map(data.inventory, (book, i) => ({
...book,
// Generate a unique id
key: i,
})),
(book) => book.key,
);
const [checkoutBook, setCheckoutBook] = useLocalState('CheckoutBook', false);
const [bookName, setBookName] = useState('Insert Book name...');
const [checkoutee, setCheckoutee] = useState('Recipient');
const [checkoutPeriod, setCheckoutPeriod] = useState(5);
return (
<Modal width="500px">
<Box fontSize="20px" pb={1}>
Are you sure you want to loan out this book?
</Box>
<Dropdown
over
mb={1.7}
width="100%"
selected={bookName}
options={inventory.map((book) => book.title)}
value={bookName}
onSelected={(e) => setBookName(e)}
/>
<LabeledList>
<LabeledList.Item label="Loan To">
<Input
width="160px"
value={checkoutee}
onChange={(e, value) => setCheckoutee(value)}
/>
</LabeledList.Item>
<LabeledList.Item label="Loan Period">
<NumberInput
value={checkoutPeriod}
unit=" Minutes"
minValue={1}
step={1}
stepPixelSize={10}
onChange={(e, value) => setCheckoutPeriod(value)}
/>
</LabeledList.Item>
</LabeledList>
<Stack justify="center" align="center" pt={1}>
<Stack.Item>
<Button
icon="upload"
content="Loan Out"
fontSize="16px"
color="good"
onClick={() => {
setCheckoutBook(false);
act('checkout', {
book_name: bookName,
loaned_to: checkoutee,
checkout_time: checkoutPeriod,
});
}}
lineHeight={2}
/>
</Stack.Item>
<Stack.Item>
<Button
icon="times"
content="Return"
fontSize="16px"
color="bad"
onClick={() => setCheckoutBook(false)}
lineHeight={2}
/>
</Stack.Item>
</Stack>
</Modal>
);
};
export const Archive = (props) => {
const { act, data } = useBackend();
const { can_connect, can_db_request, page_count, our_page } = data;
if (!can_connect) {
return (
<NoticeBox>
Unable to retrieve book listings. Please contact your system
administrator for assistance.
</NoticeBox>
);
}
return (
<Stack vertical justify="space-between" height="100%">
<Stack.Item grow>
<ScrollableSection
header="Remote Archive"
contents={<SearchAndDisplay />}
/>
</Stack.Item>
<Stack.Item align="center">
<PageSelect
minimum_page_count={1}
page_count={page_count}
current_page={our_page}
disabled={!can_db_request}
call_on_change={(value) =>
act('switch_page', {
page: value,
})
}
/>
</Stack.Item>
</Stack>
);
};
export const SearchAndDisplay = (props) => {
const { act, data } = useBackend();
const {
search_categories = [],
book_id,
title,
category,
author,
params_changed,
can_db_request,
} = data;
const records = sortBy(
map(data.pages, (record, i) => ({
...record,
// Generate a unique id
key: i,
})),
(record) => record.key,
);
return (
<Box>
<Stack justify="space-between">
<Stack.Item pb={0.6}>
<Stack>
<Stack.Item>
<Input
value={book_id}
placeholder={book_id === null ? 'ID' : book_id}
mt={0.5}
width="70px"
onChange={(e, value) =>
act('set_search_id', {
id: value,
})
}
/>
</Stack.Item>
<Stack.Item>
<Dropdown
width="120px"
options={search_categories}
selected={category}
onSelected={(value) =>
act('set_search_category', {
category: value,
})
}
/>
</Stack.Item>
<Stack.Item>
<Input
value={title}
placeholder={title || 'Title'}
mt={0.5}
onChange={(e, value) =>
act('set_search_title', {
title: value,
})
}
/>
</Stack.Item>
<Stack.Item>
<Input
value={author}
placeholder={author || 'Author'}
mt={0.5}
onChange={(e, value) =>
act('set_search_author', {
author: value,
})
}
/>
</Stack.Item>
</Stack>
</Stack.Item>
<Stack.Item>
<Button
disabled={!can_db_request}
textAlign="right"
onClick={() => act('search')}
color={params_changed ? 'good' : ''}
icon="book"
>
Search
</Button>
<Button
disabled={!can_db_request}
textAlign="right"
onClick={() => act('clear_data')}
color="bad"
icon="fire"
>
Reset Search
</Button>
</Stack.Item>
</Stack>
<Table>
<Table.Row>
<Table.Cell fontSize={1.5}>#</Table.Cell>
<Table.Cell fontSize={1.5}>Category</Table.Cell>
<Table.Cell fontSize={1.5}>Title</Table.Cell>
<Table.Cell fontSize={1.5}>Author</Table.Cell>
</Table.Row>
{records.map((record) => (
<Table.Row key={record.key}>
<Table.Cell>
<Button
onClick={() =>
act('print_book', {
book_id: record.id,
})
}
icon="print"
>
{record.id}
</Button>
</Table.Cell>
<Table.Cell>{record.category}</Table.Cell>
<Table.Cell>{record.title}</Table.Cell>
<Table.Cell>{record.author}</Table.Cell>
</Table.Row>
))}
</Table>
</Box>
);
};
export const Upload = (props) => {
const { act, data } = useBackend();
const {
active_newscaster_cooldown,
cache_author,
cache_content,
cache_title,
can_db_request,
has_cache,
has_scanner,
cooldown_string,
} = data;
const [uploadToDB, setUploadToDB] = useLocalState('UploadDB', false);
if (!has_scanner) {
return (
<NoticeBox>
No nearby scanner detected, construct one to continue.
</NoticeBox>
);
}
if (!has_cache) {
return <NoticeBox>Scan in a book to upload.</NoticeBox>;
}
const contentHtml = {
__html: sanitizeText(cache_content),
};
return (
<>
<Stack vertical height="100%">
<Stack.Item>
<Box fontSize="20px" textAlign="center" pt="6px">
Current Scan Cache
</Box>
</Stack.Item>
<Stack.Item grow>
<Stack vertical height="100%">
<Stack.Item>
<Stack justify="center">
<Stack.Item>
<Box pt={1} fontSize={'20px'}>
Title:
</Box>
</Stack.Item>
<Stack.Item>
<Input
fontSize="20px"
value={cache_title}
placeholder={cache_title || 'Title'}
mt={0.5}
width={22}
onChange={(e, value) =>
act('set_cache_title', {
title: value,
})
}
/>
</Stack.Item>
<Stack.Item>
<Box pt={1} fontSize="20px">
Author:
</Box>
</Stack.Item>
<Stack.Item>
<Input
fontSize="20px"
value={cache_author}
placeholder={cache_author || 'Author'}
mt={0.5}
onChange={(e, value) =>
act('set_cache_author', {
author: value,
})
}
/>
</Stack.Item>
</Stack>
</Stack.Item>
<Stack.Item grow>
<Section
fill
scrollable
preserveWhitespace
fontSize="15px"
title="Content:"
>
<Box dangerouslySetInnerHTML={contentHtml} />
</Section>
</Stack.Item>
</Stack>
</Stack.Item>
<Stack.Item>
<Stack>
<Stack.Item grow>
<Button
disabled={!active_newscaster_cooldown}
fluid
tooltip={
active_newscaster_cooldown
? "Send your book to the station's newscaster's channel."
: 'Please wait ' +
cooldown_string +
' before sending your book to the newscaster!'
}
tooltipPosition="top"
icon="newspaper"
content="Newscaster"
fontSize="30px"
lineHeight={2}
textAlign="center"
onClick={() => act('news_post')}
/>
</Stack.Item>
<Stack.Item grow>
<Button
disabled={!can_db_request}
fluid
icon="server"
content="Archive"
fontSize="30px"
lineHeight={2}
textAlign="center"
onClick={() => setUploadToDB(true)}
/>
</Stack.Item>
</Stack>
</Stack.Item>
</Stack>
{!!uploadToDB && <UploadModal />}
</>
);
};
const UploadModal = (props) => {
const { act, data } = useBackend();
const { upload_categories, default_category, can_db_request } = data;
const [uploadToDB, setUploadToDB] = useLocalState('UploadDB', false);
const [uploadCategory, setUploadCategory] = useState('');
const display_category = uploadCategory || default_category;
return (
<Modal width="650px">
<Box fontSize="20px" pb={2}>
Are you sure you want to upload this book to the database?
</Box>
<LabeledList>
<LabeledList.Item label="Category">
<Dropdown
options={upload_categories}
selected={display_category}
onSelected={(value) => setUploadCategory(value)}
/>
</LabeledList.Item>
</LabeledList>
<Stack justify="center" align="center" pt={2}>
<Stack.Item>
<Button
disabled={!can_db_request}
icon="upload"
content="Upload To DB"
fontSize="18px"
color="good"
onClick={() => {
setUploadToDB(false);
act('upload', {
category: display_category,
});
}}
lineHeight={2}
/>
</Stack.Item>
<Stack.Item>
<Button
icon="times"
content="Return"
fontSize="18px"
color="bad"
onClick={() => setUploadToDB(false)}
lineHeight={2}
/>
</Stack.Item>
</Stack>
</Modal>
);
};
export const Print = (props) => {
const { act, data } = useBackend();
const { deity, religion, bible_name, bible_sprite, posters } = data;
const [selectedPoster, setSelectedPoster] = useState(posters[0]);
return (
<Stack vertical fill>
<Stack.Item grow>
<Stack fill>
<Stack.Item width="50%">
<Section fill scrollable>
{posters.map((poster) => (
<div
key={poster}
title={poster}
className={classes([
'Button',
'Button--fluid',
'Button--color--transparent',
'Button--ellipsis',
selectedPoster &&
poster === selectedPoster &&
'Button--selected',
])}
onClick={() => setSelectedPoster(poster)}
>
{poster}
</div>
))}
</Section>
</Stack.Item>
<Stack.Item>
<Stack vertical height="100%">
<Stack.Item
textAlign="center"
fontSize="25px"
italic
bold
textColor="#0b94c4"
>
{bible_name}
</Stack.Item>
<Stack.Item textAlign="center" fontSize="22px" textColor="purple">
In the Name of {deity}
</Stack.Item>
<Stack.Item textAlign="center" fontSize="22px" textColor="purple">
For the Sake of {religion}
</Stack.Item>
<Stack.Item align="center">
<Box className={classes(['bibles224x224', bible_sprite])} />
</Stack.Item>
</Stack>
</Stack.Item>
</Stack>
</Stack.Item>
<Stack.Item>
<Stack justify="space-between">
<Stack.Item grow>
<Button
fluid
icon="scroll"
content="Poster"
fontSize="30px"
lineHeight={2}
textAlign="center"
onClick={() =>
act('print_poster', {
poster_name: selectedPoster,
})
}
/>
</Stack.Item>
<Stack.Item grow>
<Button
fluid
icon="cross"
content="Bible"
fontSize="30px"
lineHeight={2}
textAlign="center"
onClick={() => act('print_bible')}
/>
</Stack.Item>
</Stack>
</Stack.Item>
</Stack>
);
};
const ForbiddenModal = (props) => {
const { act, data } = useBackend();
return (
<Modal>
<Box className="LibraryComputer__CultText" fontSize="28px">
Accessing Forbidden Lore Vault v 1.3:
</Box>
<Box className="LibraryComputer__CultText" pt={0.4}>
Are you absolutely sure you want to proceed?
</Box>
<Box className="LibraryComputer__CultText" pt={0.2} bold>
EldritchRelics Inc. will take no responsibility for this choice
</Box>
<Stack justify="center" align="center">
<Stack.Item>
<Button
className="LibraryComputer__CultText"
fluid
icon="check"
content="Assent"
color="good"
fontSize="20px"
onClick={() => act('lore_spawn')}
lineHeight={2}
/>
</Stack.Item>
<Stack.Item>
<Button
className="LibraryComputer__CultText"
fluid
icon="times"
content="Decline"
color="bad"
fontSize="20px"
onClick={() => act('lore_deny')}
lineHeight={2}
/>
</Stack.Item>
</Stack>
</Modal>
);
};
export const Forbidden = (props) => {
const description =
'Abf vqrnz cebprffhf pbzchgngvbanyvf fghqrer vapvcvrzhf\nCebprffhf pbzchgngvbanyrf fhag erf nofgenpgnr dhnr pbzchgngberf vapbyhag\nHg ribyihag, cebprffhf nyvn nofgenpgn dhnr qngn znavchyner qvphaghe\nRibyhgvbavf cebprffhf qvevtvghe cre rkrzcyhz erthynr cebtenzzngvf ibpngv\nUbzvarf cebtenzzngn nq cebprffhf erpgbf rssvpvhag\nEriren fcvevghf pbzchgngbevv phz vapnagnzragvf pbavhatvzhf\nCebprffhf pbzchgngvbanyvf rfg zhyghz fvzvyvf vqrnr irarsvpnr fcvevghf\nivqrev nhg gnatv aba cbgrfg\nAba rfg rk zngrevn pbzcbfvgn\nFrq vq cynpreng vcfhz\nAba cbgrfg bcrenev bchf vagryyrpghnyr\nErfcbaqrev cbgrfg\nZhaqhz nssvprer cbgrfg rebtnaqb crphavnz nq evcnz iry cre oenppuvhz \nebobgv snoevpnaqb zbqrenaqb\nPbafvyvvf hgvzhe cebprffvohf nhthenaqv fhag fvphg vapnagnzragn irarsvpvv';
return (
<Box className="LibraryComputer__CultNonsense" preserveWhitespace>
{description}
<ForbiddenModal />
</Box>
);
};
export const ScrollableSection = (props) => {
const { header, contents } = props;
return (
<Section fill scrollable>
<Box fontSize="20px" textAlign="center">
{header}
</Box>
<Box position="relative" top="10px">
{contents}
</Box>
</Section>
);
};
export const PopoutEntry = (props) => {
const { act, data } = useBackend();
const { id, text, icon, color, font } = props;
const { show_dropdown, screen_state } = data;
const selected_color = color || 'good';
const deselected_color = color || '';
return (
<Stack.Item>
<Button
fluid
fontSize="13px"
onClick={() =>
act('set_screen', {
screen_index: id,
})
}
color={id === screen_state ? selected_color : deselected_color}
fontFamily={font}
icon={icon}
tooltip={!show_dropdown && text}
content={!!show_dropdown && text}
/>
</Stack.Item>
);
};
export const PageSelect = (props) => {
const {
minimum_page_count,
page_count,
current_page,
call_on_change,
disabled,
} = props;
if (page_count === 1) {
return null;
}
return (
<Stack>
<Stack.Item>
<Button
disabled={current_page === minimum_page_count || disabled}
icon="angle-double-left"
onClick={() => call_on_change(minimum_page_count)}
/>
</Stack.Item>
<Stack.Item>
<Button
disabled={current_page === minimum_page_count || disabled}
icon="chevron-left"
onClick={() => call_on_change(current_page - 1)}
/>
</Stack.Item>
<Stack.Item>
<Input
placeholder={current_page + '/' + page_count}
onChange={(e, value) => {
// I am so sorry
if (value !== '') {
call_on_change(value);
e.target.value = null;
}
}}
/>
</Stack.Item>
<Stack.Item>
<Button
disabled={current_page === page_count || disabled}
icon="chevron-right"
onClick={() => call_on_change(current_page + 1)}
/>
</Stack.Item>
<Stack.Item>
<Button
disabled={current_page === page_count || disabled}
icon="angle-double-right"
onClick={() => call_on_change(page_count)}
/>
</Stack.Item>
</Stack>
);
};
@@ -0,0 +1,58 @@
import { Button, Input, Stack } from 'tgui-core/components';
export function PageSelect(props) {
const {
call_on_change,
current_page,
disabled,
minimum_page_count,
page_count,
} = props;
if (page_count === 1) return;
return (
<Stack>
<Stack.Item>
<Button
disabled={current_page === minimum_page_count || disabled}
icon="angle-double-left"
onClick={() => call_on_change(minimum_page_count)}
/>
</Stack.Item>
<Stack.Item>
<Button
disabled={current_page === minimum_page_count || disabled}
icon="chevron-left"
onClick={() => call_on_change(current_page - 1)}
/>
</Stack.Item>
<Stack.Item>
<Input
placeholder={current_page + '/' + page_count}
onChange={(e, value) => {
// I am so sorry
if (value !== '') {
call_on_change(value);
e.currentTarget.value = '';
}
}}
/>
</Stack.Item>
<Stack.Item>
<Button
disabled={current_page === page_count || disabled}
icon="chevron-right"
onClick={() => call_on_change(current_page + 1)}
/>
</Stack.Item>
<Stack.Item>
<Button
disabled={current_page === page_count || disabled}
icon="angle-double-right"
onClick={() => call_on_change(page_count)}
/>
</Stack.Item>
</Stack>
);
}
@@ -0,0 +1,71 @@
import { useBackend } from 'tgui/backend';
import { Button, Section, Stack } from 'tgui-core/components';
import { LibraryConsoleData } from '../types';
export function PopoutMenu(props) {
const { act, data } = useBackend<LibraryConsoleData>();
const { display_lore, screen_state, show_dropdown } = data;
return (
<Section fill maxWidth={show_dropdown ? '150px' : '36px'}>
<Stack vertical fill>
<Stack.Item>
<Button
fluid
fontSize="13px"
onClick={() => act('toggle_dropdown')}
icon={show_dropdown === 1 ? 'chevron-left' : 'chevron-right'}
tooltip={!show_dropdown && 'Expand'}
>
{!!show_dropdown && 'Collapse'}
</Button>
</Stack.Item>
<PopoutEntry id={1} icon="list" text="Inventory" />
<PopoutEntry id={2} icon="calendar" text="Checkout" />
<PopoutEntry id={3} icon="server" text="Archive" />
<PopoutEntry id={4} icon="upload" text="Upload" />
<PopoutEntry id={5} icon="print" text="Print" />
{!!display_lore && (
<PopoutEntry
id={6}
icon="question"
text={screen_state === 6 ? 'Gur Fbeprere' : 'Forbidden Lore'}
color="black"
font="copperplate"
/>
)}
</Stack>
</Section>
);
}
export function PopoutEntry(props) {
const { act, data } = useBackend<LibraryConsoleData>();
const { id, text, icon, color, font } = props;
const { screen_state, show_dropdown } = data;
const selected_color = color || 'good';
const deselected_color = color || '';
return (
<Stack.Item>
<Button
fluid
fontSize="13px"
onClick={() =>
act('set_screen', {
screen_index: id,
})
}
color={id === screen_state ? selected_color : deselected_color}
fontFamily={font}
icon={icon}
tooltip={!show_dropdown && text}
>
{!!show_dropdown && text}
</Button>
</Stack.Item>
);
}
@@ -0,0 +1,26 @@
import { ReactNode } from 'react';
import { Section, Stack } from 'tgui-core/components';
type Props = {
contents: ReactNode;
header: ReactNode;
};
export function ScrollableSection(props: Props) {
const { contents, header } = props;
return (
<Stack fill vertical>
<Stack.Item>
<Section fontSize="20px" textAlign="center" color="label">
{header}
</Section>
</Stack.Item>
<Stack.Item grow>
<Section fill scrollable>
{contents}
</Section>
</Stack.Item>
</Stack>
);
}
@@ -0,0 +1,162 @@
import { useBackend } from 'tgui/backend';
import {
Button,
Dropdown,
Input,
Section,
Stack,
Table,
} from 'tgui-core/components';
import { LibraryConsoleData } from '../types';
export function SearchAndDisplay(props) {
return (
<Stack fill vertical>
<Stack.Item>
<SearchTabs />
</Stack.Item>
<Stack.Item grow>
<SearchResults />
</Stack.Item>
</Stack>
);
}
function SearchTabs(props) {
const { act, data } = useBackend<LibraryConsoleData>();
const {
author,
book_id,
can_db_request,
category,
params_changed,
search_categories = [],
title,
} = data;
return (
<Section fill>
<Stack justify="space-between">
<Stack.Item pb={0.6}>
<Stack>
<Stack.Item>
<Input
value={book_id}
placeholder={book_id === null ? 'ID' : book_id}
mt={0.5}
width="70px"
onChange={(e, value) =>
act('set_search_id', {
id: value,
})
}
/>
</Stack.Item>
<Stack.Item>
<Dropdown
width="120px"
options={search_categories}
selected={category}
onSelected={(value) =>
act('set_search_category', {
category: value,
})
}
/>
</Stack.Item>
<Stack.Item>
<Input
value={title}
placeholder={title || 'Title'}
mt={0.5}
onChange={(e, value) =>
act('set_search_title', {
title: value,
})
}
/>
</Stack.Item>
<Stack.Item>
<Input
value={author}
placeholder={author || 'Author'}
mt={0.5}
onChange={(e, value) =>
act('set_search_author', {
author: value,
})
}
/>
</Stack.Item>
</Stack>
</Stack.Item>
<Stack.Item>
<Button
disabled={!can_db_request}
textAlign="right"
onClick={() => act('search')}
color={params_changed ? 'good' : ''}
icon="book"
>
Search
</Button>
<Button
disabled={!can_db_request}
textAlign="right"
onClick={() => act('clear_data')}
color="bad"
icon="fire"
>
Reset Search
</Button>
</Stack.Item>
</Stack>
</Section>
);
}
function SearchResults(props) {
const { act, data } = useBackend<LibraryConsoleData>();
const { pages } = data;
const sorted = pages
.map((record, i) => ({
...record,
// Generate a unique id
key: i,
}))
.sort((a, b) => a.key - b.key);
return (
<Section fill scrollable>
<Table>
<Table.Row className="candystripe">
<Table.Cell fontSize={1.5}>#</Table.Cell>
<Table.Cell fontSize={1.5}>Category</Table.Cell>
<Table.Cell fontSize={1.5}>Title</Table.Cell>
<Table.Cell fontSize={1.5}>Author</Table.Cell>
</Table.Row>
{sorted.map((record) => (
<Table.Row key={record.key} className="candystripe">
<Table.Cell>
<Button
onClick={() =>
act('print_book', {
book_id: record.id,
})
}
icon="print"
>
{record.id}
</Button>
</Table.Cell>
<Table.Cell>{record.category}</Table.Cell>
<Table.Cell>{record.title}</Table.Cell>
<Table.Cell>{record.author}</Table.Cell>
</Table.Row>
))}
</Table>
</Section>
);
}
@@ -0,0 +1,49 @@
import { useState } from 'react';
import { Stack } from 'tgui-core/components';
import { useBackend } from '../../backend';
import { Window } from '../../layouts';
import { PopoutMenu } from './components/PopoutMenu';
import { Archive } from './screens/Archive';
import { Checkout } from './screens/Checkout';
import { Forbidden } from './screens/Forbidden';
import { Inventory } from './screens/Inventory';
import { Print } from './screens/Print';
import { Upload } from './screens/Upload';
import { LibraryConsoleData } from './types';
import { LibraryContext } from './useLibraryContext';
export function LibraryConsole(props) {
const { data } = useBackend<LibraryConsoleData>();
const { display_lore, screen_state } = data;
const checkoutBookState = useState(false);
const uploadToDBState = useState(false);
return (
<LibraryContext.Provider value={{ checkoutBookState, uploadToDBState }}>
<Window
theme={display_lore ? 'spookyconsole' : ''}
title="Library Terminal"
width={880}
height={520}
>
<Window.Content>
<Stack fill>
<Stack.Item>
<PopoutMenu />
</Stack.Item>
<Stack.Item grow>
{screen_state === 1 && <Inventory />}
{screen_state === 2 && <Checkout />}
{screen_state === 3 && <Archive />}
{screen_state === 4 && <Upload />}
{screen_state === 5 && <Print />}
{screen_state === 6 && <Forbidden />}
</Stack.Item>
</Stack>
</Window.Content>
</Window>
</LibraryContext.Provider>
);
}
@@ -0,0 +1,41 @@
import { useBackend } from 'tgui/backend';
import { NoticeBox, Stack } from 'tgui-core/components';
import { PageSelect } from '../components/PageSelect';
import { SearchAndDisplay } from '../components/Search';
import { LibraryConsoleData } from '../types';
export function Archive(props) {
const { act, data } = useBackend<LibraryConsoleData>();
const { can_connect, can_db_request, page_count, our_page } = data;
if (!can_connect) {
return (
<NoticeBox>
Unable to retrieve book listings. Please contact your system
administrator for assistance.
</NoticeBox>
);
}
return (
<Stack vertical justify="space-between" fill>
<Stack.Item grow>
<SearchAndDisplay />
</Stack.Item>
<Stack.Item align="center">
<PageSelect
minimum_page_count={1}
page_count={page_count}
current_page={our_page}
disabled={!can_db_request}
call_on_change={(value) =>
act('switch_page', {
page: value,
})
}
/>
</Stack.Item>
</Stack>
);
}
@@ -0,0 +1,203 @@
import { useState } from 'react';
import { useBackend } from 'tgui/backend';
import {
Button,
Dropdown,
Input,
LabeledList,
Modal,
NoticeBox,
NumberInput,
Stack,
Table,
} from 'tgui-core/components';
import { PageSelect } from '../components/PageSelect';
import { ScrollableSection } from '../components/ScrollableSection';
import { LibraryConsoleData } from '../types';
import { useLibraryContext } from '../useLibraryContext';
export function Checkout(props) {
const { act, data } = useBackend<LibraryConsoleData>();
const { checkout_page, checkout_page_count } = data;
const { checkoutBookState } = useLibraryContext();
const [checkoutBook, setCheckoutBook] = checkoutBookState;
return (
<Stack vertical height="100%" justify="space-between">
<Stack.Item grow>
<Stack vertical height="100%">
<Stack.Item grow>
<ScrollableSection
header="Checked Out Books"
contents={<CheckoutEntries />}
/>
</Stack.Item>
<Stack.Item align="center">
<PageSelect
minimum_page_count={1}
page_count={checkout_page_count}
current_page={checkout_page}
call_on_change={(value) =>
act('switch_checkout_page', {
page: value,
})
}
/>
</Stack.Item>
</Stack>
</Stack.Item>
<Stack.Item>
<Button
fluid
icon="barcode"
fontSize="20px"
onClick={() => setCheckoutBook(true)}
>
Check-Out Book
</Button>
</Stack.Item>
{!!checkoutBook && <CheckoutModal />}
</Stack>
);
}
function CheckoutModal(props) {
const { act, data } = useBackend<LibraryConsoleData>();
const inventory = data.inventory
.map((book, i) => ({
...book,
// Generate a unique id
key: i,
}))
.sort((a, b) => a.key - b.key);
const { checkoutBookState } = useLibraryContext();
const [checkoutBook, setCheckoutBook] = checkoutBookState;
const [bookName, setBookName] = useState('Insert Book name...');
const [checkoutee, setCheckoutee] = useState('Recipient');
const [checkoutPeriod, setCheckoutPeriod] = useState(5);
return (
<Modal width="500px" py={4}>
<Stack fill vertical>
<Stack.Item fontSize="20px">
Are you sure you want to loan out this book?
</Stack.Item>
<Stack.Item>
<Dropdown
over
width="100%"
selected={bookName}
options={inventory.map((book) => book.title)}
onSelected={(e) => setBookName(e)}
/>
</Stack.Item>
<Stack.Item>
<LabeledList>
<LabeledList.Item label="Loan To">
<Input
width="160px"
value={checkoutee}
onChange={(e, value) => setCheckoutee(value)}
/>
</LabeledList.Item>
<LabeledList.Item label="Loan Period">
<NumberInput
value={checkoutPeriod}
unit=" Minutes"
minValue={1}
maxValue={1440}
step={1}
stepPixelSize={10}
onChange={(value) => setCheckoutPeriod(value)}
/>
</LabeledList.Item>
</LabeledList>
</Stack.Item>
<Stack.Item>
<Stack justify="center" align="center" pt={1}>
<Stack.Item>
<Button
icon="upload"
fontSize="16px"
color="good"
onClick={() => {
setCheckoutBook(false);
act('checkout', {
book_name: bookName,
loaned_to: checkoutee,
checkout_time: checkoutPeriod,
});
}}
lineHeight={2}
>
Loan Out
</Button>
</Stack.Item>
<Stack.Item>
<Button
icon="times"
fontSize="16px"
color="bad"
onClick={() => setCheckoutBook(false)}
lineHeight={2}
>
Return
</Button>
</Stack.Item>
</Stack>
</Stack.Item>
</Stack>
</Modal>
);
}
export function CheckoutEntries(props) {
const { act, data } = useBackend<LibraryConsoleData>();
const { checkouts = [] } = data;
return (
<Table>
<Table.Row header className="candystripe">
<Table.Cell>Title</Table.Cell>
<Table.Cell>Author</Table.Cell>
<Table.Cell>Borrower</Table.Cell>
<Table.Cell>Time Left</Table.Cell>
<Table.Cell>Check-In</Table.Cell>
</Table.Row>
{checkouts.length === 0 ? (
<Table.Row>
<Table.Cell textAlign="center" colSpan={5}>
<NoticeBox>No books checked out.</NoticeBox>
</Table.Cell>
</Table.Row>
) : (
checkouts.map((entry) => (
<Table.Row key={entry.id} className="candystripe">
<Table.Cell>{entry.title}</Table.Cell>
<Table.Cell>{entry.author}</Table.Cell>
<Table.Cell>{entry.borrower}</Table.Cell>
<Table.Cell backgroundColor={entry.overdue ? 'bad' : 'good'}>
{entry.overdue ? 'Overdue' : entry.due_in_minutes + ' Minutes'}
</Table.Cell>
<Table.Cell width="70px" textAlign="center">
<Button
mb={1}
onClick={() =>
act('checkin', {
checked_out_id: entry.ref,
})
}
icon="box-open"
/>
</Table.Cell>
</Table.Row>
))
)}
</Table>
);
}
@@ -0,0 +1,60 @@
import { useBackend } from 'tgui/backend';
import { Box, Button, Modal, Stack } from 'tgui-core/components';
const description =
'Abf vqrnz cebprffhf pbzchgngvbanyvf fghqrer vapvcvrzhf\nCebprffhf pbzchgngvbanyrf fhag erf nofgenpgnr dhnr pbzchgngberf vapbyhag\nHg ribyihag, cebprffhf nyvn nofgenpgn dhnr qngn znavchyner qvphaghe\nRibyhgvbavf cebprffhf qvevtvghe cre rkrzcyhz erthynr cebtenzzngvf ibpngv\nUbzvarf cebtenzzngn nq cebprffhf erpgbf rssvpvhag\nEriren fcvevghf pbzchgngbevv phz vapnagnzragvf pbavhatvzhf\nCebprffhf pbzchgngvbanyvf rfg zhyghz fvzvyvf vqrnr irarsvpnr fcvevghf\nivqrev nhg gnatv aba cbgrfg\nAba rfg rk zngrevn pbzcbfvgn\nFrq vq cynpreng vcfhz\nAba cbgrfg bcrenev bchf vagryyrpghnyr\nErfcbaqrev cbgrfg\nZhaqhz nssvprer cbgrfg rebtnaqb crphavnz nq evcnz iry cre oenppuvhz \nebobgv snoevpnaqb zbqrenaqb\nPbafvyvvf hgvzhe cebprffvohf nhthenaqv fhag fvphg vapnagnzragn irarsvpvv';
export function Forbidden(props) {
return (
<Box className="LibraryComputer__CultNonsense" preserveWhitespace>
{description}
<ForbiddenModal />
</Box>
);
}
function ForbiddenModal(props) {
const { act } = useBackend();
return (
<Modal>
<Box className="LibraryComputer__CultText" fontSize="28px">
Accessing Forbidden Lore Vault v 1.3:
</Box>
<Box className="LibraryComputer__CultText" pt={0.4}>
Are you absolutely sure you want to proceed?
</Box>
<Box className="LibraryComputer__CultText" pt={0.2} bold>
EldritchRelics Inc. will take no responsibility for this choice
</Box>
<Stack justify="center" align="center">
<Stack.Item>
<Button
className="LibraryComputer__CultText"
fluid
icon="check"
color="good"
fontSize="20px"
onClick={() => act('lore_spawn')}
lineHeight={2}
>
Assent
</Button>
</Stack.Item>
<Stack.Item>
<Button
className="LibraryComputer__CultText"
fluid
icon="times"
color="bad"
fontSize="20px"
onClick={() => act('lore_deny')}
lineHeight={2}
>
Decline
</Button>
</Stack.Item>
</Stack>
</Modal>
);
}
@@ -0,0 +1,83 @@
import { useBackend } from 'tgui/backend';
import { Button, NoticeBox, Stack, Table } from 'tgui-core/components';
import { PageSelect } from '../components/PageSelect';
import { ScrollableSection } from '../components/ScrollableSection';
import { LibraryConsoleData } from '../types';
export function Inventory(props) {
const { act, data } = useBackend<LibraryConsoleData>();
const { inventory_page_count, inventory_page, has_inventory } = data;
if (!has_inventory) {
return (
<NoticeBox>No Book Records detected. Update your inventory!</NoticeBox>
);
}
return (
<Stack vertical justify="space-between" height="100%">
<Stack.Item grow>
<ScrollableSection
header="Library Inventory"
contents={<InventoryDetails />}
/>
</Stack.Item>
<Stack.Item align="center">
<PageSelect
minimum_page_count={1}
page_count={inventory_page_count}
current_page={inventory_page}
call_on_change={(value) =>
act('switch_inventory_page', {
page: value,
})
}
/>
</Stack.Item>
</Stack>
);
}
function InventoryDetails(props) {
const { act, data } = useBackend<LibraryConsoleData>();
const { inventory = [] } = data;
const sorted = inventory
.map((book, i) => ({
...book,
// Generate a unique id
key: i,
}))
.sort((a, b) => a.key - b.key);
return (
<Table>
<Table.Row header className="candystripe">
<Table.Cell>Title</Table.Cell>
<Table.Cell>Author</Table.Cell>
<Table.Cell textAlign="center">Remove</Table.Cell>
</Table.Row>
{sorted.map((book) => (
<Table.Row key={book.key} className="candystripe">
<Table.Cell>{book.title}</Table.Cell>
<Table.Cell>{book.author}</Table.Cell>
<Table.Cell collapsing>
<Button
mb={1}
color="bad"
onClick={() =>
act('inventory_remove', {
book_id: book.ref,
})
}
icon="times"
>
Clear Record
</Button>
</Table.Cell>
</Table.Row>
))}
</Table>
);
}
@@ -0,0 +1,97 @@
import { useState } from 'react';
import { useBackend } from 'tgui/backend';
import { Box, Button, Section, Stack, Tabs } from 'tgui-core/components';
import { classes } from 'tgui-core/react';
import { LibraryConsoleData } from '../types';
export function Print(props) {
const { act, data } = useBackend<LibraryConsoleData>();
const { bible_name, bible_sprite, deity, posters, religion } = data;
const [selectedPoster, setSelectedPoster] = useState(posters[0]);
return (
<Stack vertical fill>
<Stack.Item grow>
<Stack fill>
<Stack.Item width="50%">
<Section fill scrollable>
<Tabs vertical>
{posters.map((poster) => {
const selected = selectedPoster === poster;
return (
<Tabs.Tab
className="candystripe"
selected={selected}
color={selected && 'good'}
key={poster}
onClick={() => setSelectedPoster(poster)}
>
{poster}
</Tabs.Tab>
);
})}
</Tabs>
</Section>
</Stack.Item>
<Stack.Item>
<Stack vertical height="100%">
<Stack.Item
textAlign="center"
fontSize="25px"
italic
bold
textColor="#0b94c4"
>
{bible_name}
</Stack.Item>
<Stack.Item textAlign="center" fontSize="22px" textColor="purple">
In the Name of {deity}
</Stack.Item>
<Stack.Item textAlign="center" fontSize="22px" textColor="purple">
For the Sake of {religion}
</Stack.Item>
<Stack.Item align="center">
<Box className={classes(['bibles224x224', bible_sprite])} />
</Stack.Item>
</Stack>
</Stack.Item>
</Stack>
</Stack.Item>
<Stack.Item>
<Stack justify="space-between">
<Stack.Item grow>
<Button
fluid
icon="scroll"
fontSize="30px"
lineHeight={2}
textAlign="center"
onClick={() =>
act('print_poster', {
poster_name: selectedPoster,
})
}
>
Poster
</Button>
</Stack.Item>
<Stack.Item grow>
<Button
fluid
icon="cross"
fontSize="30px"
lineHeight={2}
textAlign="center"
onClick={() => act('print_bible')}
>
Bible
</Button>
</Stack.Item>
</Stack>
</Stack.Item>
</Stack>
);
}
@@ -0,0 +1,216 @@
import { useState } from 'react';
import { useBackend } from 'tgui/backend';
import { sanitizeText } from 'tgui/sanitize';
import {
Box,
Button,
Dropdown,
Input,
LabeledList,
Modal,
NoticeBox,
Section,
Stack,
} from 'tgui-core/components';
import { LibraryConsoleData } from '../types';
import { useLibraryContext } from '../useLibraryContext';
export function Upload(props) {
const { act, data } = useBackend<LibraryConsoleData>();
const {
active_newscaster_cooldown,
cache_author,
cache_content,
cache_title,
can_db_request,
cooldown_string,
has_cache,
has_scanner,
} = data;
const { uploadToDBState } = useLibraryContext();
const [uploadToDB, setUploadToDB] = uploadToDBState;
if (!has_scanner) {
return (
<NoticeBox>
No nearby scanner detected, construct one to continue.
</NoticeBox>
);
}
if (!has_cache) {
return <NoticeBox>Scan in a book to upload.</NoticeBox>;
}
const contentHtml = {
__html: sanitizeText(cache_content),
};
return (
<>
<Stack vertical height="100%">
<Stack.Item>
<Box fontSize="20px" textAlign="center" pt="6px">
Current Scan Cache
</Box>
</Stack.Item>
<Stack.Item grow>
<Stack vertical height="100%">
<Stack.Item>
<Stack justify="center">
<Stack.Item>
<Box pt={1} fontSize={'20px'}>
Title:
</Box>
</Stack.Item>
<Stack.Item>
<Input
fontSize="20px"
value={cache_title}
placeholder={cache_title || 'Title'}
mt={0.5}
width={22}
onChange={(e, value) =>
act('set_cache_title', {
title: value,
})
}
/>
</Stack.Item>
<Stack.Item>
<Box pt={1} fontSize="20px">
Author:
</Box>
</Stack.Item>
<Stack.Item>
<Input
fontSize="20px"
value={cache_author}
placeholder={cache_author || 'Author'}
mt={0.5}
onChange={(e, value) =>
act('set_cache_author', {
author: value,
})
}
/>
</Stack.Item>
</Stack>
</Stack.Item>
<Stack.Item grow>
<Section
fill
scrollable
preserveWhitespace
fontSize="15px"
title="Content:"
>
<Box dangerouslySetInnerHTML={contentHtml} />
</Section>
</Stack.Item>
</Stack>
</Stack.Item>
<Stack.Item>
<Stack>
<Stack.Item grow>
<Button
disabled={!active_newscaster_cooldown}
fluid
tooltip={
active_newscaster_cooldown
? "Send your book to the station's newscaster's channel."
: 'Please wait ' +
cooldown_string +
' before sending your book to the newscaster!'
}
tooltipPosition="top"
icon="newspaper"
fontSize="30px"
lineHeight={2}
textAlign="center"
onClick={() => act('news_post')}
>
Newscaster
</Button>
</Stack.Item>
<Stack.Item grow>
<Button
disabled={!can_db_request}
fluid
icon="server"
fontSize="30px"
lineHeight={2}
textAlign="center"
onClick={() => setUploadToDB(true)}
>
Archive
</Button>
</Stack.Item>
</Stack>
</Stack.Item>
</Stack>
{!!uploadToDB && <UploadModal />}
</>
);
}
function UploadModal(props) {
const { act, data } = useBackend<LibraryConsoleData>();
const { upload_categories, default_category, can_db_request } = data;
const { uploadToDBState } = useLibraryContext();
const [uploadToDB, setUploadToDB] = uploadToDBState;
const [uploadCategory, setUploadCategory] = useState('');
const display_category = uploadCategory || default_category;
return (
<Modal width="650px">
<Box fontSize="20px" pb={2}>
Are you sure you want to upload this book to the database?
</Box>
<LabeledList>
<LabeledList.Item label="Category">
<Dropdown
options={upload_categories}
selected={display_category}
onSelected={(value) => setUploadCategory(value)}
/>
</LabeledList.Item>
</LabeledList>
<Stack justify="center" align="center" pt={2}>
<Stack.Item>
<Button
disabled={!can_db_request}
icon="upload"
fontSize="18px"
color="good"
onClick={() => {
setUploadToDB(false);
act('upload', {
category: display_category,
});
}}
lineHeight={2}
>
Upload To DB
</Button>
</Stack.Item>
<Stack.Item>
<Button
icon="times"
fontSize="18px"
color="bad"
onClick={() => setUploadToDB(false)}
lineHeight={2}
>
Return
</Button>
</Stack.Item>
</Stack>
</Modal>
);
}
@@ -0,0 +1,63 @@
import { BooleanLike } from 'tgui-core/react';
type CheckoutEntry = {
author: string;
borrower: string;
due_in_minutes: number;
id: string;
overdue: BooleanLike;
ref: string;
title: string;
};
type InventoryEntry = {
author: string;
ref: string;
title: string;
};
type Page = {
author: string;
category: string;
id: string;
title: string;
};
export type LibraryConsoleData = {
active_newscaster_cooldown: BooleanLike;
author: string;
bible_name: string;
bible_sprite: string;
book_id: string;
cache_author: string;
cache_content: string;
cache_title: string;
can_connect: BooleanLike;
can_db_request: BooleanLike;
category: string;
checkout_page_count: number;
checkout_page: number;
checkouts: CheckoutEntry[];
cooldown_string: string;
default_category: string;
deity: string;
display_lore: BooleanLike;
has_cache: BooleanLike;
has_checkout: BooleanLike;
has_inventory: BooleanLike;
has_scanner: BooleanLike;
inventory_page_count: number;
inventory_page: number;
inventory: InventoryEntry[];
our_page: number;
page_count: number;
pages: Page[];
params_changed: BooleanLike;
posters: string[];
religion: string;
screen_state: number;
search_categories: string[];
show_dropdown: BooleanLike;
title: string;
upload_categories: string[];
};
@@ -0,0 +1,15 @@
import { createContext, Dispatch, SetStateAction, useContext } from 'react';
type LibraryContextType = {
checkoutBookState: [boolean, Dispatch<SetStateAction<boolean>>];
uploadToDBState: [boolean, Dispatch<SetStateAction<boolean>>];
};
export const LibraryContext = createContext<LibraryContextType>({
checkoutBookState: [false, () => {}],
uploadToDBState: [false, () => {}],
});
export function useLibraryContext() {
return useContext(LibraryContext);
}
@@ -12,7 +12,7 @@ import {
import { useBackend } from '../backend';
import { Window } from '../layouts';
import { PageSelect } from './LibraryConsole';
import { PageSelect } from './LibraryConsole/components/PageSelect';
export const LibraryVisitor = (props) => {
return (