Refactor ItemPage and related components for improved structure and readability

- Updated styles in _main.scss to enhance layout and visual consistency.
- Refactored ItemPage.jsx to utilize new tab components (OverviewTab, QuotesTab, PhotosTab, PresetsTab) for better organization of content.
- Created InfoItem and WarningBanner components for reusable UI elements.
- Implemented QuotesTab and PhotosTab components to handle quotes and photos display logic.
- Added PresetsTab component to manage settings display.
- Enhanced user experience with dynamic tab switching and improved data handling.
This commit is contained in:
alexsparkes
2025-11-02 23:28:46 +00:00
parent 6785011eb5
commit 59824eb6ef
11 changed files with 623 additions and 253 deletions

View File

@@ -145,30 +145,124 @@
display: flex;
flex-flow: row;
justify-content: space-between;
align-items: flex-start;
gap: 30px;
flex-wrap: wrap;
.itemShowcase {
.itemContent {
display: flex;
flex-flow: column;
gap: 25px;
width: 60%;
max-width: 650px;
.description {
max-lines: 3;
font-size: 16px;
}
flex: 1;
img {
width: 100%;
height: auto;
border-radius: 12px;
}
table {
table-layout: fixed;
width: 100%;
max-width: 650px !important;
word-wrap: break-word !important;
font-size: 16px;
border-collapse: collapse;
}
.itemTop {
display: flex;
flex-direction: column;
gap: 18px;
}
.itemTabs {
display: flex;
flex-wrap: wrap;
gap: 12px;
.itemTab {
border-radius: 999px;
padding: 0.55rem 1.3rem;
border: 1px solid transparent;
background: transparent;
font-weight: 600;
cursor: pointer;
transition:
transform 0.2s ease,
box-shadow 0.2s ease;
@include themed {
color: t($subColor);
border-color: rgb(255 255 255 / 10%);
background-color: rgb(255 255 255 / 5%);
}
&:hover {
transform: translateY(-1px);
@include themed {
border-color: rgb(255 255 255 / 18%);
}
}
&.active {
@include themed {
color: t($color);
background-color: t($modal-sidebarActive);
border-color: transparent;
}
}
}
}
.tabContent {
display: flex;
flex-direction: column;
gap: 25px;
}
.itemHighlights {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 18px;
.highlightCard {
display: flex;
flex-direction: column;
gap: 6px;
padding: 18px;
border-radius: 14px;
border: 1px solid transparent;
@include themed {
background-color: t($modal-secondaryColour);
border-color: rgb(255 255 255 / 8%);
box-shadow: 0 0 0 1px t($modal-sidebarActive);
}
.highlightLabel {
font-size: 14px;
@include themed {
color: t($subColor);
}
}
.highlightValue {
font-size: 30px;
font-weight: 600;
@include themed {
color: t($color);
}
}
}
}
.marketplaceDetails {
.moreInfo {
margin-top: 10px;
}
}
}
@@ -177,8 +271,9 @@
background-repeat: no-repeat;
background-size: cover;
border-radius: 15px;
width: 30%;
max-width: 300px;
flex: 0 0 300px;
width: 300px;
max-width: 100%;
max-height: 700px;
.front {
@@ -190,6 +285,7 @@
width: 100%;
box-sizing: border-box !important;
border-radius: 12px 12px 0 0;
-webkit-backdrop-filter: blur(40px) saturate(150%) brightness(75%);
backdrop-filter: blur(40px) saturate(150%) brightness(75%);
@include themed {
@@ -269,6 +365,7 @@
flex-flow: column;
text-align: center;
align-items: center;
-webkit-user-select: none;
user-select: none;
img {
@@ -382,6 +479,7 @@ p.author {
.nice-tag {
border-radius: 150px;
padding: 1px 12px;
-webkit-backdrop-filter: blur(16px) saturate(180%);
backdrop-filter: blur(16px) saturate(180%);
background-color: rgb(255 255 255 / 10%);
border: 1px solid rgb(209 213 219 / 30%);
@@ -437,6 +535,7 @@ p.author {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 5;
line-clamp: 5;
}
}
@@ -559,6 +658,7 @@ p.author {
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
-webkit-user-select: none;
user-select: none;
@include themed {

View File

@@ -74,6 +74,8 @@ p.description {
display: flex;
flex-flow: row;
justify-content: space-between;
flex-wrap: wrap;
align-items: center;
gap: 25px;
.itemWarning {
@@ -167,6 +169,7 @@ p.description {
gap: 15px;
.subtitle {
-webkit-user-select: text !important;
user-select: text !important;
}
}

View File

@@ -1,28 +1,26 @@
import variables from 'config/variables';
import { PureComponent, Fragment } from 'react';
import { toast } from 'react-toastify';
import {
MdIosShare,
MdFlag,
MdAccountCircle,
MdCalendarMonth,
MdFormatQuote,
MdImage,
MdTranslate,
MdOutlineWarning,
MdStyle,
} from 'react-icons/md';
import { MdIosShare, MdFlag, MdAccountCircle } from 'react-icons/md';
import Modal from 'react-modal';
import { Header } from 'components/Layout/Settings';
import { Button } from 'components/Elements';
import { install, uninstall } from 'utils/marketplace';
import { Carousel } from '../components/Elements/Carousel';
import { ShareModal } from 'components/Elements';
import placeholderIcon from 'assets/icons/marketplace-placeholder.png';
import { Items } from '../components/Items/Items';
// Tab components
import OverviewTab from './components/OverviewTab';
import QuotesTab from './components/QuotesTab';
import PhotosTab from './components/PhotosTab';
import PresetsTab from './components/PresetsTab';
// Helper components
import InfoItem from './components/InfoItem';
import WarningBanner from './components/WarningBanner';
class ItemPage extends PureComponent {
constructor(props) {
super(props);
@@ -33,6 +31,7 @@ class ItemPage extends PureComponent {
shareModal: false,
count: 5,
moreByCurator: [],
activeTab: 'overview',
};
}
@@ -63,11 +62,18 @@ class ItemPage extends PureComponent {
}
incrementCount(type) {
const newCount =
this.state.count !== this.props.data.data[type].length
? this.props.data.data[type].length
: 5;
const data = this.props.data.data;
let length;
if (type === 'quotes' && Array.isArray(data.quotes)) {
length = data.quotes.length;
} else if (type === 'settings' && data.settings) {
length = Object.keys(data.settings).length;
} else {
return;
}
const newCount = this.state.count !== length ? length : 5;
this.setState({ count: newCount });
}
@@ -84,18 +90,6 @@ class ItemPage extends PureComponent {
const locale = localStorage.getItem('language');
const shortLocale = locale.includes('_') ? locale.split('_')[0] : locale;
const languageNames = new Intl.DisplayNames([shortLocale], { type: 'language' });
const convertedType = (() => {
const map = {
photos: 'photo_packs',
quotes: 'quote_packs',
settings: 'preset_settings',
};
return map[this.props.data.data.type];
})();
const moreByCurator = this.state.moreByCurator
.filter((item) => item.type === convertedType && item.name !== this.props.data.data.name)
.sort(() => 0.5 - Math.random())
.slice(0, 3);
// Extract colour from data (British spelling as used in API)
const mainColor = this.props.data.data.colour;
@@ -117,6 +111,25 @@ class ItemPage extends PureComponent {
const isLight = isLightColor(mainColor);
const textColor = isLight ? '#000000' : '#ffffff';
const { activeTab } = this.state;
const quotes = Array.isArray(this.props.data.data.quotes) ? this.props.data.data.quotes : [];
const photos = Array.isArray(this.props.data.data.photos) ? this.props.data.data.photos : [];
const settings = this.props.data.data.settings;
const hasPhotos = photos.length > 0;
const hasQuotes = quotes.length > 0;
const hasSettings = !!settings;
// Format date for details section
let formattedDate = '';
if (this.props.data.data.updated_at) {
const dateObj = new Date(this.props.data.data.updated_at);
formattedDate = new Intl.DateTimeFormat(shortLocale, {
year: 'numeric',
month: 'long',
day: '2-digit',
}).format(dateObj);
}
// Create dynamic styles for theming with the main color
const themedStyles = mainColor ? (
<style>{`
@@ -190,12 +203,6 @@ class ItemPage extends PureComponent {
return null;
}
// prevent console error
let iconsrc = this.props.data.icon;
if (!this.props.data.icon) {
iconsrc = null;
}
let updateButton;
if (this.state.showUpdateButton) {
updateButton = (
@@ -209,52 +216,10 @@ class ItemPage extends PureComponent {
);
}
const itemWarning = () => {
const template = (message) => (
<div className="itemWarning">
<MdOutlineWarning />
<div className="text">
<span className="header">Warning</span>
<span>{message}</span>
</div>
</div>
);
if (this.props.data.data.sideload === true) {
return template(variables.getMessage('modals.main.marketplace.product.sideload_warning'));
}
if (this.props.data.data.image_api === true) {
return template(variables.getMessage('modals.main.marketplace.product.third_party_api'));
}
if (this.props.data.data.language !== undefined && this.props.data.data.language !== null) {
if (shortLocale !== this.props.data.data.language) {
return template(variables.getMessage('modals.main.marketplace.product.not_in_language'));
}
}
return null;
};
const moreInfoItem = (icon, header, text) => (
<div className="infoItem">
{icon}
<div className="text">
<span className="header">{header}</span>
<span>{text}</span>
</div>
</div>
);
let dateObj, formattedDate;
if (this.props.data.data.updated_at) {
dateObj = new Date(this.props.data.data.updated_at);
formattedDate = new Intl.DateTimeFormat(shortLocale, {
year: 'numeric',
month: 'long',
day: '2-digit',
}).format(dateObj);
// prevent console error
let iconsrc = this.props.data.icon;
if (!this.props.data.icon) {
iconsrc = null;
}
return (
@@ -291,135 +256,7 @@ class ItemPage extends PureComponent {
goBack={this.props.toggleFunction}
/> */}
<div className="itemPage">
<div className="itemShowcase">
<div className="subHeader">
{moreInfoItem(
<MdAccountCircle />,
variables.getMessage('modals.main.marketplace.product.created_by'),
this.props.data.author,
)}
{itemWarning()}
</div>
{this.props.data.data.photos && (
<div className="carousel">
<div className="carousel_container">
<Carousel data={this.props.data.data.photos} />
</div>
</div>
)}
{this.props.data.data.settings && (
<img
alt="product"
draggable={false}
src={iconsrc}
onError={(e) => {
e.target.onerror = null;
e.target.src = placeholderIcon;
}}
/>
)}
<div className="marketplaceDescription">
<span className="title">
{variables.getMessage('modals.main.marketplace.product.description')}
</span>
<span
className="subtitle"
dangerouslySetInnerHTML={{ __html: this.props.data.description }}
/>
</div>
{this.props.data.data.quotes && (
<>
<table>
<tbody>
<tr>
<th>{variables.getMessage('modals.main.settings.sections.quote.title')}</th>
<th>{variables.getMessage('modals.main.settings.sections.quote.author')}</th>
</tr>
{this.props.data.data.quotes.slice(0, this.state.count).map((quote, index) => (
<tr key={index}>
<td>{quote.quote}</td>
<td>{quote.author}</td>
</tr>
))}
</tbody>
</table>
<div className="showMoreItems">
<span className="link" onClick={() => this.incrementCount('quotes')}>
{this.state.count !== this.props.data.data.quotes.length
? variables.getMessage('modals.main.marketplace.product.show_all')
: variables.getMessage('modals.main.marketplace.product.show_less')}
</span>
</div>
</>
)}
{this.props.data.data.settings && (
<>
<table>
<tbody>
<tr>
<th>{variables.getMessage('modals.main.marketplace.product.setting')}</th>
<th>{variables.getMessage('modals.main.marketplace.product.value')}</th>
</tr>
{Object.entries(this.props.data.data.settings)
.slice(0, this.state.count)
.map(([key, value]) => (
<tr key={key}>
<td>{key}</td>
<td>{value}</td>
</tr>
))}
</tbody>
</table>
<div className="showMoreItems">
<span className="link" onClick={() => this.incrementCount('settings')}>
{this.state.count !== this.props.data.data.settings.length
? variables.getMessage('modals.main.marketplace.product.show_all')
: variables.getMessage('modals.main.marketplace.product.show_less')}
</span>
</div>
</>
)}
<div className="marketplaceDescription">
<span className="title">
{variables.getMessage('modals.main.marketplace.product.details')}
</span>
<div className="moreInfo">
{this.props.data.data.updated_at &&
moreInfoItem(
<MdCalendarMonth />,
variables.getMessage('modals.main.marketplace.product.updated_at'),
formattedDate,
)}
{this.props.data.data.quotes &&
moreInfoItem(
<MdFormatQuote />,
variables.getMessage('modals.main.marketplace.product.no_quotes'),
this.props.data.data.quotes.length,
)}
{this.props.data.data.photos &&
moreInfoItem(
<MdImage />,
variables.getMessage('modals.main.marketplace.product.no_images'),
this.props.data.data.photos.length,
)}
{this.props.data.data.quotes && this.props.data.data.language
? moreInfoItem(
<MdTranslate />,
variables.getMessage('modals.main.settings.sections.language.title'),
languageNames.of(this.props.data.data.language),
)
: null}
{moreInfoItem(
<MdStyle />,
variables.getMessage('modals.main.settings.sections.background.type.title'),
variables.getMessage(
'modals.main.marketplace.' + this.getName(this.props.data.data.type),
) || 'marketplace',
)}
</div>
</div>
</div>
<div className="itemInfo">
<aside className="itemInfo">
<div className="front">
<img
className="icon"
@@ -432,7 +269,10 @@ class ItemPage extends PureComponent {
}}
/>
{localStorage.getItem('welcomePreview') !== 'true' ? (
this.props.button
<>
{this.props.button}
{updateButton}
</>
) : (
<p style={{ textAlign: 'center' }}>
{variables.getMessage(
@@ -470,26 +310,106 @@ class ItemPage extends PureComponent {
</>
)}
{this.props.data.data.in_collections?.length > 0 && (
<div>
<div className="inCollection">
<span className="subtitle">
{variables.getMessage('modals.main.marketplace.product.part_of')}
</span>
<span
className="title"
onClick={() =>
this.props.toggleFunction(
'collection',
this.props.data.data.in_collections[0].name,
)
}
>
{this.props.data.data.in_collections[0].display_name}
</span>
</div>
<div className="inCollection">
<span className="subtitle">
{variables.getMessage('modals.main.marketplace.product.part_of')}
</span>
<span
className="title"
onClick={() =>
this.props.toggleFunction(
'collection',
this.props.data.data.in_collections[0].name,
)
}
>
{this.props.data.data.in_collections[0].display_name}
</span>
</div>
)}
</div>
</aside>
<div className="itemContent">
<div className="itemTop">
<div className="subHeader">
<InfoItem
icon={<MdAccountCircle />}
header={variables.getMessage('modals.main.marketplace.product.created_by')}
text={this.props.data.author}
/>
<WarningBanner data={this.props.data.data} shortLocale={shortLocale} />
</div>
<div className="itemTabs">
<button
type="button"
className={`itemTab ${activeTab === 'overview' ? 'active' : ''}`}
onClick={() => this.setState({ activeTab: 'overview' })}
>
{variables.getMessage('modals.main.marketplace.product.overview_tab') ||
'Overview'}
</button>
{hasQuotes && (
<button
type="button"
className={`itemTab ${activeTab === 'quotes' ? 'active' : ''}`}
onClick={() => this.setState({ activeTab: 'quotes' })}
>
{variables.getMessage('modals.main.marketplace.product.quotes_tab') || 'Quotes'}
</button>
)}
{hasPhotos && (
<button
type="button"
className={`itemTab ${activeTab === 'photos' ? 'active' : ''}`}
onClick={() => this.setState({ activeTab: 'photos' })}
>
{variables.getMessage('modals.main.marketplace.product.photos_tab') || 'Photos'}
</button>
)}
{hasSettings && (
<button
type="button"
className={`itemTab ${activeTab === 'presets' ? 'active' : ''}`}
onClick={() => this.setState({ activeTab: 'presets' })}
>
{variables.getMessage('modals.main.marketplace.product.presets_tab') ||
'Presets'}
</button>
)}
</div>
</div>
<div className="tabContent">
{activeTab === 'overview' && (
<OverviewTab
data={this.props.data.data}
description={this.props.data.description}
iconsrc={iconsrc}
shortLocale={shortLocale}
languageNames={languageNames}
formattedDate={formattedDate}
getName={this.getName}
count={this.state.count}
onIncrementCount={(type) => this.incrementCount(type)}
/>
)}
{activeTab === 'quotes' && hasQuotes && (
<QuotesTab
quotes={quotes}
count={this.state.count}
onIncrementCount={(type) => this.incrementCount(type)}
/>
)}
{activeTab === 'photos' && hasPhotos && <PhotosTab photos={photos} />}
{activeTab === 'presets' && hasSettings && (
<PresetsTab
settings={settings}
count={this.state.count}
onIncrementCount={(type) => this.incrementCount(type)}
/>
)}
</div>
</div>
</div>
{/* {moreByCurator.length > 1 && (

View File

@@ -0,0 +1,11 @@
const InfoItem = ({ icon, header, text }) => (
<div className="infoItem">
{icon}
<div className="text">
<span className="header">{header}</span>
<span>{text}</span>
</div>
</div>
);
export default InfoItem;

View File

@@ -0,0 +1,134 @@
import variables from 'config/variables';
import { MdCalendarMonth, MdFormatQuote, MdImage, MdTranslate, MdStyle } from 'react-icons/md';
import { Carousel } from '../../components/Elements/Carousel';
import placeholderIcon from 'assets/icons/marketplace-placeholder.png';
import InfoItem from './InfoItem';
const OverviewTab = ({
data,
description,
iconsrc,
shortLocale,
languageNames,
formattedDate,
getName,
count,
onIncrementCount,
}) => {
const quotes = Array.isArray(data.quotes) ? data.quotes : [];
const photos = Array.isArray(data.photos) ? data.photos : [];
const hasPhotos = photos.length > 0;
const hasQuotes = quotes.length > 0;
const hasSettings = data.settings;
// Quote statistics
const totalQuotes = hasQuotes ? quotes.length : 0;
const uniqueAuthorsCount = hasQuotes ? new Set(quotes.map((quote) => quote.author)).size : 0;
const averageCharacters = hasQuotes
? Math.round(
quotes.reduce(
(accumulator, quote) => accumulator + (quote.quote ? quote.quote.length : 0),
0,
) / quotes.length,
)
: 0;
const totalQuotesLabel =
variables.getMessage('modals.main.marketplace.product.no_quotes') || 'Total quotes';
const uniqueAuthorsLabel =
variables.getMessage('modals.main.marketplace.product.unique_authors') || 'Unique authors';
const averageCharactersLabel =
variables.getMessage('modals.main.marketplace.product.average_characters') || 'Avg. characters';
const formatNumber = (value) =>
typeof value === 'number' ? value.toLocaleString(shortLocale) : value;
return (
<>
{/* Preview image for settings presets */}
{hasSettings && (
<img
alt="product"
draggable={false}
src={iconsrc}
onError={(e) => {
e.target.onerror = null;
e.target.src = placeholderIcon;
}}
/>
)}
{/* Description */}
<div className="marketplaceDescription">
<span className="title">
{variables.getMessage('modals.main.marketplace.product.description')}
</span>
<span className="subtitle" dangerouslySetInnerHTML={{ __html: description }} />
</div>
{/* Quote pack statistics */}
{hasQuotes && (
<div className="itemHighlights">
<div className="highlightCard">
<span className="highlightLabel">{totalQuotesLabel}</span>
<span className="highlightValue">{formatNumber(totalQuotes)}</span>
</div>
<div className="highlightCard">
<span className="highlightLabel">{uniqueAuthorsLabel}</span>
<span className="highlightValue">{formatNumber(uniqueAuthorsCount)}</span>
</div>
<div className="highlightCard">
<span className="highlightLabel">{averageCharactersLabel}</span>
<span className="highlightValue">{formatNumber(averageCharacters)}</span>
</div>
</div>
)}
{/* Details section */}
<div className="marketplaceDescription marketplaceDetails">
<span className="title">
{variables.getMessage('modals.main.marketplace.product.details')}
</span>
<div className="moreInfo">
{data.updated_at && (
<InfoItem
icon={<MdCalendarMonth />}
header={variables.getMessage('modals.main.marketplace.product.updated_at')}
text={formattedDate}
/>
)}
{hasQuotes && (
<InfoItem
icon={<MdFormatQuote />}
header={variables.getMessage('modals.main.marketplace.product.no_quotes')}
text={quotes.length}
/>
)}
{hasPhotos && (
<InfoItem
icon={<MdImage />}
header={variables.getMessage('modals.main.marketplace.product.no_images')}
text={photos.length}
/>
)}
{hasQuotes && data.language && (
<InfoItem
icon={<MdTranslate />}
header={variables.getMessage('modals.main.settings.sections.language.title')}
text={languageNames.of(data.language)}
/>
)}
<InfoItem
icon={<MdStyle />}
header={variables.getMessage('modals.main.settings.sections.background.type.title')}
text={
variables.getMessage('modals.main.marketplace.' + getName(data.type)) || 'marketplace'
}
/>
</div>
</div>
</>
);
};
export default OverviewTab;

View File

@@ -0,0 +1,13 @@
import { Carousel } from '../../components/Elements/Carousel';
const PhotosTab = ({ photos }) => {
return (
<div className="carousel">
<div className="carousel_container">
<Carousel data={photos} />
</div>
</div>
);
};
export default PhotosTab;

View File

@@ -0,0 +1,33 @@
import variables from 'config/variables';
const PresetsTab = ({ settings, count, onIncrementCount }) => {
return (
<>
<table>
<tbody>
<tr>
<th>{variables.getMessage('modals.main.marketplace.product.setting')}</th>
<th>{variables.getMessage('modals.main.marketplace.product.value')}</th>
</tr>
{Object.entries(settings)
.slice(0, count)
.map(([key, value]) => (
<tr key={key}>
<td>{key}</td>
<td>{value}</td>
</tr>
))}
</tbody>
</table>
<div className="showMoreItems">
<span className="link" onClick={() => onIncrementCount('settings')}>
{count !== Object.keys(settings).length
? variables.getMessage('modals.main.marketplace.product.show_all')
: variables.getMessage('modals.main.marketplace.product.show_less')}
</span>
</div>
</>
);
};
export default PresetsTab;

View File

@@ -0,0 +1,31 @@
import variables from 'config/variables';
const QuotesTab = ({ quotes, count, onIncrementCount }) => {
return (
<>
<table>
<tbody>
<tr>
<th>{variables.getMessage('modals.main.settings.sections.quote.title')}</th>
<th>{variables.getMessage('modals.main.settings.sections.quote.author')}</th>
</tr>
{quotes.slice(0, count).map((quote, index) => (
<tr key={index}>
<td>{quote.quote}</td>
<td>{quote.author}</td>
</tr>
))}
</tbody>
</table>
<div className="showMoreItems">
<span className="link" onClick={() => onIncrementCount('quotes')}>
{count !== quotes.length
? variables.getMessage('modals.main.marketplace.product.show_all')
: variables.getMessage('modals.main.marketplace.product.show_less')}
</span>
</div>
</>
);
};
export default QuotesTab;

View File

@@ -0,0 +1,32 @@
import variables from 'config/variables';
import { MdOutlineWarning } from 'react-icons/md';
const WarningBanner = ({ data, shortLocale }) => {
const template = (message) => (
<div className="itemWarning">
<MdOutlineWarning />
<div className="text">
<span className="header">Warning</span>
<span>{message}</span>
</div>
</div>
);
if (data.sideload === true) {
return template(variables.getMessage('modals.main.marketplace.product.sideload_warning'));
}
if (data.image_api === true) {
return template(variables.getMessage('modals.main.marketplace.product.third_party_api'));
}
if (data.language !== undefined && data.language !== null) {
if (shortLocale !== data.language) {
return template(variables.getMessage('modals.main.marketplace.product.not_in_language'));
}
}
return null;
};
export default WarningBanner;

View File

@@ -0,0 +1,9 @@
// Tab components
export { default as OverviewTab } from './OverviewTab';
export { default as QuotesTab } from './QuotesTab';
export { default as PhotosTab } from './PhotosTab';
export { default as PresetsTab } from './PresetsTab';
// Helper components
export { default as InfoItem } from './InfoItem';
export { default as WarningBanner } from './WarningBanner';