mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-11-11 21:38:44 +00:00
Merge branch 'master' of https://github.com/naturalcrit/homebrewery into refactor-navigation
This commit is contained in:
@@ -7,6 +7,7 @@ storage
|
||||
build/*
|
||||
config/local.*
|
||||
config/docker.*
|
||||
config/dev.*
|
||||
|
||||
todo.md
|
||||
startDB.bat
|
||||
|
||||
@@ -335,7 +335,7 @@
|
||||
}
|
||||
|
||||
//these media queries are just for the list layout and could be moved inside it, not sure it is a good idea
|
||||
@media screen and (max-width : 1400px) {
|
||||
@container (max-width : 1400px) {
|
||||
.brewCollection .brewGroup.list {
|
||||
.brewItem:not(.detailsOpen) {
|
||||
.info {
|
||||
@@ -361,7 +361,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
@media screen and (max-width : 1200px) {
|
||||
@container (max-width : 1200px) {
|
||||
.brewCollection .brewGroup.list {
|
||||
.brewItem:not(.detailsOpen) {
|
||||
grid-template-columns : 35% 3fr 150px;
|
||||
@@ -388,7 +388,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
@media screen and (max-width : 800px) {
|
||||
@container (max-width : 800px) {
|
||||
.brewCollection .brewGroup.list {
|
||||
.brewItem:not(.detailsOpen) {
|
||||
.info {
|
||||
@@ -443,7 +443,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
@media screen and (max-width : 650px) {
|
||||
@container (max-width : 650px) {
|
||||
.brewCollection .brewGroup.list {
|
||||
.brewItem:not(.detailsOpen) {
|
||||
grid-template-columns : 3fr 2fr;
|
||||
|
||||
@@ -21,15 +21,16 @@
|
||||
|
||||
.sitePage.listPage {
|
||||
--bg-clr : white;
|
||||
container-type: inline-size;
|
||||
.content {
|
||||
padding : clamp(20px, 5vh, 200px) clamp(20px, 20vw, 200px);
|
||||
padding : clamp(20px, 5vh, 200px) clamp(20px, 20cqi, 200px);
|
||||
overflow-y : scroll;
|
||||
scrollbar-gutter : stable;
|
||||
@media screen and (max-width : 1200px) {
|
||||
padding : clamp(20px, 5vh, 200px) clamp(20px, 10vw, 200px);
|
||||
@container (max-width : 1200px) {
|
||||
padding : clamp(20px, 5vh, 200px) clamp(20px, 10cqi, 200px);
|
||||
}
|
||||
@media screen and (max-width : 800px) {
|
||||
padding : clamp(20px, 5vh, 200px) clamp(20px, 5vw, 200px);
|
||||
@container (max-width : 800px) {
|
||||
padding : clamp(20px, 5vh, 200px) clamp(20px, 5cqi, 200px);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
@@ -53,6 +54,7 @@
|
||||
color : white;
|
||||
}
|
||||
.brewCollection {
|
||||
|
||||
h1 {
|
||||
width : fit-content;
|
||||
margin-bottom : 20px;
|
||||
@@ -97,9 +99,9 @@
|
||||
gap : 20px;
|
||||
justify-items : center;
|
||||
|
||||
@media screen and (max-width : 1700px) {--columns : 3;}
|
||||
@media screen and (max-width : 1400px) {--columns : 2;}
|
||||
@media screen and (max-width : 900px) {--columns : 1;}
|
||||
@container (max-width : 1700px) {--columns : 3;}
|
||||
@container (max-width : 1400px) {--columns : 2;}
|
||||
@container (max-width : 900px) {--columns : 1;}
|
||||
h1 { position : absolute; }
|
||||
|
||||
&:not(:has(.brewItem:last-child:nth-child(3n))) {
|
||||
|
||||
@@ -46,21 +46,25 @@ const SharePage = (props)=>{
|
||||
document.addEventListener('keydown', handleControlKeys);
|
||||
fetchThemeBundle(undefined, setThemeBundle, currentBrew.renderer, currentBrew.theme);
|
||||
|
||||
// listen for changes in the brew version
|
||||
const eventSource = new EventSource('/stream');
|
||||
eventSource.addEventListener('message', (evt)=>{
|
||||
const messageData = JSON.parse(evt.data);
|
||||
let eventSource;
|
||||
// Start a stream if the brew has been updated in the last month
|
||||
if(Date.parse(props.brew?.updatedAt) > (new Date).setDate((new Date).getDate() - 30)){
|
||||
// listen for changes in the brew version
|
||||
eventSource = new EventSource(`/api/stream/${props.brew.shareId}`);
|
||||
|
||||
if(messageData.eventType == 'brewUpdated'){
|
||||
if(messageData.shareId == currentBrew.shareId && messageData.version != currentBrew.version) {
|
||||
eventSource.addEventListener('brewUpdated', (evt)=>{
|
||||
const messageData = JSON.parse(evt.data);
|
||||
|
||||
if(messageData.version != currentBrew.version) {
|
||||
console.log('should fetch brew');
|
||||
fetchUpdatedBrew();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return ()=>{
|
||||
document.removeEventListener('keydown', handleControlKeys);
|
||||
eventSource?.close();
|
||||
};
|
||||
}, []);
|
||||
console.log('brew: ', currentBrew);
|
||||
|
||||
@@ -10,6 +10,13 @@ import ErrorIndex from '../errorPage/errors/errorIndex.js';
|
||||
import request from '../../utils/request-middleware.js';
|
||||
import Navbar from '../../navbar/navbar.jsx';
|
||||
|
||||
const DEFAULT_SORT_TYPE = 'alpha';
|
||||
const DEFAULT_SORT_DIR = 'asc';
|
||||
const DEFAULT_LAYOUT_MODE = 'grid';
|
||||
const DEFAULT_THEME_MODE = 'light';
|
||||
const USERPAGE_LAYOUT_MODE = 'HB_listPage_layout_mode';
|
||||
const USERPAGE_THEME_MODE = 'HB_listPage_theme_mode';
|
||||
|
||||
const VaultPage = (props)=>{
|
||||
const [pageState, setPageState] = useState(parseInt(props.query.page) || 1);
|
||||
|
||||
@@ -21,6 +28,8 @@ const VaultPage = (props)=>{
|
||||
const [totalBrews, setTotalBrews] = useState(null);
|
||||
const [searching, setSearching] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [layoutMode, setLayoutMode] = useState(DEFAULT_LAYOUT_MODE);
|
||||
const [themeMode, setThemeMode] = useState(DEFAULT_THEME_MODE);
|
||||
|
||||
const titleRef = useRef(null);
|
||||
const authorRef = useRef(null);
|
||||
@@ -31,6 +40,13 @@ const VaultPage = (props)=>{
|
||||
|
||||
useEffect(()=>{
|
||||
disableSubmitIfFormInvalid();
|
||||
|
||||
const newLayoutMode = localStorage.getItem(USERPAGE_LAYOUT_MODE) || DEFAULT_LAYOUT_MODE;
|
||||
const newThemeMode = localStorage.getItem(USERPAGE_THEME_MODE) || DEFAULT_THEME_MODE;
|
||||
|
||||
setLayoutMode(newLayoutMode);
|
||||
setThemeMode(newThemeMode);
|
||||
|
||||
loadPage(pageState, true, props.query.sort, props.query.dir);
|
||||
}, []);
|
||||
|
||||
@@ -68,7 +84,7 @@ const VaultPage = (props)=>{
|
||||
updateStateWithBrews([], 1);
|
||||
});
|
||||
|
||||
if(response.ok)
|
||||
if(response?.ok)
|
||||
updateStateWithBrews(response.body.brews, page);
|
||||
};
|
||||
|
||||
@@ -77,7 +93,8 @@ const VaultPage = (props)=>{
|
||||
|
||||
const response = await request.get(`/api/vault/total?title=${title}&author=${author}&v3=${v3}&legacy=${legacy}`)
|
||||
.catch((error)=>{
|
||||
console.log('error at loadTotal: ', error);
|
||||
console.log('error at loadTotal: ');
|
||||
console.error(error);
|
||||
setError(error);
|
||||
updateStateWithBrews([], 1);
|
||||
});
|
||||
@@ -239,7 +256,7 @@ const VaultPage = (props)=>{
|
||||
const oppositeDir = dirState === 'asc' ? 'desc' : 'asc';
|
||||
|
||||
return (
|
||||
<div className={`sort-option ${sortState === optionValue ? `active` : ''}`}>
|
||||
<div className={`option ${sortState === optionValue ? `active` : ''}`}>
|
||||
<button onClick={()=>loadPage(1, false, optionValue, oppositeDir)}>
|
||||
{optionTitle}
|
||||
</button>
|
||||
@@ -250,10 +267,10 @@ const VaultPage = (props)=>{
|
||||
);
|
||||
};
|
||||
|
||||
const renderSortBar = ()=>{
|
||||
const renderSortOptions = ()=>{
|
||||
|
||||
return (
|
||||
<div className='sort-container'>
|
||||
<div className='radio-toggle sort-container'>
|
||||
{renderSortOption('Title', 'title', props.query.dir)}
|
||||
{renderSortOption('Created Date', 'createdAt', props.query.dir)}
|
||||
{renderSortOption('Updated Date', 'updatedAt', props.query.dir)}
|
||||
@@ -262,6 +279,64 @@ const VaultPage = (props)=>{
|
||||
);
|
||||
};
|
||||
|
||||
const handleLayoutChange = (e, mode)=>{
|
||||
setLayoutMode(e.target.checked ? mode : DEFAULT_LAYOUT_MODE);
|
||||
localStorage.setItem(USERPAGE_LAYOUT_MODE, e.target.checked ? mode : DEFAULT_LAYOUT_MODE);
|
||||
return;
|
||||
};
|
||||
const renderLayoutModeOptions = ()=>{
|
||||
return (
|
||||
<div className='radio-toggle layout-container'>
|
||||
<h6>View:</h6>
|
||||
<label className='option' title='grid'>
|
||||
<input name='layout-mode' type='radio' onChange={(e)=>handleLayoutChange(e, 'grid')} checked={layoutMode === 'grid'} />
|
||||
<i className='fac compressed-grid'></i>
|
||||
</label>
|
||||
<label className='option' title='list'>
|
||||
<input name='layout-mode' type='radio' onChange={(e)=>handleLayoutChange(e, 'list')} checked={layoutMode === 'list'} />
|
||||
<i className='fas fa-bars'></i>
|
||||
</label>
|
||||
<label className='option' title='card'>
|
||||
<input name='layout-mode' type='radio' onChange={(e)=>handleLayoutChange(e, 'card')} checked={layoutMode === 'card'} />
|
||||
<i className='fac format-card'></i>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const handleThemeChange = (e, mode)=>{
|
||||
setThemeMode(e.target.checked ? mode : DEFAULT_THEME_MODE);
|
||||
localStorage.setItem(USERPAGE_THEME_MODE, e.target.checked ? mode : DEFAULT_THEME_MODE);
|
||||
return;
|
||||
};
|
||||
|
||||
const renderThemePicker = ()=>{
|
||||
return (
|
||||
<div className="radio-toggle theme-container">
|
||||
<h6>Theme:</h6>
|
||||
<label className="option">
|
||||
<input type="radio" name="theme" onChange={(e)=>handleThemeChange(e, 'light')} checked={themeMode === 'light'}/>
|
||||
<i className="fas fa-sun"/>
|
||||
</label>
|
||||
<label className="option">
|
||||
<input type="radio" name="theme" onChange={(e)=>handleThemeChange(e, 'dark')} checked={themeMode === 'dark'}/>
|
||||
<i className="fas fa-moon"/>
|
||||
</label>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
const renderNav = () =>{
|
||||
return (
|
||||
<div className="listPageNav">
|
||||
{renderLayoutModeOptions()}
|
||||
{renderThemePicker()}
|
||||
{renderSortOptions()}
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
const renderPaginationControls = ()=>{
|
||||
if(!totalBrews || totalBrews < 10) return null;
|
||||
|
||||
@@ -342,13 +417,23 @@ const VaultPage = (props)=>{
|
||||
}
|
||||
|
||||
if(error) {
|
||||
const errorText = ErrorIndex()[error.HBErrorCode.toString()] || '';
|
||||
if(error.HBErrorCode === undefined) {
|
||||
return (
|
||||
<div className='foundBrews noBrews'>
|
||||
<h3>Error: Unknown error</h3>
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
|
||||
return (
|
||||
<div className='foundBrews noBrews'>
|
||||
const errorText = ErrorIndex()[error.HBErrorCode.toString()] || '';
|
||||
|
||||
return (
|
||||
<div className='foundBrews noBrews'>
|
||||
<h3>Error: {errorText}</h3>
|
||||
</div>
|
||||
);
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if(!brewCollection) {
|
||||
@@ -368,22 +453,26 @@ const VaultPage = (props)=>{
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='foundBrews'>
|
||||
<div className={`foundBrews ${themeMode + 'Mode'}`}>
|
||||
<span className='totalBrews'>
|
||||
{`Brews found: `}
|
||||
<span>{totalBrews}</span>
|
||||
</span>
|
||||
{brewCollection.length > 10 && renderPaginationControls()}
|
||||
{brewCollection.map((brew, index)=>{
|
||||
return (
|
||||
<BrewItem
|
||||
brew={{ ...brew }}
|
||||
key={index}
|
||||
reportError={props.reportError}
|
||||
renderStorage={false}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<div className='brewCollection'>
|
||||
<div className={`brewGroup ${layoutMode}`}>
|
||||
{brewCollection.map((brew, index)=>{
|
||||
return (
|
||||
<BrewItem
|
||||
brew={{ ...brew }}
|
||||
key={index}
|
||||
reportError={props.reportError}
|
||||
renderStorage={false}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
{renderPaginationControls()}
|
||||
</div>
|
||||
);
|
||||
@@ -398,7 +487,7 @@ const VaultPage = (props)=>{
|
||||
<SplitPane showDividerButtons={false}>
|
||||
<div className='form dataGroup'>{renderForm()}</div>
|
||||
<div className='resultsContainer dataGroup'>
|
||||
{renderSortBar()}
|
||||
{renderNav()}
|
||||
{renderFoundBrews()}
|
||||
</div>
|
||||
</SplitPane>
|
||||
|
||||
@@ -117,69 +117,40 @@
|
||||
height : 100%;
|
||||
overflow-y : auto;
|
||||
font-size : 0.34cm;
|
||||
|
||||
h3 {
|
||||
font-family : 'Open Sans';
|
||||
font-weight : 900;
|
||||
color : white;
|
||||
}
|
||||
|
||||
.sort-container {
|
||||
display : flex;
|
||||
flex-wrap : wrap;
|
||||
column-gap : 15px;
|
||||
justify-content : center;
|
||||
height : 30px;
|
||||
color : white;
|
||||
background-color : #555555;
|
||||
border-top : 1px solid #666666;
|
||||
border-bottom : 1px solid #666666;
|
||||
|
||||
.sort-option {
|
||||
display : flex;
|
||||
align-items : center;
|
||||
padding : 0 8px;
|
||||
|
||||
&:hover { background-color : #444444; }
|
||||
|
||||
&.active {
|
||||
background-color : #333333;
|
||||
|
||||
button {
|
||||
font-weight : 800;
|
||||
color : white;
|
||||
|
||||
& + .sortDir { padding-left : 5px; }
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
padding : 0;
|
||||
font-size : 11px;
|
||||
font-weight : normal;
|
||||
color : #CCCCCC;
|
||||
text-transform : uppercase;
|
||||
background-color : transparent;
|
||||
|
||||
&:hover { background : none; }
|
||||
}
|
||||
}
|
||||
}
|
||||
container-type : inline-size;
|
||||
|
||||
.foundBrews {
|
||||
position : relative;
|
||||
width : 100%;
|
||||
height : 100%;
|
||||
max-height : 100%;
|
||||
padding : 70px 50px;
|
||||
container-type : inline-size;
|
||||
display:flex;
|
||||
flex-direction: column;
|
||||
gap:20px;
|
||||
padding : clamp(20px, 5vh, 200px) clamp(20px, 20vw, 200px);
|
||||
overflow-y : scroll;
|
||||
scrollbar-gutter : stable;
|
||||
@container (max-width : 1200px) {
|
||||
padding : clamp(20px, 5vh, 200px) clamp(20px, 10vw, 200px);
|
||||
}
|
||||
@container (max-width : 800px) {
|
||||
padding : clamp(20px, 5vh, 200px) clamp(20px, 5vw, 200px);
|
||||
}
|
||||
|
||||
h3 { font-size : 25px; }
|
||||
&::-webkit-scrollbar {
|
||||
width : 15px;
|
||||
&:horizontal {
|
||||
width : auto;
|
||||
height : 20px;
|
||||
}
|
||||
&-thumb {
|
||||
background : linear-gradient(90deg, var(--bg-clr) 12px, #00000000 12px);
|
||||
&:horizontal { background : linear-gradient(0deg, var(--bg-clr) 15px, #00000000 15px); }
|
||||
}
|
||||
&-corner { visibility : hidden; }
|
||||
}
|
||||
|
||||
&.noBrews {
|
||||
display : grid;
|
||||
place-items : center;
|
||||
margin : 10px 0px;
|
||||
font-size : 25px;
|
||||
font-style : italic;
|
||||
font-weight : 900;
|
||||
color : white;
|
||||
}
|
||||
|
||||
@@ -202,6 +173,64 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.brewCollection {
|
||||
h1 {
|
||||
width : fit-content;
|
||||
margin-bottom : 20px;
|
||||
font-size : 25px;
|
||||
color : #DDDDDD;
|
||||
|
||||
&.active::before,
|
||||
&.inactive::before {
|
||||
padding-right : 0.5em;
|
||||
font-family : 'Font Awesome 6 Free';
|
||||
font-size : 0.6cm;
|
||||
font-weight : 900;
|
||||
}
|
||||
&.active {
|
||||
color : white;
|
||||
&::before { content : '\f107'; }
|
||||
}
|
||||
&.inactive::before { content : '\f105'; }
|
||||
}
|
||||
h1:hover { cursor : pointer; }
|
||||
|
||||
.brewGroup {
|
||||
margin-bottom : 20px;
|
||||
&.grid {
|
||||
display : flex;
|
||||
flex-wrap : wrap;
|
||||
gap : 20px;
|
||||
justify-content : center;
|
||||
width : fit-content;
|
||||
min-width : 200px;
|
||||
}
|
||||
|
||||
&.list {
|
||||
display : flex;
|
||||
flex-direction : column;
|
||||
gap : 2px;
|
||||
}
|
||||
&.card {
|
||||
--columns : 4;
|
||||
display : grid;
|
||||
grid-template-columns : repeat(var(--columns), 1fr);
|
||||
gap : 20px;
|
||||
justify-items : center;
|
||||
|
||||
@container (max-width : 1700px) {--columns : 3;}
|
||||
@container (max-width : 1400px) {--columns : 2;}
|
||||
@container (max-width : 900px) {--columns : 1;}
|
||||
h1 { position : absolute; }
|
||||
|
||||
&:not(:has(.brewItem:last-child:nth-child(3n))) {
|
||||
.brewItem:last-child { margin-right : auto; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.totalBrews {
|
||||
position : fixed;
|
||||
right : 0;
|
||||
@@ -233,66 +262,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
.brewItem {
|
||||
width : 47%;
|
||||
margin-right : 40px;
|
||||
color : black;
|
||||
isolation : isolate;
|
||||
transition : width 0.5s;
|
||||
|
||||
&::after {
|
||||
position : absolute;
|
||||
inset : 0;
|
||||
z-index : -2;
|
||||
display : block;
|
||||
content : '';
|
||||
background-image : url('/assets/parchmentBackground.jpg');
|
||||
}
|
||||
|
||||
&:nth-child(even of .brewItem) { margin-right : 0; }
|
||||
|
||||
h2 {
|
||||
font-family : 'MrEavesRemake';
|
||||
font-size : 0.75cm;
|
||||
font-weight : 800;
|
||||
line-height : 0.988em;
|
||||
color : var(--HB_Color_HeaderText);
|
||||
}
|
||||
.info {
|
||||
position : relative;
|
||||
z-index : 2;
|
||||
font-family : 'ScalySansRemake';
|
||||
font-size : 1.2em;
|
||||
|
||||
>span {
|
||||
margin-right : 12px;
|
||||
line-height : 1.5em;
|
||||
}
|
||||
}
|
||||
.links { z-index : 2; }
|
||||
|
||||
hr {
|
||||
visibility : hidden;
|
||||
margin : 0px;
|
||||
}
|
||||
|
||||
.thumbnail { z-index : -1; }
|
||||
}
|
||||
|
||||
.paginationControls {
|
||||
position : absolute;
|
||||
top : 35px;
|
||||
left : 50%;
|
||||
display : grid;
|
||||
grid-template-areas : 'previousPage currentPage nextPage';
|
||||
grid-template-columns : 50px 1fr 50px;
|
||||
gap : 20px;
|
||||
place-items : center;
|
||||
width : auto;
|
||||
width : fit-content;
|
||||
margin-inline:auto;
|
||||
font-size : 15px;
|
||||
translate : -50%;
|
||||
|
||||
&:last-child { top : unset; }
|
||||
|
||||
.pages {
|
||||
display : flex;
|
||||
@@ -314,8 +292,11 @@
|
||||
text-underline-position : under;
|
||||
cursor : pointer;
|
||||
|
||||
&.currentPage {
|
||||
&:hover {
|
||||
color : gold;
|
||||
}
|
||||
&.currentPage {
|
||||
color:@yellow;
|
||||
text-decoration : underline;
|
||||
pointer-events : none;
|
||||
}
|
||||
@@ -339,6 +320,77 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.listPageNav {
|
||||
position : relative;
|
||||
display : flex;
|
||||
flex-wrap : wrap;
|
||||
row-gap : 10px;
|
||||
column-gap : 20px;
|
||||
align-items : center;
|
||||
justify-content : center;
|
||||
width : 100%;
|
||||
height : fit-content;
|
||||
font-family : 'Open Sans', sans-serif;
|
||||
color : white;
|
||||
background : #555555;
|
||||
border-top : 1px solid #666666;
|
||||
border-bottom : 1px solid #666666;
|
||||
row-rule : 1px solid #666666;
|
||||
|
||||
> div {
|
||||
width : fit-content;
|
||||
height : 30px;
|
||||
padding-inline : 10px;
|
||||
}
|
||||
|
||||
.radio-toggle {
|
||||
z-index : 1;
|
||||
display : flex;
|
||||
flex-wrap : wrap;
|
||||
row-gap : 5px;
|
||||
align-items : center;
|
||||
justify-content : center;
|
||||
text-align : center;
|
||||
h6 {
|
||||
margin-right : 5px;
|
||||
font-size : 14px;
|
||||
}
|
||||
|
||||
.option {
|
||||
display : flex;
|
||||
align-items : center;
|
||||
height : 100%;
|
||||
padding : 0 8px;
|
||||
color : #CCCCCC;
|
||||
|
||||
input[type='radio'] { display : none; }
|
||||
|
||||
button {
|
||||
padding : 0;
|
||||
font-size : 11px;
|
||||
font-weight : normal;
|
||||
text-transform : uppercase;
|
||||
background-color : transparent;
|
||||
}
|
||||
&:hover { background-color : #444444; }
|
||||
|
||||
&.active, &:has(input:checked) {
|
||||
font-weight : bold;
|
||||
color : white;
|
||||
background-color : #333333;
|
||||
|
||||
button {
|
||||
height : 100%;
|
||||
font-weight : 800;
|
||||
& + .sortDir { padding-left : 5px; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes trailingDots {
|
||||
@@ -351,16 +403,4 @@
|
||||
|
||||
66%,
|
||||
100% { content : ' ...'; }
|
||||
}
|
||||
|
||||
@container (width < 670px) {
|
||||
.vaultPage {
|
||||
|
||||
.dataGroup.form .brewLookup { padding : 1px 20px 20px 10px; }
|
||||
|
||||
.dataGroup.resultsContainer .foundBrews .brewItem {
|
||||
width : 100%;
|
||||
margin-inline : auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-21
@@ -20,6 +20,7 @@ import api from './homebrew.api.js';
|
||||
const { homebrewApi, getBrew, getCSS } = api;
|
||||
import adminApi from './admin.api.js';
|
||||
import vaultApi from './vault.api.js';
|
||||
import streamApi from './stream.api.js';
|
||||
import pageRoutes from './page-routes.js';
|
||||
|
||||
import serveCompressedStaticAssets from './static-assets.mv.js';
|
||||
@@ -32,7 +33,6 @@ import bodyParser from 'body-parser';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import forceSSL from './forcessl.mw.js';
|
||||
|
||||
import Stream from './eventStreamSource.js';
|
||||
import dbCheck from './middleware/dbCheck.js';
|
||||
|
||||
import cors from 'cors';
|
||||
@@ -115,6 +115,7 @@ export default async function createApp(vite) {
|
||||
app.use(homebrewApi);
|
||||
app.use(adminApi(vite));
|
||||
app.use(vaultApi);
|
||||
app.use(streamApi);
|
||||
|
||||
String.prototype.replaceAll = function(s, r){return this.split(s).join(r);};
|
||||
|
||||
@@ -194,26 +195,6 @@ export default async function createApp(vite) {
|
||||
}
|
||||
});
|
||||
|
||||
// Create Event Stream source for pages to listen to
|
||||
app.get('/stream', (req, res)=>{
|
||||
res.writeHead(200, {
|
||||
'Content-Type' : 'text/event-stream',
|
||||
'Cache-Control' : 'no-cache',
|
||||
'Connection' : 'keep-alive',
|
||||
'Content-Encoding' : 'none'
|
||||
});
|
||||
|
||||
Stream.on('sendUpdate', (event, data)=>{
|
||||
if(isLocalEnvironment) { console.log('Event:', event, '\nData:', data); }
|
||||
res.write(`data: ${JSON.stringify({ ...data, eventType: event })}\n\n`);
|
||||
});
|
||||
});
|
||||
|
||||
// After Stream starts, send initStream event
|
||||
setTimeout(()=>{
|
||||
Stream.emit('sendUpdate', 'initStream', { time: new Date });
|
||||
}, 1000);
|
||||
|
||||
|
||||
// Local only
|
||||
if(isLocalEnvironment){
|
||||
|
||||
@@ -4,4 +4,5 @@ export default nconf
|
||||
.argv()
|
||||
.env({ lowerCase: true })
|
||||
.file('environment', { file: `config/${process.env.NODE_ENV}.json` })
|
||||
.file('development', { file: 'config/dev.json' })
|
||||
.file('defaults', { file: 'config/default.json' });
|
||||
@@ -1,9 +1,94 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import _ from 'lodash';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import config from './config.js';
|
||||
|
||||
// DEBUG
|
||||
const DEBUG = {
|
||||
showEvents : config.get('showStreamEvents'),
|
||||
showSubscriberCount : config.get('showStreamSubscriberCount')
|
||||
};
|
||||
|
||||
// Delays
|
||||
const REPORT_DELAY = config.get('REPORT_DELAY') || 30 * 60 * 1000; //ms
|
||||
const HEARTBEAT_DELAY = config.get('HEARTBEAT_DELAY') || 20 * 1000; //ms
|
||||
|
||||
|
||||
const Stream = new EventEmitter;
|
||||
|
||||
// Create array of stream subscribers
|
||||
let subscribers = [];
|
||||
|
||||
// Listener functions
|
||||
const subscribe = function(shareId, res){
|
||||
const id = nanoid(24); // NanoID is assumed globally unique;
|
||||
const subscriber = {
|
||||
id,
|
||||
shareId,
|
||||
stream : res
|
||||
};
|
||||
|
||||
subscribers.push(subscriber);
|
||||
|
||||
Stream.emit('sendUpdate', 'subscribe', { id: subscriber.id, shareId: subscriber.shareId });
|
||||
|
||||
return subscriber.id;
|
||||
};
|
||||
|
||||
const unsubscribe = function(id){
|
||||
subscribers
|
||||
.filter((sub)=>{return sub.id == id;})
|
||||
.forEach((sub)=>{
|
||||
// Unsubscribe event - not currently used
|
||||
// Stream.emit('sendUpdate', 'unsubscribe', { id: sub.id, shareId: sub.shareId });
|
||||
sub?.stream?.status(200).end();
|
||||
});
|
||||
|
||||
subscribers = subscribers.filter((sub)=>{ return sub.id != id; });
|
||||
};
|
||||
|
||||
// Create heartbeat
|
||||
const heartbeatId = config.get('node_env') != 'test' ?
|
||||
setInterval(()=>{
|
||||
if(DEBUG.showEvents){ console.log('STREAM HEARTBEAT @ ', new Date); };
|
||||
subscribers
|
||||
.forEach((sub)=>{
|
||||
sub?.stream?.write(`:\n\n`);
|
||||
});
|
||||
}, HEARTBEAT_DELAY)
|
||||
:
|
||||
undefined;
|
||||
|
||||
Stream.on('close', ()=>{
|
||||
console.log('STREAM CLOSE');
|
||||
clearInterval(heartbeatId);
|
||||
});
|
||||
|
||||
// Create global sendUpdate listener
|
||||
Stream.on('sendUpdate', (event, data)=>{
|
||||
if(DEBUG.showEvents) console.log('Event:', event, '\nData:', data);
|
||||
subscribers
|
||||
.filter((sub)=>{
|
||||
if(data?.id) return data.id == sub.id;
|
||||
return data.shareId == sub.shareId;
|
||||
})
|
||||
.forEach((sub)=>{
|
||||
sub?.stream?.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
||||
});
|
||||
});
|
||||
|
||||
// DEBUG: Report subscriber count periodically
|
||||
if(DEBUG.showSubscriberCount){
|
||||
const reportIntervalId = setInterval(()=>{
|
||||
console.log('Subscriber count:', subscribers.length, ' @ ', new Date);
|
||||
}, REPORT_DELAY);
|
||||
}
|
||||
|
||||
export default {
|
||||
emit : function(event) {return Stream.emit(event, ...([...arguments].slice(1)));}, // Arguments doesn't work for arrow functions
|
||||
on : (event, listener)=>{return Stream.on(event, listener);},
|
||||
off : (event, listener)=>{return Stream.off(event, listener);}
|
||||
off : (event, listener)=>{return Stream.off(event, listener);},
|
||||
subscribe,
|
||||
unsubscribe
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import express from 'express';
|
||||
|
||||
import Stream from './eventStreamSource.js';
|
||||
|
||||
const app = express.Router();
|
||||
|
||||
// Create Event Stream source for pages to listen to
|
||||
app.get('/api/stream/:id', (req, res)=>{
|
||||
res.writeHead(200, {
|
||||
'Content-Type' : 'text/event-stream',
|
||||
'Cache-Control' : 'no-cache',
|
||||
'Connection' : 'keep-alive',
|
||||
'Content-Encoding' : 'none'
|
||||
});
|
||||
|
||||
const subId = Stream.subscribe(req.params.id, res);
|
||||
|
||||
res.on('close', ()=>{
|
||||
Stream.unsubscribe(subId);
|
||||
});
|
||||
});
|
||||
|
||||
export default app;
|
||||
Reference in New Issue
Block a user