refactor(features): Change organisation of welcome + Marketplace

This commit is contained in:
alexsparkes
2024-02-27 17:07:32 +00:00
parent 6041372860
commit dbd85cdc95
43 changed files with 7 additions and 7 deletions

View File

@@ -8,7 +8,7 @@ import Preview from '../../helpers/preview/Preview';
import EventBus from 'utils/eventbus';
import Welcome from './welcome/Welcome';
import Welcome from 'features/welcome/Welcome';
export default class Modals extends PureComponent {
constructor() {

View File

@@ -1,53 +0,0 @@
import { MdOutlineArrowForward, MdOutlineOpenInNew } from 'react-icons/md';
import { Button } from 'components/Elements';
import variables from 'config/variables';
const Collection = ({ collection, toggle, collectionFunction }) => {
const { news, background_colour, img, display_name, description, name } = collection;
const getStyle = () => {
if (news) {
return { backgroundColor: background_colour };
}
return {
backgroundImage: `linear-gradient(to left, #000, transparent, #000), url('${variables.constants.DDG_IMAGE_PROXY + img}')`,
};
};
return (
<div className="collection" style={getStyle()}>
<div className="content">
<span className="title">{display_name} using component</span>
<span className="subtitle">{description}</span>
</div>
{collection.news === true ? (
<a
className="btn-collection"
href={collection.news_link}
target="_blank"
rel="noopener noreferrer"
>
{variables.getMessage('modals.main.marketplace.learn_more')} <MdOutlineOpenInNew />
</a>
) : (
<Button
type="collection"
onClick={() => collectionFunction(collection.name)}
icon={<MdOutlineArrowForward />}
label={variables.getMessage('modals.main.marketplace.explore_collection')}
iconPlacement={'right'}
/>
)}
{/*<Button
type="collection"
onClick={() => toggle('collection', name)}
icon={<MdOutlineArrowForward />}
label={variables.getMessage('modals.main.marketplace.explore_collection')}
iconPlacement="right"
/>*/}
</div>
);
};
export default Collection;
export { Collection };

View File

@@ -1 +0,0 @@
export * from './Collection';

View File

@@ -1,91 +0,0 @@
import React, { useState, useEffect, useCallback, useRef, memo } from 'react';
import { MdOutlineArrowForwardIos, MdOutlineArrowBackIos } from 'react-icons/md';
import useEmblaCarousel from 'embla-carousel-react';
import Autoplay from 'embla-carousel-autoplay';
import variables from 'config/variables';
import './carousel.scss';
function EmblaCarousel({ data }) {
const autoplay = useRef(
Autoplay({ delay: 3000, stopOnInteraction: false }, (emblaRoot) => emblaRoot.parentElement),
);
const [emblaRef, emblaApi] = useEmblaCarousel({ loop: false }, [autoplay.current]);
const [prevBtnEnabled, setPrevBtnEnabled] = useState(false);
const [nextBtnEnabled, setNextBtnEnabled] = useState(false);
const scroll = useCallback(
(direction) => {
if (!emblaApi) {
return;
}
if (direction === 'next') {
emblaApi.scrollNext();
} else {
emblaApi.scrollPrev();
}
autoplay.current.reset();
},
[emblaApi],
);
const onSelect = useCallback(() => {
if (!emblaApi) {
return;
}
setPrevBtnEnabled(emblaApi.canScrollPrev());
setNextBtnEnabled(emblaApi.canScrollNext());
}, [emblaApi]);
useEffect(() => {
if (!emblaApi) {
return;
}
onSelect();
emblaApi.on('select', onSelect);
}, [emblaApi, onSelect]);
return (
<div className="carousel">
<div className="carousel_viewport" ref={emblaRef}>
<div className="carousel_container">
{data.map((photo, index) => (
<div className="carousel_slide" key={index}>
<div className="carousel_slide_inner">
<img
src={variables.constants.DDG_IMAGE_PROXY + photo.url.default}
alt="Marketplace example screenshot"
/>
</div>
</div>
))}
</div>
</div>
{data.length > 1 && (
<>
<button
className="carousel_button prev btn-icon"
onClick={() => scroll('prev')}
disabled={!prevBtnEnabled}
>
<MdOutlineArrowBackIos />
</button>
<button
className="carousel_button next btn-icon"
onClick={() => scroll('next')}
disabled={!nextBtnEnabled}
>
<MdOutlineArrowForwardIos />
</button>
</>
)}
</div>
);
}
const Carousel = memo(EmblaCarousel);
export { Carousel as default, Carousel };

View File

@@ -1,84 +0,0 @@
.carousel {
position: relative;
width: 100%;
margin-left: 5px;
}
.carousel_viewport {
overflow: hidden;
width: 100%;
&.is-draggable {
cursor: move;
}
&.is-dragging {
cursor: grabbing;
}
border-radius: 15px !important;
}
.carousel_container {
display: flex;
-webkit-touch-callout: none;
-webkit-user-select: none;
user-select: none;
-webkit-tap-highlight-color: transparent;
margin-left: -10px;
}
.carousel_slide {
position: relative;
min-width: 100%;
padding-left: 10px;
img {
position: absolute;
display: block;
top: 50%;
left: 50%;
width: auto;
min-height: 100%;
min-width: 100%;
max-width: none;
transform: translate(-50%, -50%);
}
}
.carousel_slide_inner {
position: relative;
overflow: hidden;
height: 250px;
}
.carousel_button {
outline: 0;
cursor: pointer;
touch-action: manipulation;
position: absolute;
z-index: 1;
top: 50%;
transform: translateY(-50%);
border: 0;
width: 30px !important;
height: 30px !important;
display: grid;
place-items: center;
justify-content: center;
align-items: center;
padding: 0;
&:disabled {
cursor: default;
opacity: 0.3;
}
&.prev {
left: 27px;
}
&.next {
right: 27px;
}
}

View File

@@ -1 +0,0 @@
export * from './Carousel';

View File

@@ -1,19 +0,0 @@
import { memo } from 'react';
import variables from 'config/variables';
function Lightbox({ modalClose, img }) {
variables.stats.postEvent('modal', 'Opened lightbox');
return (
<>
<span className="closeModal" onClick={modalClose}>
&times;
</span>
<img src={img} className="lightboximg" draggable={false} alt="Item screenshot" />
</>
);
}
const MemoizedLightbox = memo(Lightbox);
export default MemoizedLightbox;
export { MemoizedLightbox as Lightbox };

View File

@@ -1 +0,0 @@
export * from './Lightbox';

View File

@@ -1,28 +0,0 @@
import { memo } from 'react';
import variables from 'config/variables';
import { MdClose } from 'react-icons/md';
import { Tooltip } from 'components/Elements';
function SideloadFailedModal({ modalClose, reason }) {
return (
<div className="smallModal">
<div className="shareHeader">
<span className="title">{variables.getMessage('modals.main.error_boundary.title')}</span>
<Tooltip
title={variables.getMessage('modals.main.settings.sections.advanced.reset_modal.cancel')}
>
<div className="close" onClick={modalClose}>
<MdClose />
</div>
</Tooltip>
</div>
<span>{variables.getMessage('modals.main.addons.sideload.failed')}</span>
<span className="subtitle">{reason}</span>
</div>
);
}
const MemoizedSideloadFailedModal = memo(SideloadFailedModal);
export default MemoizedSideloadFailedModal;
export { MemoizedSideloadFailedModal as SideloadFailedModal };

View File

@@ -1 +0,0 @@
export * from './SideloadFailedModal';

View File

@@ -1,3 +0,0 @@
export * from './Carousel';
export * from './SideloadFailedModal';
export * from './Lightbox';

View File

@@ -1,307 +0,0 @@
import variables from 'config/variables';
import { PureComponent, Fragment } from 'react';
import { toast } from 'react-toastify';
import {
MdIosShare,
MdFlag,
MdAccountCircle,
MdBugReport,
MdFormatQuote,
MdImage,
MdTranslate,
MdExpandMore,
MdExpandLess,
MdStyle,
} 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 '../Elements/Carousel';
import { ShareModal } from 'components/Elements';
class Item extends PureComponent {
constructor(props) {
super(props);
this.state = {
showLightbox: false,
showUpdateButton:
this.props.addonInstalled === true &&
this.props.addonInstalledVersion !== this.props.data.version,
shareModal: false,
count: 5,
};
}
updateAddon() {
uninstall(this.props.data.type, this.props.data.display_name);
install(this.props.data.type, this.props.data);
toast(variables.getMessage('toasts.updated'));
this.setState({
showUpdateButton: false,
});
}
incrementCount(type) {
const newCount =
this.state.count !== this.props.data.data[type].length
? this.props.data.data[type].length
: 5;
this.setState({ count: newCount });
}
getName(name) {
const nameMappings = {
photos: 'photo_packs',
quotes: 'quote_packs',
settings: 'preset_settings',
};
return nameMappings[name] || name;
}
render() {
if (!this.props.data.display_name) {
return null;
}
// prevent console error
let iconsrc = variables.constants.DDG_IMAGE_PROXY + this.props.data.icon;
if (!this.props.data.icon) {
iconsrc = null;
}
let updateButton;
if (this.state.showUpdateButton) {
updateButton = (
<Fragment key="update">
<button className="removeFromMue" onClick={() => this.updateAddon()}>
{variables.getMessage('modals.main.addons.product.buttons.update_addon')}
</button>
</Fragment>
);
}
const moreInfoItem = (icon, header, text) => (
<div className="infoItem">
{icon}
<div className="text">
<span className="header">{header}</span>
<span>{text}</span>
</div>
</div>
);
return (
<div id="item">
<Modal
closeTimeoutMS={300}
isOpen={this.state.shareModal}
className="Modal mainModal"
overlayClassName="Overlay"
ariaHideApp={false}
onRequestClose={() => this.setState({ shareModal: false })}
>
<ShareModal
data={
variables.constants.API_URL + '/marketplace/share/' + btoa(this.props.data.api_name)
}
modalClose={() => this.setState({ shareModal: false })}
/>
</Modal>
<Header
title={variables.getMessage('modals.main.navbar.marketplace')}
secondaryTitle={this.props.data.data.display_name}
report={false}
goBack={this.props.toggleFunction}
/>
<div className="itemPage">
<div className="itemShowcase">
{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}
onClick={() => this.setState({ showLightbox: true })}
/>
)}
{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 ? (
<>
<MdExpandMore />{' '}
{variables.getMessage('modals.main.marketplace.product.show_all')}
</>
) : (
<>
<MdExpandLess />{' '}
{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 ? (
<>
<MdExpandMore />{' '}
{variables.getMessage('modals.main.marketplace.product.show_all')}
</>
) : (
<>
<MdExpandLess />{' '}
{variables.getMessage('modals.main.marketplace.product.show_less')}
</>
)}
</span>
</div>
</>
)}
<div>
<p className="title">
{variables.getMessage('modals.main.marketplace.product.description')}
</p>
<p dangerouslySetInnerHTML={{ __html: this.props.data.description }} />
</div>
<div className="moreInfo">
{moreInfoItem(
<MdBugReport />,
variables.getMessage('modals.main.marketplace.product.version'),
updateButton ? (
<span>
{this.props.data.version} (Installed: {this.props.data.addonInstalledVersion})
</span>
) : (
<span>{this.props.data.version}</span>
),
)}
{moreInfoItem(
<MdAccountCircle />,
variables.getMessage('modals.main.marketplace.product.author'),
this.props.data.author,
)}
{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'),
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
className="itemInfo"
style={{
backgroundImage: `url("${
variables.constants.DDG_IMAGE_PROXY + this.props.data.data.icon_url
}")`,
}}
>
<div className="front">
<img
className="icon"
alt="icon"
draggable={false}
src={variables.constants.DDG_IMAGE_PROXY + this.props.data.data.icon_url}
/>
{this.props.button}
<div className="iconButtons">
<Button
type="icon"
onClick={() => this.setState({ shareModal: true })}
icon={<MdIosShare />}
tooltipTitle={variables.getMessage('widgets.quote.share')}
tooltipKey="share"
/>
<Button
type="icon"
onClick={() => this.setState({ shareModal: true })}
icon={<MdFlag />}
tooltipTitle={variables.getMessage(
'modals.main.marketplace.product.buttons.report',
)}
tooltipKey="report"
/>
</div>
{this.props.data.data.collection && (
<div className="inCollection">
<span className="subtitle">
{variables.getMessage('modals.main.marketplace.product.part_of')}
</span>
<span className="title">{this.props.data.data.collection}</span>
<button>{variables.getMessage('modals.main.marketplace.product.explore')}</button>
</div>
)}
</div>
</div>
</div>
</div>
);
}
}
export { Item as default, Item };

View File

@@ -1,119 +0,0 @@
import variables from 'config/variables';
import React, { memo } from 'react';
import { MdAutoFixHigh, MdOutlineArrowForward, MdOutlineOpenInNew } from 'react-icons/md';
import { Button } from 'components/Elements';
import MemoizedLightbox from '../Elements/Lightbox/Lightbox';
function Items({
type,
items,
collection,
toggleFunction,
collectionFunction,
onCollection,
filter,
}) {
return (
<>
{(type === 'all' && !onCollection && (filter === null || filter === '')) ||
(type === 'collections' && !onCollection && (filter === null || filter === '')) ? (
<>
<div
className="collection"
style={
collection.news
? { backgroundColor: collection.background_colour }
: {
backgroundImage: `linear-gradient(to right, rgba(0, 0, 0, 0.9), rgba(0, 0, 0, 0.7), transparent, rgba(0, 0, 0, 0.7), rgba(0 ,0, 0, 0.9)), url('${collection.img}')`,
}
}
>
<div className="content">
<span className="title">{collection.display_name}</span>
<span className="subtitle">{collection.description}</span>
</div>
{collection.news === true ? (
<a
className="btn-collection"
href={collection.news_link}
target="_blank"
rel="noopener noreferrer"
>
{variables.getMessage('modals.main.marketplace.learn_more')} <MdOutlineOpenInNew />
</a>
) : (
<Button
type="collection"
onClick={() => collectionFunction(collection.name)}
icon={<MdOutlineArrowForward />}
label={variables.getMessage('modals.main.marketplace.explore_collection')}
iconPlacement={'right'}
/>
)}
</div>
</>
) : null}
<div className="items">
{items
?.filter(
(item) =>
item.name.toLowerCase().includes(filter.toLowerCase()) ||
filter === '' ||
item.author.toLowerCase().includes(filter.toLowerCase()) ||
item.type.toLowerCase().includes(filter.toLowerCase()),
)
.map((item) => (
<div className="item" onClick={() => toggleFunction(item)} key={item.name}>
<img
className="item-back"
alt=""
draggable={false}
src={variables.constants.DDG_IMAGE_PROXY + item.icon_url}
aria-hidden="true"
/>
<img
className="item-icon"
alt="icon"
draggable={false}
src={variables.constants.DDG_IMAGE_PROXY + item.icon_url}
/>
<div className="card-details">
<span className="card-title">{item.display_name || item.name}</span>
<span className="card-subtitle">
{variables.getMessage('modals.main.marketplace.by', { author: item.author })}
</span>
{type === 'all' && !onCollection ? (
<span className="card-type">
{variables.getMessage('modals.main.marketplace.' + item.type)}
</span>
) : null}
</div>
</div>
))}
</div>
<div className="loader"></div>
{type === 'all' && !onCollection ? (
<div className="createYourOwn">
<MdAutoFixHigh />
<span className="title">{variables.getMessage('modals.main.marketplace.cant_find')}</span>
<span className="subtitle">
{variables.getMessage('modals.main.marketplace.knowledgebase_one') + ' '}
<a
className="link"
target="_blank"
href={variables.constants.KNOWLEDGEBASE}
rel="noreferrer"
>
{variables.getMessage('modals.main.marketplace.knowledgebase_two')}
</a>
{' ' + variables.getMessage('modals.main.marketplace.knowledgebase_three')}
</span>
</div>
) : null}
</>
);
}
const MemoizedItems = memo(Items);
export { MemoizedItems as default, MemoizedItems as Items };

View File

@@ -1,2 +0,0 @@
export * from './Elements';
export * from './Items';

View File

@@ -1,290 +0,0 @@
import variables from 'config/variables';
import { PureComponent } from 'react';
import { MdUpdate, MdOutlineExtensionOff, MdSendTimeExtension } from 'react-icons/md';
import { toast } from 'react-toastify';
import Modal from 'react-modal';
import { SideloadFailedModal } from '../components/Elements/SideloadFailedModal/SideloadFailedModal';
import Item from '../components/Items/Item';
import Items from '../components/Items/Items';
import { Dropdown, FileUpload } from 'components/Form/Settings';
import { Header, CustomActions } from 'components/Layout/Settings';
import { Button } from 'components/Elements';
import { install, uninstall, urlParser } from 'utils/marketplace';
export default class Added extends PureComponent {
constructor() {
super();
this.state = {
installed: JSON.parse(localStorage.getItem('installed')),
item: {},
button: '',
showFailed: false,
failedReason: '',
};
this.buttons = {
uninstall: (
<button className="removeFromMue" onClick={() => this.uninstall()}>
{variables.getMessage('modals.main.marketplace.product.buttons.remove')}
</button>
),
};
}
installAddon(input) {
let failedReason = '';
if (!input.name) {
failedReason = variables.getMessage('modals.main.addons.sideload.errors.no_name');
} else if (!input.author) {
failedReason = variables.getMessage('modals.main.addons.sideload.errors.no_author');
} else if (!input.type) {
failedReason = variables.getMessage('modals.main.addons.sideload.errors.no_type');
} else if (!input.version) {
failedReason = variables.getMessage('modals.main.addons.sideload.errors.no_version');
} else if (
input.type === 'photos' &&
(!input.photos ||
!input.photos.length ||
!input.photos[0].url ||
!input.photos[0].url.default ||
!input.photos[0].photographer ||
!input.photos[0].location)
) {
failedReason = variables.getMessage('modals.main.addons.sideload.errors.invalid_photos');
} else if (
input.type === 'quotes' &&
(!input.quotes || !input.quotes.length || !input.quotes[0].quote || !input.quotes[0].author)
) {
failedReason = variables.getMessage('modals.main.addons.sideload.errors.invalid_quotes');
}
if (failedReason !== '' && this.state.showFailed === false) {
return this.setState({
failedReason,
showFailed: true,
});
}
install(input.type, input);
toast(variables.getMessage('toasts.installed'));
variables.stats.postEvent('marketplace', 'Sideload');
}
getSideloadButton() {
return (
<Button
type="settings"
onClick={() => document.getElementById('file-input').click()}
ref={this.customDnd}
icon={<MdSendTimeExtension />}
label={variables.getMessage('modals.main.addons.sideload.title')}
/>
);
}
toggle(type, data) {
if (type === 'item') {
const installed = JSON.parse(localStorage.getItem('installed'));
const info = {
data: installed.find((i) => i.name === data.name),
};
this.setState({
item: {
type: info.data.type,
display_name: info.data.name,
author: info.data.author,
description: urlParser(info.data.description.replace(/\n/g, '<br>')),
//updated: info.updated,
version: info.data.version,
icon: info.data.screenshot_url,
data: info.data,
},
button: this.buttons.uninstall,
});
variables.stats.postEvent('marketplace', 'Item viewed');
} else {
this.setState({
item: {},
});
}
}
uninstall() {
uninstall(this.state.item.type, this.state.item.display_name);
toast(variables.getMessage('toasts.uninstalled'));
this.setState({
button: '',
installed: JSON.parse(localStorage.getItem('installed')),
});
variables.stats.postEvent('marketplace', 'Uninstall');
}
sortAddons(value, sendEvent) {
let installed = JSON.parse(localStorage.getItem('installed'));
switch (value) {
case 'newest':
installed.reverse();
break;
case 'oldest':
break;
case 'a-z':
installed.sort();
break;
case 'z-a':
installed.sort();
installed.reverse();
break;
default:
break;
}
this.setState({
installed,
});
if (sendEvent) {
variables.stats.postEvent('marketplace', 'Sort');
}
}
updateCheck() {
let updates = 0;
this.state.installed.forEach(async (item) => {
const data = await (
await fetch(variables.constants.API_URL + 'marketplace//item/' + item.name)
).json();
if (data.version !== item.version) {
updates++;
}
});
if (updates > 0) {
toast(
variables.getMessage('modals.main.addons.updates_available', {
amount: updates,
}),
);
} else {
toast(variables.getMessage('modals.main.addons.no_updates'));
}
}
componentDidMount() {
this.sortAddons(localStorage.getItem('sortAddons'), false);
}
render() {
const sideLoadBackendElements = () => (
<>
<Modal
closeTimeoutMS={100}
onRequestClose={() => this.setState({ showFailed: false })}
isOpen={this.state.showFailed}
className="Modal resetmodal mainModal resetmodal"
overlayClassName="Overlay resetoverlay"
ariaHideApp={false}
>
<SideloadFailedModal
modalClose={() => this.setState({ showFailed: false })}
reason={this.state.failedReason}
/>
</Modal>
<FileUpload
id="file-input"
type="settings"
accept="application/json"
loadFunction={(e) => this.installAddon(JSON.parse(e))}
/>
</>
);
if (this.state.installed.length === 0) {
return (
<>
<Header title={variables.getMessage('modals.main.navbar.addons')} report={false}>
<CustomActions>{this.getSideloadButton()}</CustomActions>
</Header>
<div className="emptyItems">
<div className="emptyNewMessage">
<MdOutlineExtensionOff />
<span className="title">
{variables.getMessage('modals.main.addons.empty.title')}
</span>
<span className="subtitle">
{variables.getMessage('modals.main.addons.empty.description')}
</span>
</div>
</div>
</>
);
}
if (this.state.item.display_name) {
return (
<Item
data={this.state.item}
button={this.state.button}
toggleFunction={() => this.toggle()}
/>
);
}
return (
<>
<div className="modalHeader">
<span className="mainTitle">{variables.getMessage('modals.main.addons.added')}</span>
<div className="filter">
{sideLoadBackendElements()}
<div className="buttonSection">
<Button
type="settings"
onClick={() => this.updateCheck()}
icon={<MdUpdate />}
label={variables.getMessage('modals.main.addons.check_updates')}
/>
<Button
type="settings"
onClick={() => document.getElementById('file-input').click()}
icon={<MdSendTimeExtension />}
label={variables.getMessage('modals.main.addons.sideload.title')}
/>
</div>
</div>
</div>
<Dropdown
label={variables.getMessage('modals.main.addons.sort.title')}
name="sortAddons"
onChange={(value) => this.sortAddons(value)}
items={[
{
value: 'newest',
text: variables.getMessage('modals.main.addons.sort.newest'),
},
{
value: 'oldest',
text: variables.getMessage('modals.main.addons.sort.oldest'),
},
{
value: 'a-z',
text: variables.getMessage('modals.main.addons.sort.a_z'),
},
{
value: 'z-a',
text: variables.getMessage('modals.main.addons.sort.z_a'),
},
]}
/>
<Items
items={this.state.installed}
filter=""
toggleFunction={(input) => this.toggle('item', input)}
/>
</>
);
}
}

View File

@@ -1,453 +0,0 @@
import variables from 'config/variables';
import { PureComponent } from 'react';
import { toast } from 'react-toastify';
import {
MdWifiOff,
MdLocalMall,
MdClose,
MdSearch,
MdOutlineArrowForward,
MdLibraryAdd,
} from 'react-icons/md';
import Item from '../components/Items/Item';
import Items from '../components/Items/Items';
import Dropdown from '../../../../../../components/Form/Settings/Dropdown/Dropdown';
import { Header } from 'components/Layout/Settings';
import { Button } from 'components/Elements';
import { install, urlParser, uninstall } from 'utils/marketplace';
class Marketplace extends PureComponent {
constructor() {
super();
this.state = {
items: [],
button: '',
done: false,
item: {},
collection: false,
filter: '',
};
this.buttons = {
uninstall: (
<Button
type="settings"
onClick={() => this.manage('uninstall')}
icon={<MdClose />}
label={variables.getMessage('modals.main.marketplace.product.buttons.remove')}
/>
),
install: (
<Button
type="settings"
onClick={() => this.manage('install')}
icon={<MdLibraryAdd />}
label={variables.getMessage('modals.main.marketplace.product.buttons.addtomue')}
/>
),
};
this.controller = new AbortController();
}
async toggle(type, data) {
if (type === 'item') {
let info;
// get item info
try {
let type = this.props.type;
if (type === 'all' || type === 'collections') {
type = data.type;
}
info = await (
await fetch(`${variables.constants.API_URL}/marketplace/item/${type}/${data.name}`, {
signal: this.controller.signal,
})
).json();
} catch (e) {
if (this.controller.signal.aborted === false) {
return toast(variables.getMessage('toasts.error'));
}
}
if (this.controller.signal.aborted === true) {
return;
}
// check if already installed
let button = this.buttons.install;
let addonInstalled = false;
let addonInstalledVersion;
const installed = JSON.parse(localStorage.getItem('installed'));
if (installed.some((item) => item.name === info.data.name)) {
button = this.buttons.uninstall;
addonInstalled = true;
for (let i = 0; i < installed.length; i++) {
if (installed[i].name === info.data.name) {
addonInstalledVersion = installed[i].version;
break;
}
}
}
this.setState({
item: {
type: info.data.type,
display_name: info.data.name,
author: info.data.author,
description: urlParser(info.data.description.replace(/\n/g, '<br>')),
//updated: info.updated,
version: info.data.version,
icon: info.data.screenshot_url,
data: info.data,
addonInstalled,
addonInstalledVersion,
api_name: data.name,
},
button: button,
});
variables.stats.postEvent('marketplace-item', `${this.state.item.display_name} viewed`);
} else if (type === 'collection') {
this.setState({
done: false,
});
const collection = await (
await fetch(`${variables.constants.API_URL}/marketplace/collection/${data}`, {
signal: this.controller.signal,
})
).json();
this.setState({
items: collection.data.items,
collectionTitle: collection.data.display_name,
collectionDescription: collection.data.description,
collectionImg: collection.data.img,
collection: true,
done: true,
});
} else {
this.setState({
item: {},
});
}
}
async getItems() {
const dataURL =
this.props.type === 'collections'
? variables.constants.API_URL + '/marketplace/collections'
: variables.constants.API_URL + '/marketplace/items/' + this.props.type;
const { data } = await (
await fetch(dataURL, {
signal: this.controller.signal,
})
).json();
const collections = await (
await fetch(variables.constants.API_URL + '/marketplace/collections', {
signal: this.controller.signal,
})
).json();
if (this.controller.signal.aborted === true) {
return;
}
this.setState({
items: data,
oldItems: data,
collections: collections.data,
done: true,
});
this.sortMarketplace(localStorage.getItem('sortMarketplace'), false);
}
manage(type) {
if (type === 'install') {
install(this.state.item.type, this.state.item.data);
} else {
uninstall(this.state.item.type, this.state.item.display_name);
}
toast(variables.getMessage('toasts.' + type + 'ed'));
this.setState({
button: type === 'install' ? this.buttons.uninstall : this.buttons.install,
});
variables.stats.postEvent(
'marketplace-item',
`${this.state.item.display_name} ${type === 'install' ? 'installed' : 'uninstalled'}`,
);
variables.stats.postEvent('marketplace', type === 'install' ? 'Install' : 'Uninstall');
}
async installCollection() {
this.setState({ busy: true });
try {
const installed = JSON.parse(localStorage.getItem('installed'));
for (const item of this.state.items) {
if (installed.some((i) => i.name === item.display_name)) continue; // don't install if already installed
let { data } = await (
await fetch(`${variables.constants.API_URL}/marketplace/item/${item.type}/${item.name}`, {
signal: this.controller.signal,
})
).json();
install(data.type, data);
variables.stats.postEvent('marketplace-item', `${item.display_name} installed}`);
variables.stats.postEvent('marketplace', 'Install');
}
toast(variables.getMessage('toasts.installed'));
} catch (error) {
console.error(error);
toast(variables.getMessage('toasts.error'));
} finally {
this.setState({ busy: false });
}
}
sortMarketplace(value, sendEvent) {
let items = this.state.oldItems;
switch (value) {
case 'a-z':
items.sort();
// fix sort not working sometimes
if (this.state.sortType === 'z-a') {
items.reverse();
}
break;
case 'z-a':
items.sort();
items.reverse();
break;
default:
break;
}
this.setState({
items: items,
sortType: value,
});
if (sendEvent) {
variables.stats.postEvent('marketplace', 'Sort');
}
}
returnToMain() {
this.setState({
items: this.state.oldItems,
collection: false,
});
}
componentDidMount() {
if (navigator.onLine === false || localStorage.getItem('offlineMode') === 'true') {
return;
}
this.getItems();
}
componentWillUnmount() {
// stop making requests
this.controller.abort();
}
render() {
const errorMessage = (msg) => {
return (
<>
<div className="flexTopMarketplace">
<span className="mainTitle">
{variables.getMessage('modals.main.navbar.marketplace')}
</span>
</div>
<div className="emptyItems">
<div className="emptyMessage">{msg}</div>
</div>
</>
);
};
if (navigator.onLine === false || localStorage.getItem('offlineMode') === 'true') {
return errorMessage(
<>
<MdWifiOff />
<span className="title">
{variables.getMessage('modals.main.marketplace.offline.title')}
</span>
<span className="subtitle">
{variables.getMessage('modals.main.marketplace.offline.description')}
</span>
</>,
);
}
if (this.state.done === false) {
return errorMessage(
<>
<div className="loaderHolder">
<div id="loader"></div>
<span className="subtitle">{variables.getMessage('modals.main.loading')}</span>
</div>
</>,
);
}
if (!this.state.items || this.state.items?.length === 0) {
this.getItems();
return (
<>
{errorMessage(
<>
<MdLocalMall />
<span className="title">
{variables.getMessage('modals.main.addons.empty.title')}
</span>
<span className="subtitle">
{variables.getMessage('modals.main.marketplace.no_items')}
</span>
</>,
)}
</>
);
}
if (this.state.item.display_name) {
return (
<Item
data={this.state.item}
button={this.state.button}
toggleFunction={() => this.toggle()}
addonInstalled={this.state.item.addonInstalled}
addonInstalledVersion={this.state.item.addonInstalledVersion}
icon={this.state.item.screenshot_url}
/>
);
}
return (
<>
{this.state.collection === true ? (
<>
<Header
title={variables.getMessage('modals.main.navbar.marketplace')}
secondaryTitle={variables.getMessage('modals.main.marketplace.collection')}
report={false}
goBack={() => this.returnToMain()}
/>
<div
className="collectionPage"
style={{
backgroundImage: `linear-gradient(to bottom, transparent, black), url('${variables.constants.DDG_IMAGE_PROXY + this.state.collectionImg}')`,
}}
>
<div className="nice-tag">
{variables.getMessage('modals.main.marketplace.collection')}
</div>
<div className="content">
<span className="mainTitle">{this.state.collectionTitle}</span>
<span className="subtitle">{this.state.collectionDescription}</span>
</div>
<Button
type="collection"
onClick={() => this.installCollection()}
disabled={this.state.busy}
icon={<MdLibraryAdd />}
label={variables.getMessage('modals.main.marketplace.add_all')}
/>
</div>
</>
) : (
<>
<div className="flexTopMarketplace">
<span className="mainTitle">
{variables.getMessage('modals.main.navbar.marketplace')}
</span>
</div>
<div className="headerExtras marketplaceCondition">
{this.props.type !== 'collections' && (
<div>
<form className="marketplaceSearch">
<input
label={variables.getMessage('widgets.search')}
placeholder={variables.getMessage('widgets.search')}
name="filter"
id="filter"
value={this.state.filter}
onChange={(event) => this.setState({ filter: event.target.value })}
/>
<MdSearch />
</form>
</div>
)}
<Dropdown
label={variables.getMessage('modals.main.addons.sort.title')}
name="sortMarketplace"
onChange={(value) => this.sortMarketplace(value)}
items={[
{
value: 'a-z',
text: variables.getMessage('modals.main.addons.sort.a_z'),
},
{
value: 'z-a',
text: variables.getMessage('modals.main.addons.sort.z_a'),
},
]}
/>
</div>
</>
)}
{this.props.type === 'collections' && !this.state.collection ? (
this.state.items.map((item) => (
<>
{!item.news ? (
<div
className="collection"
style={
item.news
? { backgroundColor: item.background_colour }
: {
backgroundImage: `linear-gradient(to left, #000, transparent, #000), url('${variables.constants.DDG_IMAGE_PROXY + item.img}')`,
}
}
>
<div className="content">
<span className="title">{item.display_name}</span>
<span className="subtitle">{item.description}</span>
</div>
<Button
type="collection"
onClick={() => this.toggle('collection', item.name)}
icon={<MdOutlineArrowForward />}
label={variables.getMessage('modals.main.marketplace.explore_collection')}
iconPlacement="right"
/>
</div>
) : null}
</>
))
) : (
<Items
type={this.props.type}
items={this.state.items}
collection={
this.state.collections[Math.floor(Math.random() * this.state.collections.length)] ||
[]
}
onCollection={this.state.collection}
toggleFunction={(input) => this.toggle('item', input)}
collectionFunction={(input) => this.toggle('collection', input)}
filter={this.state.filter}
/>
)}
</>
);
}
}
export default Marketplace;

View File

@@ -1,37 +0,0 @@
/* eslint-disable no-unused-vars */
import variables from 'config/variables';
import { PureComponent } from 'react';
import { MdOutlineExtensionOff } from 'react-icons/md';
export default class Create extends PureComponent {
constructor() {
super();
this.state = {};
}
render() {
return (
<>
<div className="flexTopMarketplace">
<span className="mainTitle">
{variables.getMessage('modals.main.addons.create.title')}
</span>
</div>
<div className="emptyItems">
<div className="emptyNewMessage">
<MdOutlineExtensionOff />
<span className="title">
{variables.getMessage('modals.main.addons.create.moved_title')}
</span>
<span className="subtitle">
{variables.getMessage('modals.main.addons.create.moved_description')}
</span>
<div className="createButtons">
<button>Coming soon...</button>
</div>
</div>
</div>
</>
);
}
}

View File

@@ -3,7 +3,7 @@ import { PureComponent, createRef } from 'react';
import { MdOutlineWifiOff } from 'react-icons/md';
import Modal from 'react-modal';
import Lightbox from '../../marketplace/components/Elements/Lightbox/Lightbox';
import Lightbox from '../../../../../marketplace/components/Elements/Lightbox/Lightbox';
export default class Changelog extends PureComponent {
constructor() {

View File

@@ -2,8 +2,8 @@ import variables from 'config/variables';
import { memo } from 'react';
import Tabs from './backend/Tabs';
import Added from '../marketplace/views/Added';
import Create from '../marketplace/views/Create';
import Added from '../../../../marketplace/views/Added';
import Create from '../../../../marketplace/views/Create';
function Addons(props) {
return (

View File

@@ -2,7 +2,7 @@ import variables from 'config/variables';
import { memo } from 'react';
import Tabs from './backend/Tabs';
import MarketplaceTab from '../marketplace/views/Browse';
import MarketplaceTab from '../../../../marketplace/views/Browse';
function Marketplace(props) {
return (

View File

@@ -1,40 +0,0 @@
import variables from 'config/variables';
import { MdOutlineOpenInNew } from 'react-icons/md';
import languages from '@/i18n/languages.json';
import { Radio } from 'components/Form/Settings';
import { Header, Content } from '../components/Layout';
function ChooseLanguage() {
return (
<Content>
<Header
title={variables.getMessage('modals.welcome.sections.language.title')}
subtitle={variables.getMessage('modals.welcome.sections.language.description')}
/>
<a
href={variables.constants.TRANSLATIONS_URL}
className="link"
target="_blank"
rel="noopener noreferrer"
>
GitHub
<MdOutlineOpenInNew />
</a>
<a
href={variables.constants.WEBLATE_URL}
className="link"
target="_blank"
rel="noopener noreferrer"
>
Weblate
<MdOutlineOpenInNew />
</a>
<div className="languageSettings">
<Radio name="language" options={languages} category="welcomeLanguage" />
</div>
</Content>
);
}
export { ChooseLanguage as default, ChooseLanguage };

View File

@@ -1,45 +0,0 @@
import variables from 'config/variables';
import languages from '@/i18n/languages.json';
import { Header, Content } from '../components/Layout';
function Final(props) {
return (
<Content>
<Header
title={variables.getMessage('modals.welcome.sections.final.title')}
subtitle={variables.getMessage('modals.welcome.sections.final.description')}
/>
<span className="title">{variables.getMessage('modals.welcome.sections.final.changes')}</span>
<span className="subtitle">
{variables.getMessage('modals.welcome.sections.final.changes_description')}
</span>
<div className="themesToggleArea themesToggleAreaWelcome">
<div className="toggle" onClick={() => props.switchTab(1)}>
<span>
{variables.getMessage('modals.main.settings.sections.language.title')}:{' '}
{languages.find((i) => i.value === localStorage.getItem('language')).name}
</span>
</div>
<div className="toggle" onClick={() => props.switchTab(3)}>
<span>
{variables.getMessage('modals.main.settings.sections.appearance.theme.title')}:{' '}
{variables.getMessage(
'modals.main.settings.sections.appearance.theme.' + localStorage.getItem('theme'),
)}
</span>
</div>
{/*}
{this.state.importedSettings.length !== 0 && (
<div className="toggle" onClick={() => this.props.switchTab(2)}>
{variables.getMessage('modals.main.settings.sections.final.imported', {
amount: this.state.importedSettings.length,
})}{' '}
{this.state.importedSettings.length}
</div>
)}*/}
</div>
</Content>
);
}
export { Final as default, Final };

View File

@@ -1,69 +0,0 @@
import variables from 'config/variables';
import { useState } from 'react';
import { FileUpload } from 'components/Form/Settings';
import { MdCloudUpload } from 'react-icons/md';
import { importSettings as importSettingsFunction } from 'utils/settings';
import { Header, Content } from '../components/Layout';
import default_settings from 'utils/data/default_settings.json';
function ImportSettings(props) {
const [importedSettings, setImportedSettings] = useState([]);
const importSettings = (e) => {
importSettingsFunction(e);
const settings = [];
const data = JSON.parse(e.target.result);
Object.keys(data).forEach((setting) => {
// language and theme already shown, the others are only used internally
if (
setting === 'language' ||
setting === 'theme' ||
setting === 'firstRun' ||
setting === 'showWelcome' ||
setting === 'showReminder'
) {
return;
}
const defaultSetting = default_settings.find((i) => i.name === setting);
if (defaultSetting !== undefined) {
if (data[setting] === String(defaultSetting.value)) {
return;
}
}
settings.push({
name: setting,
value: data[setting],
});
});
setImportedSettings(settings);
props.switchTab(5);
};
return (
<Content>
<Header
title={variables.getMessage('modals.welcome.sections.settings.title')}
subtitle={variables.getMessage('modals.welcome.sections.settings.description')}
/>
<button className="upload" onClick={() => document.getElementById('file-input').click()}>
<MdCloudUpload />
<span>{variables.getMessage('modals.main.settings.buttons.import')}</span>
</button>
<FileUpload
id="file-input"
accept="application/json"
type="settings"
loadFunction={(e) => importSettings(e)}
/>
<span className="title">{variables.getMessage('modals.welcome.tip')}</span>
<span className="subtitle">
{variables.getMessage('modals.welcome.sections.settings.tip')}
</span>
</Content>
);
}
export { ImportSettings as default, ImportSettings };

View File

@@ -1,91 +0,0 @@
import variables from 'config/variables';
import { useState, useEffect, useCallback } from 'react';
import { Header, Content } from '../components/Layout';
import { MdOutlineWavingHand, MdOpenInNew } from 'react-icons/md';
import { FaDiscord, FaGithub } from 'react-icons/fa';
const DISCORD_LINK = 'https://discord.gg/' + variables.constants.DISCORD_SERVER;
const GITHUB_LINK =
'https://github.com/' + variables.constants.ORG_NAME + '/' + variables.constants.REPO_NAME;
function WelcomeNotice({ config }) {
const { icon: Icon, title, subtitle, link } = config;
return (
<div className="welcomeNotice">
<div className="icon">
<Icon />
</div>
<div className="text">
<span className="title">{title}</span>
<span className="subtitle">{subtitle}</span>
</div>
{link && (
<a href={link} target="_blank" rel="noopener noreferrer">
<MdOpenInNew />
{variables.getMessage('modals.welcome.sections.intro.notices.github_open')}
</a>
)}
</div>
);
}
function Intro() {
const [welcomeImage, setWelcomeImage] = useState(0);
const updateWelcomeImage = useCallback(() => {
setWelcomeImage((prevWelcomeImage) => (prevWelcomeImage < 3 ? prevWelcomeImage + 1 : 0));
}, []);
const ShareYourMue = (
<div className="examples">
<img
src={`/src/assets/welcome-images/example${welcomeImage + 1}.webp`}
alt="Example Mue setup"
draggable={false}
/>
<span className="shareYourMue">#shareyourmue</span>
</div>
);
useEffect(() => {
const timer = setInterval(updateWelcomeImage, 3000);
return () => clearInterval(timer);
}, [updateWelcomeImage]);
return (
<Content>
<Header title={variables.getMessage('modals.welcome.sections.intro.title')} />
{ShareYourMue}
<WelcomeNotice
config={{
icon: MdOutlineWavingHand,
title: variables.getMessage('modals.welcome.sections.intro.title'),
subtitle: variables.getMessage('modals.welcome.sections.intro.description'),
}}
/>
<WelcomeNotice
config={{
icon: FaDiscord,
title: variables.getMessage('modals.welcome.sections.intro.notices.discord_title'),
subtitle: variables.getMessage(
'modals.welcome.sections.intro.notices.discord_description',
),
link: DISCORD_LINK,
}}
/>
<WelcomeNotice
config={{
icon: FaGithub,
title: variables.getMessage('modals.welcome.sections.intro.notices.github_title'),
subtitle: variables.getMessage(
'modals.welcome.sections.intro.notices.github_description',
),
link: GITHUB_LINK,
}}
/>
</Content>
);
}
export { Intro as default, Intro };

View File

@@ -1,78 +0,0 @@
import variables from 'config/variables';
import { MdOutlineOpenInNew } from 'react-icons/md';
import { Checkbox } from 'components/Form/Settings';
import { Header, Content } from '../components/Layout';
function OfflineMode() {
return (
<>
<Checkbox
name="offlineMode"
text={variables.getMessage('modals.main.settings.sections.advanced.offline_mode')}
element=".other"
/>
<span className="subtitle">
{variables.getMessage('modals.welcome.sections.privacy.offline_mode_description')}
</span>
</>
);
}
function DuckDuckGoProxy() {
return (
<>
<Checkbox
name="ddgProxy"
text={`${variables.getMessage('modals.main.settings.sections.background.ddg_image_proxy')} (${variables.getMessage('modals.main.settings.sections.background.title')})`}
/>
<span className="subtitle">
{variables.getMessage('modals.welcome.sections.privacy.ddg_proxy_description')}
</span>
</>
);
}
function Links() {
return (
<>
<span className="title">
{variables.getMessage('modals.welcome.sections.privacy.links.title')}
</span>
<a
className="link"
href={variables.constants.PRIVACY_URL}
target="_blank"
rel="noopener noreferrer"
>
{variables.getMessage('modals.welcome.sections.privacy.links.privacy_policy')}
<MdOutlineOpenInNew />
</a>
<a
className="link"
href={`https://github.com/${variables.constants.ORG_NAME}`}
target="_blank"
rel="noopener noreferrer"
>
{variables.getMessage('modals.welcome.sections.privacy.links.source_code')}
<MdOutlineOpenInNew />
</a>
</>
);
}
function PrivacyOptions() {
return (
<Content>
<Header
title={variables.getMessage('modals.welcome.sections.privacy.title')}
subtitle={variables.getMessage('modals.welcome.sections.privacy.description')}
/>
<OfflineMode />
<DuckDuckGoProxy />
<Links />
</Content>
);
}
export { PrivacyOptions as default, PrivacyOptions };

View File

@@ -1,53 +0,0 @@
import variables from 'config/variables';
import { MdArchive, MdOutlineWhatshot } from 'react-icons/md';
import { useState } from 'react';
import { Header, Content } from '../components/Layout';
const STYLES = {
NEW: 'new',
LEGACY: 'legacy',
};
const StyleSelection = () => {
const widgetStyle = localStorage.getItem('widgetStyle') || STYLES.NEW;
const [style, setStyle] = useState(widgetStyle);
const changeStyle = (type) => {
setStyle(type);
localStorage.setItem('widgetStyle', type);
};
const styleMapping = {
[STYLES.LEGACY]: {
className: style === STYLES.LEGACY ? 'toggle legacyStyle active' : 'toggle legacyStyle',
icon: <MdArchive />,
text: variables.getMessage('modals.welcome.sections.style.legacy'),
},
[STYLES.NEW]: {
className: style === STYLES.NEW ? 'toggle newStyle active' : 'toggle newStyle',
icon: <MdOutlineWhatshot />,
text: variables.getMessage('modals.welcome.sections.style.modern'),
},
};
return (
<Content>
<Header
title={variables.getMessage('modals.welcome.sections.style.title')}
subtitle={variables.getMessage('modals.welcome.sections.style.description')}
/>
<div className="themesToggleArea">
<div className="options">
{Object.entries(styleMapping).map(([type, { className, icon, text }]) => (
<div className={className} onClick={() => changeStyle(type)} key={type}>
{icon}
<span>{text}</span>
</div>
))}
</div>
</div>
</Content>
);
};
export { StyleSelection as default, StyleSelection };

View File

@@ -1,70 +0,0 @@
import variables from 'config/variables';
import { useState } from 'react';
import { MdAutoAwesome, MdLightMode, MdDarkMode } from 'react-icons/md';
import { loadSettings } from 'utils/settings';
import { Header, Content } from '../components/Layout';
const THEMES = {
AUTO: 'auto',
LIGHT: 'light',
DARK: 'dark',
};
function ThemeSelection() {
const currentTheme = localStorage.getItem('theme') || THEMES.AUTO;
const [theme, setTheme] = useState(currentTheme);
const changeTheme = (type) => {
setTheme(type);
localStorage.setItem('theme', type);
loadSettings(true);
};
const themeMapping = {
[THEMES.AUTO]: {
className: theme === THEMES.AUTO ? 'toggle auto active' : 'toggle auto',
icon: <MdAutoAwesome />,
text: variables.getMessage('modals.main.settings.sections.appearance.theme.auto'),
},
[THEMES.LIGHT]: {
className: theme === THEMES.LIGHT ? 'toggle lightTheme active' : 'toggle lightTheme',
icon: <MdLightMode />,
text: variables.getMessage('modals.main.settings.sections.appearance.theme.light'),
},
[THEMES.DARK]: {
className: theme === THEMES.DARK ? 'toggle darkTheme active' : 'toggle darkTheme',
icon: <MdDarkMode />,
text: variables.getMessage('modals.main.settings.sections.appearance.theme.dark'),
},
};
return (
<Content>
<Header
title={variables.getMessage('modals.welcome.sections.theme.title')}
subtitle={variables.getMessage('modals.welcome.sections.theme.description')}
/>
<div className="themesToggleArea">
<div className={themeMapping[THEMES.AUTO].className} onClick={() => changeTheme(THEMES.AUTO)}>
{themeMapping[THEMES.AUTO].icon}
<span>{themeMapping[THEMES.AUTO].text}</span>
</div>
<div className="options">
{Object.entries(themeMapping)
.filter(([type]) => type !== THEMES.AUTO)
.map(([type, { className, icon, text }]) => (
<div className={className} onClick={() => changeTheme(type)} key={type}>
{icon}
<span>{text}</span>
</div>
))}
</div>
</div>
<span className="title">{variables.getMessage('modals.welcome.tip')}</span>
<span className="subtitle">{variables.getMessage('modals.welcome.sections.theme.tip')}</span>
</Content>
);
}
export { ThemeSelection as default, ThemeSelection };

View File

@@ -1,7 +0,0 @@
export * from './Intro';
export * from './ChooseLanguage';
export * from './ImportSettings';
export * from './ThemeSelection';
export * from './StyleSelection';
export * from './PrivacyOptions';
export * from './Final';

View File

@@ -1,159 +0,0 @@
// Importing necessary libraries and components
import { useState, useEffect } from 'react';
import variables from 'config/variables';
import { MdArrowBackIosNew, MdArrowForwardIos, MdOutlinePreview } from 'react-icons/md';
import EventBus from 'utils/eventbus';
import { ProgressBar, AsideImage } from './components/Elements';
import { Button } from 'components/Elements';
import { Wrapper, Panel } from './components/Layout';
import './welcome.scss';
import {
Intro,
ChooseLanguage,
ImportSettings,
ThemeSelection,
StyleSelection,
PrivacyOptions,
Final,
} from './Sections';
// WelcomeModal component
function WelcomeModal({ modalClose, modalSkip }) {
// State variables
const [currentTab, setCurrentTab] = useState(0);
const [buttonText, setButtonText] = useState(variables.getMessage('modals.welcome.buttons.next'));
const finalTab = 6;
// useEffect hook to handle tab changes and event bus listener
useEffect(() => {
// Get the current welcome tab from local storage
const welcomeTab = localStorage.getItem('welcomeTab');
if (welcomeTab) {
const tab = Number(welcomeTab);
setCurrentTab(tab);
setButtonText(
tab !== finalTab + 1
? variables.getMessage('modals.welcome.buttons.next')
: variables.getMessage('modals.welcome.buttons.finish'),
);
}
// Listener for the 'refresh' event
const refreshListener = (data) => {
if (data === 'welcomeLanguage') {
localStorage.setItem('welcomeTab', currentTab);
localStorage.setItem('bgtransition', false);
window.location.reload();
}
};
// Subscribe to the 'refresh' event
EventBus.on('refresh', refreshListener);
// Cleanup function to unsubscribe from the 'refresh' event
return () => {
EventBus.off('refresh', refreshListener);
};
}, [currentTab, finalTab]);
// Function to update the current tab and button text
const updateTabAndButtonText = (newTab) => {
setCurrentTab(newTab);
setButtonText(
newTab !== finalTab
? variables.getMessage('modals.welcome.buttons.next')
: variables.getMessage('modals.welcome.buttons.finish'),
);
localStorage.setItem('bgtransition', true);
localStorage.removeItem('welcomeTab');
};
// Functions to navigate to the previous and next tabs
const prevTab = () => {
updateTabAndButtonText(currentTab - 1);
};
const nextTab = () => {
if (buttonText === variables.getMessage('modals.welcome.buttons.finish')) {
modalClose();
return;
}
updateTabAndButtonText(currentTab + 1);
};
// Function to switch to a specific tab
const switchToTab = (tab) => {
updateTabAndButtonText(tab);
};
// Navigation component
const Navigation = () => {
return (
<div className="welcomeButtons">
{currentTab !== 0 ? (
<Button
type="settings"
onClick={() => prevTab()}
icon={<MdArrowBackIosNew />}
label={variables.getMessage('modals.welcome.buttons.previous')}
/>
) : (
<Button
type="settings"
onClick={() => modalSkip()}
icon={<MdOutlinePreview />}
label={variables.getMessage('modals.welcome.buttons.preview')}
/>
)}
<Button
type="settings"
onClick={() => nextTab()}
icon={<MdArrowForwardIos />}
label={buttonText}
iconPlacement={'right'}
/>
</div>
);
};
// Mapping of tab numbers to components
const tabComponents = {
0: <Intro />,
1: <ChooseLanguage />,
2: <ImportSettings switchTab={switchToTab} />,
3: <ThemeSelection />,
4: <StyleSelection />,
5: <PrivacyOptions />,
6: <Final currentTab={currentTab} switchTab={switchToTab} />,
};
// Current tab component
let CurrentTab = tabComponents[currentTab] || <Intro />;
// Render the WelcomeModal component
return (
<Wrapper>
<Panel type="aside">
<AsideImage currentTab={currentTab} />
<ProgressBar numberOfTabs={finalTab + 1} currentTab={currentTab} switchTab={switchToTab} />
</Panel>
<Panel type="content">
{CurrentTab}
<Navigation
currentTab={currentTab}
changeTab={switchToTab}
buttonText={buttonText}
modalSkip={modalSkip}
/>
</Panel>
</Wrapper>
);
}
// Export the WelcomeModal component
export default WelcomeModal;

View File

@@ -1,432 +0,0 @@
import variables from 'config/variables';
import { PureComponent } from 'react';
import {
MdCloudUpload,
MdAutoAwesome,
MdLightMode,
MdDarkMode,
MdOutlineWavingHand,
MdOpenInNew,
MdOutlineWhatshot,
MdArchive,
MdOutlineOpenInNew,
} from 'react-icons/md';
import { FaDiscord, FaGithub } from 'react-icons/fa';
import { Radio, Checkbox, FileUpload } from 'components/Form/Settings';
import { loadSettings, importSettings } from 'utils/settings';
import default_settings from 'utils/data/default_settings.json';
import languages from '@/i18n/languages.json';
class WelcomeSections extends PureComponent {
constructor() {
super();
this.state = {
// themes
autoClass: 'toggle auto active',
lightClass: 'toggle lightTheme',
darkClass: 'toggle darkTheme',
// styles
newStyle: 'toggle newStyle active',
legacyStyle: 'toggle legacyStyle',
// welcome
welcomeImage: 0,
// final
importedSettings: [],
};
this.changeWelcomeImg = this.changeWelcomeImg.bind(this);
this.welcomeImages = 3;
}
changeTheme(type) {
this.setState({
autoClass: type === 'auto' ? 'toggle auto active' : 'toggle auto',
lightClass: type === 'light' ? 'toggle lightTheme active' : 'toggle lightTheme',
darkClass: type === 'dark' ? 'toggle darkTheme active' : 'toggle darkTheme',
});
localStorage.setItem('theme', type);
loadSettings(true);
}
changeStyle(type) {
this.setState({
newStyle: type === 'new' ? 'toggle newStyle active' : 'toggle newStyle',
legacyStyle: type === 'legacy' ? 'toggle legacyStyle active' : 'toggle legacyStyle',
});
localStorage.setItem('widgetStyle', type);
}
getSetting(name) {
const value = localStorage.getItem(name).replace('false', 'Off').replace('true', 'On');
return value.charAt(0).toUpperCase() + value.slice(1);
}
importSettings(e) {
importSettings(e);
const settings = [];
const data = JSON.parse(e.target.result);
Object.keys(data).forEach((setting) => {
// language and theme already shown, the others are only used internally
if (
setting === 'language' ||
setting === 'theme' ||
setting === 'firstRun' ||
setting === 'showWelcome' ||
setting === 'showReminder'
) {
return;
}
const defaultSetting = default_settings.find((i) => i.name === setting);
if (defaultSetting !== undefined) {
if (data[setting] === String(defaultSetting.value)) {
return;
}
}
settings.push({
name: setting,
value: data[setting],
});
});
this.setState({
importedSettings: settings,
});
this.props.switchTab(5);
}
changeWelcomeImg() {
let welcomeImage = this.state.welcomeImage;
this.setState({
welcomeImage: welcomeImage < 3 ? ++welcomeImage : 0,
});
this.timeout = setTimeout(this.changeWelcomeImg, 3000);
}
componentDidMount() {
this.timeout = setTimeout(this.changeWelcomeImg, 3000);
}
componentWillUnmount() {
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = null;
}
}
// cancel welcome image timer if not on welcome tab
componentDidUpdate() {
if (this.props.currentTab !== 0) {
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = null;
}
} else if (!this.timeout) {
this.timeout = setTimeout(this.changeWelcomeImg, 3000);
}
}
render() {
const intro = (
<>
<span className="mainTitle">
{variables.getMessage('modals.welcome.sections.intro.title')}
</span>
<div className="examples">
<img
src={`/src/assets/welcome-images/example${this.state.welcomeImage + 1}.webp`}
alt="Example Mue setup"
draggable={false}
/>
<span className="shareYourMue">#shareyourmue</span>
</div>
<div className="welcomeNotice">
<div className="icon">
<MdOutlineWavingHand />
</div>
<div className="text">
<span className="title">
{variables.getMessage('modals.welcome.sections.intro.title')}
</span>
<span className="subtitle">
{variables.getMessage('modals.welcome.sections.intro.description')}
</span>
</div>
</div>
<div className="welcomeNotice">
<div className="icon">
<FaDiscord />
</div>
<div className="text">
<span className="title">
{variables.getMessage('modals.welcome.sections.intro.notices.discord_title')}
</span>
<span className="subtitle">
{variables.getMessage('modals.welcome.sections.intro.notices.discord_description')}
</span>
</div>
<a href="https://discord.gg/zv8C9F8" target="_blank" rel="noopener noreferrer">
<MdOpenInNew />{' '}
{variables.getMessage('modals.welcome.sections.intro.notices.discord_join')}
</a>
</div>
<div className="welcomeNotice">
<div className="icon">
<FaGithub />
</div>
<div className="text">
<span className="title">
{variables.getMessage('modals.welcome.sections.intro.notices.github_title')}
</span>
<span className="subtitle">
{variables.getMessage('modals.welcome.sections.intro.notices.github_description')}
</span>
</div>
<a href="https://github.com/mue/mue" target="_blank" rel="noopener noreferrer">
<MdOpenInNew />
{variables.getMessage('modals.welcome.sections.intro.notices.github_open')}
</a>
</div>
</>
);
const chooseLanguage = (
<>
<span className="mainTitle">
{variables.getMessage('modals.welcome.sections.language.title')}
</span>
<span className="subtitle">
{variables.getMessage('modals.welcome.sections.language.description')}{' '}
</span>
<a
href={variables.constants.TRANSLATIONS_URL}
className="link"
target="_blank"
rel="noopener noreferrer"
>
GitHub
<MdOutlineOpenInNew />
</a>
<a
href={variables.constants.WEBLATE_URL}
className="link"
target="_blank"
rel="noopener noreferrer"
>
Weblate
<MdOutlineOpenInNew />
</a>
<div className="languageSettings">
<Radio name="language" options={languages} category="welcomeLanguage" />
</div>
</>
);
const theme = (
<>
<span className="mainTitle">
{variables.getMessage('modals.welcome.sections.theme.title')}
</span>
<span className="subtitle">
{variables.getMessage('modals.welcome.sections.theme.description')}
</span>
<div className="themesToggleArea">
<div className={this.state.autoClass} onClick={() => this.changeTheme('auto')}>
<MdAutoAwesome />
<span>
{variables.getMessage('modals.main.settings.sections.appearance.theme.auto')}
</span>
</div>
<div className="options">
<div className={this.state.lightClass} onClick={() => this.changeTheme('light')}>
<MdLightMode />
<span>
{variables.getMessage('modals.main.settings.sections.appearance.theme.light')}
</span>
</div>
<div className={this.state.darkClass} onClick={() => this.changeTheme('dark')}>
<MdDarkMode />
<span>
{variables.getMessage('modals.main.settings.sections.appearance.theme.dark')}
</span>
</div>
</div>
</div>
<span className="title">{variables.getMessage('modals.welcome.tip')}</span>
<span className="subtitle">
{variables.getMessage('modals.welcome.sections.theme.tip')}
</span>
</>
);
const style = (
<>
<span className="mainTitle">
{variables.getMessage('modals.welcome.sections.style.title')}
</span>
<span className="subtitle">
{variables.getMessage('modals.welcome.sections.style.description')}
</span>
<div className="themesToggleArea">
<div className="options">
<div className={this.state.legacyStyle} onClick={() => this.changeStyle('legacy')}>
<MdArchive />
<span>{variables.getMessage('modals.welcome.sections.style.legacy')}</span>
</div>
<div className={this.state.newStyle} onClick={() => this.changeStyle('new')}>
<MdOutlineWhatshot />
<span>{variables.getMessage('modals.welcome.sections.style.modern')}</span>
</div>
</div>
</div>
</>
);
const settings = (
<>
<span className="mainTitle">
{variables.getMessage('modals.welcome.sections.settings.title')}
</span>
<span className="subtitle">
{variables.getMessage('modals.welcome.sections.settings.description')}
</span>
<button className="upload" onClick={() => document.getElementById('file-input').click()}>
<MdCloudUpload />
<span>{variables.getMessage('modals.main.settings.buttons.import')}</span>
</button>
<FileUpload
id="file-input"
accept="application/json"
type="settings"
loadFunction={(e) => this.importSettings(e)}
/>
<span className="title">{variables.getMessage('modals.welcome.tip')}</span>
<span className="subtitle">
{variables.getMessage('modals.welcome.sections.settings.tip')}
</span>
</>
);
const privacy = (
<>
<span className="mainTitle">
{variables.getMessage('modals.welcome.sections.privacy.title')}
</span>
<span className="subtitle">
{variables.getMessage('modals.welcome.sections.privacy.description')}
</span>
<Checkbox
name="offlineMode"
text={variables.getMessage('modals.main.settings.sections.advanced.offline_mode')}
element=".other"
/>
<span className="subtitle">
{variables.getMessage('modals.welcome.sections.privacy.offline_mode_description')}
</span>
<Checkbox
name="ddgProxy"
text={
variables.getMessage('modals.main.settings.sections.background.ddg_image_proxy') +
' (' +
variables.getMessage('modals.main.settings.sections.background.title') +
')'
}
/>
<span className="subtitle">
{variables.getMessage('modals.welcome.sections.privacy.ddg_proxy_description')}
</span>
<span className="title">
{variables.getMessage('modals.welcome.sections.privacy.links.title')}
</span>
<a
className="link"
href={variables.constants.PRIVACY_URL}
target="_blank"
rel="noopener noreferrer"
>
{variables.getMessage('modals.welcome.sections.privacy.links.privacy_policy')}
<MdOutlineOpenInNew />
</a>
<a
className="link"
href={'https://github.com/' + variables.constants.ORG_NAME}
target="_blank"
rel="noopener noreferrer"
>
{variables.getMessage('modals.welcome.sections.privacy.links.source_code')}
<MdOutlineOpenInNew />
</a>
</>
);
const final = (
<>
<span className="mainTitle">
{variables.getMessage('modals.welcome.sections.final.title')}
</span>
<span className="subtitle">
{variables.getMessage('modals.welcome.sections.final.description')}
</span>
<span className="title">
{variables.getMessage('modals.welcome.sections.final.changes')}
</span>
<span className="subtitle">
{variables.getMessage('modals.welcome.sections.final.changes_description')}
</span>
<div className="themesToggleArea themesToggleAreaWelcome">
<div className="toggle" onClick={() => this.props.switchTab(1)}>
<span>
{variables.getMessage('modals.main.settings.sections.language.title')}:{' '}
{languages.find((i) => i.value === localStorage.getItem('language')).name}
</span>
</div>
<div className="toggle" onClick={() => this.props.switchTab(3)}>
<span>
{variables.getMessage('modals.main.settings.sections.appearance.theme.title')}:{' '}
{variables.getMessage(
'modals.main.settings.sections.appearance.theme.' + localStorage.getItem('theme'),
)}
</span>
</div>
{this.state.importedSettings.length !== 0 && (
<div className="toggle" onClick={() => this.props.switchTab(2)}>
{variables.getMessage('modals.main.settings.sections.final.imported', {
amount: this.state.importedSettings.length,
})}{' '}
{this.state.importedSettings.length}
</div>
)}
</div>
</>
);
switch (this.props.currentTab) {
case 1:
return chooseLanguage;
case 2:
return settings;
case 3:
return theme;
case 4:
return style;
case 5:
return privacy;
case 6:
return final;
// 0
default:
return intro;
}
}
}
export default WelcomeSections;

View File

@@ -1,31 +0,0 @@
const images = [
'/src/assets/icons/undraw_celebration.svg',
'/src/assets/icons/undraw_around_the_world_modified.svg',
'/src/assets/icons/undraw_add_files_modified.svg',
'/src/assets/icons/undraw_dark_mode.svg',
'/src/assets/icons/undraw_making_art.svg',
'/src/assets/icons/undraw_private_data_modified.svg',
'/src/assets/icons/undraw_upgrade_modified.svg',
];
function AsideImage({ currentTab }) {
const altTexts = [
'Celebration icon',
'Around the world icon',
'Add files icon',
'Dark mode icon',
'Making art icon',
'Private data icon',
'Upgrade icon',
];
return (
<img
className="showcaseimg"
alt={altTexts[currentTab]}
draggable={false}
src={images[currentTab]}
/>
);
}
export { AsideImage as default, AsideImage };

View File

@@ -1 +0,0 @@
export * from './AsideImage';

View File

@@ -1,31 +0,0 @@
import { memo } from 'react';
const Step = memo(({ isActive, index, onClick }) => {
const className = isActive ? 'step active' : 'step';
return (
<div className={className} onClick={onClick}>
<span>{index + 1}</span>
</div>
);
});
function ProgressBar({ numberOfTabs, currentTab, switchTab }) {
return (
<div className="progressbar">
{Array.from({ length: numberOfTabs }, (_, index) => (
<Step
key={index}
isActive={index === currentTab}
index={index}
onClick={() => switchTab(index)}
/>
))}
</div>
);
}
const MemoizedProgressBar = memo(ProgressBar);
export default MemoizedProgressBar;
export { MemoizedProgressBar as ProgressBar };

View File

@@ -1 +0,0 @@
export * from './ProgressBar';

View File

@@ -1,2 +0,0 @@
export * from './ProgressBar';
export * from './AsideImage';

View File

@@ -1,7 +0,0 @@
const Content = ({ children }) => {
return (
<div className="content">{children}</div>
)
}
export { Content as default, Content };

View File

@@ -1,12 +0,0 @@
function Header({ title, subtitle }) {
return (
<>
<span className="mainTitle">{title}</span>
<span className="subtitle">
{subtitle}
</span>
</>
);
}
export { Header as default, Header };

View File

@@ -1,7 +0,0 @@
function Layout() {
return (
<h1>Cheese</h1>
)
}
export { Layout as default, Layout };

View File

@@ -1,7 +0,0 @@
const Panel = ({ children, type }) => (
<section className={type}>
{children}
</section>
);
export { Panel as default, Panel };

View File

@@ -1,5 +0,0 @@
const Wrapper = ({ children }) => (
<div className="welcomeContent">{children}</div>
)
export { Wrapper as default, Wrapper };

View File

@@ -1,4 +0,0 @@
export * from './Wrapper';
export * from './Panel';
export * from './Header';
export * from './Content';

View File

@@ -1,2 +0,0 @@
export * from './Layout';
export * from './Elements';

View File

@@ -1,331 +0,0 @@
@import '../main/scss/index';
@import 'scss/variables';
.welcomemodal {
@include themed {
background-color: t($modal-background);
}
}
.welcomeContent {
@include themed {
background-color: t($modal-background);
}
.MuiFormControlLabel-root {
margin-right: 0;
}
.link {
display: flex;
flex-flow: row;
gap: 15px;
align-items: center;
}
@extend %tabText;
height: 80vh;
width: clamp(60vw, 1200px, 90vw);
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(1, 1fr);
grid-gap: 0;
section.aside {
display: flex;
align-items: center;
justify-content: center;
@include themed {
background-color: t($modal-sidebar);
}
}
section.content {
display: flex;
flex-flow: column;
justify-content: space-between;
overflow-y: auto;
.content {
display: flex;
flex-flow: column;
padding: 25px;
gap: 20px;
}
}
}
.progressbar {
position: fixed;
bottom: 50px;
text-align: center;
display: inline;
overflow: hidden;
white-space: nowrap;
.step {
display: inline-block;
border-bottom: 2px solid grey;
padding: 10px 20px;
margin: 5px;
transition: 0.2s ease-in-out;
cursor: pointer;
border-radius: 10px 10px 0 0;
&:hover {
background: #dd4038;
border-radius: 10px;
border-bottom: 2px solid #dd4038;
}
}
.active {
background: #d21a11;
border-bottom: 2px solid #d21a11;
border-radius: 10px;
}
}
.themesToggleArea {
display: grid;
grid-template-columns: 1fr;
grid-template-rows: auto auto;
div:nth-child(1) {
grid-column: span 1 / span 1 !important;
}
div:nth-child(2),
div:nth-child(3) {
grid-column: span 1 / span 1 !important;
}
@include themed {
.active {
background: t($modal-sidebarActive);
}
.toggle {
background: t($modal-sidebar);
text-align: center;
border-radius: 20px;
padding: 20px;
border: 3px solid t($modal-sidebarActive);
transition: 0.33s;
display: flex;
flex-direction: column;
align-items: center;
place-content: center center;
cursor: pointer;
&:hover {
background: t($modal-sidebarActive);
}
span {
font-size: 1rem;
}
svg {
font-size: 2.5em;
}
}
.auto {
svg {
font-size: 12px;
padding-right: 5px;
}
}
.options {
display: flex;
justify-content: space-between;
gap: 25px;
margin-top: 25px;
.lightTheme,
.darkTheme,
.legacyStyle,
.newStyle {
width: 50%;
padding: 50px;
span {
display: block;
}
}
}
}
}
.themesToggleArea .toggle {
margin-bottom: 10px;
}
.upload {
width: 100%;
height: 100%;
border-radius: 20px;
border: none;
outline: none;
padding: 50px;
display: flex;
flex-flow: column;
align-items: center;
transition: 0.3s;
@include themed {
background: t($modal-sidebar);
color: t($color);
cursor: pointer;
border: 3px solid t($modal-sidebarActive);
&:hover {
background: t($modal-sidebarActive);
}
}
svg {
font-size: 4em;
}
span {
font-size: 2em;
}
}
a.privacy {
text-decoration: none;
color: var(--modal-text);
font-size: 1rem;
&:hover {
color: #5352ed;
}
}
.examples {
display: flex;
flex-flow: column;
.shareYourMue {
width: -moz-fit-content;
width: fit-content;
}
img {
max-width: 60%;
border-radius: 10px 10px 10px 0;
}
}
.shareYourMue {
padding: 8px 20px;
border-radius: 0 0 10px 10px;
letter-spacing: 2px;
@include themed {
background-color: t($modal-sidebarActive);
}
}
.createButtons {
display: flex;
flex-flow: row;
justify-content: space-between;
margin-top: 15px;
button {
width: 150px;
height: 40px;
}
}
.showcaseimg {
width: 350px;
height: auto;
/* animation-name: float-in;
animation-duration: 1.2s;
animation-timing-function: ease-in; */
}
.welcomeContent {
.light {
.toggle.lightTheme {
background-color: rgb(219 219 219 / 72%);
}
}
.dark {
.toggle.darkTheme {
background-color: rgb(65 71 84 / 90%);
}
}
}
.welcomeNotice {
display: flex;
flex-flow: row;
gap: 25px;
padding: 25px;
align-items: center;
@include themed {
background-color: t($modal-sidebar);
border-radius: t($borderRadius);
}
.icon {
background: linear-gradient(238.7deg, #ff5c25 13.8%, #d21a11 49.49%, #ff456e 87.48%);
/* @include themed {
background-color: t($modal-sidebarActive);
} */
height: 50px !important;
width: 50px !important;
border-radius: 100%;
display: grid;
place-items: center;
text-align: center;
flex-shrink: 0;
color: #f18d91;
font-size: 24px;
}
.text {
display: flex;
flex-flow: column;
}
a {
text-decoration: none;
margin-left: auto;
padding: 0 20px;
@include modal-button(standard);
}
}
.toggle.active {
@include themed {
background-color: t($modal-sidebarActive) !important;
}
}
.welcomeButtons {
z-index: 999;
-webkit-backdrop-filter: blur(2px);
backdrop-filter: blur(2px);
position: sticky;
bottom: 0;
padding: 25px;
display: flex;
justify-content: flex-end;
gap: 20px;
button {
@include modal-button(standard);
}
}