mirror of
https://github.com/Viren070/guides.git
synced 2025-12-01 23:16:16 +01:00
feat(stremio): add window to modify debrid data for cost comparison table (#79)
* feat: initial attempt * feat: move new service data addition to existing service data container * feat: move currency selector back to top of table * feat: im a css god * feat: no im not * feat: final changes
This commit is contained in:
@@ -223,6 +223,8 @@ I recommend coming to your own conclusion by reading the information below, in a
|
||||
|
||||
**Cost**
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Expand me for a full table comparing the costs of all debrid services</summary>
|
||||
|
||||
@@ -234,7 +236,6 @@ The currency rates used here are updated semi-automatically every week.
|
||||
These prices do not consider any discounts/deals/sales that may be available.
|
||||
It also does not take into account any resellers that may offer the service at a cheaper price.
|
||||
|
||||
(I am working on allowing you to add/modify entries to this table.)
|
||||
</Admonition>
|
||||
|
||||
<DebridCostComparisonTable />
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import conversionRates from '@site/static/currency_rates.json';
|
||||
|
||||
export const availableCurrencies = Object.keys(conversionRates['USD']).filter((currency) =>
|
||||
Object.keys(conversionRates['EUR']).includes(currency)
|
||||
);
|
||||
|
||||
export const convertPrice = (price: number, fromCurrency: string, toCurrency: string): number => {
|
||||
if (fromCurrency === toCurrency || !toCurrency) return price;
|
||||
const rate = conversionRates[fromCurrency]?.[toCurrency];
|
||||
return rate ? price * rate : price;
|
||||
};
|
||||
|
||||
export const formatPrice = (price: number, currency: string) => {
|
||||
return Intl.NumberFormat(undefined, { style: 'currency', currency }).format(price);
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
import { availableCurrencies } from "./CurrencyRates";
|
||||
import Select from 'react-select';
|
||||
import { useColorMode } from '@docusaurus/theme-common';
|
||||
|
||||
const currencyOptions = availableCurrencies.map((currency) => ({ value: currency, label: currency })).sort((a, b) => a.label.localeCompare(b.label));
|
||||
|
||||
const getStyles = (colorMode: string) => ({
|
||||
control: (baseStyles: any, state: { isFocused: any; }) => ({
|
||||
...baseStyles,
|
||||
borderWidth: '2px',
|
||||
backgroundColor: colorMode === 'dark' ? '#1b1b1d' : 'white',
|
||||
borderRadius: 'var(--ifm-global-radius)',
|
||||
borderColor: state.isFocused ? 'var(--ifm-color-primary-darkest)' : null,
|
||||
color: 'var(--ifm-color-primary-dark)',
|
||||
boxShadow: 'var(--button-box-shadow)',
|
||||
"&:hover": {
|
||||
borderColor: state.isFocused ? 'var(--ifm-color-primary-darkest)' : null,
|
||||
boxShadow: 'var(--button-hover-box-shadow)',
|
||||
},
|
||||
}),
|
||||
input: (baseStyles: any, state: any) => ({
|
||||
...baseStyles,
|
||||
color: 'var(--ifm-color-primary-lightest)',
|
||||
}),
|
||||
singleValue: (baseStyles: any, state: any) => ({
|
||||
...baseStyles,
|
||||
color: 'var(--ifm-color-primary-lightest)',
|
||||
}),
|
||||
menu: (baseStyles: any, state: any) => ({
|
||||
...baseStyles,
|
||||
color: colorMode === 'dark' ? 'white' : 'black',
|
||||
backgroundColor: colorMode === 'dark' ? '#1b1b1d' : 'white',
|
||||
}),
|
||||
option: (baseStyles: any, state: { isFocused: any; }) => ({
|
||||
...baseStyles,
|
||||
color: 'var(--ifm-color-primary-lightest)',
|
||||
backgroundColor: state.isFocused ? 'var(--ifm-color-primary-darkest)' : null,
|
||||
"&:hover": {
|
||||
backgroundColor: 'var(--ifm-color-primary-darker)',
|
||||
},
|
||||
"&:active": {
|
||||
backgroundColor: 'var(--ifm-color-primary-darkest)',
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
interface CurrencySelectProps {
|
||||
currency: string;
|
||||
setCurrency: (currency: string) => void;
|
||||
}
|
||||
|
||||
export const CurrencySelect: React.FC<CurrencySelectProps> = ({ currency, setCurrency }) => {
|
||||
const { colorMode } = useColorMode();
|
||||
|
||||
return (
|
||||
<Select
|
||||
id="currency-select"
|
||||
value={{ value: currency, label: currency }}
|
||||
onChange={(selectedOption) => setCurrency(selectedOption ? selectedOption.value : null)}
|
||||
options={currencyOptions}
|
||||
isClearable={true}
|
||||
styles={getStyles(colorMode)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,69 +1,19 @@
|
||||
import React from 'react';
|
||||
import Select from 'react-select';
|
||||
import { useColorMode } from '@docusaurus/theme-common';
|
||||
import styles from './styles.module.css';
|
||||
import { CurrencySelect } from './CurrencySelect';
|
||||
|
||||
const getStyles = (colorMode: string) => ({
|
||||
control: (baseStyles: any, state: { isFocused: any; }) => ({
|
||||
...baseStyles,
|
||||
borderWidth: '2px',
|
||||
backgroundColor: colorMode === 'dark' ? '#1b1b1d' : 'white',
|
||||
borderRadius: 'var(--ifm-global-radius)',
|
||||
borderColor: state.isFocused ? 'var(--ifm-color-primary-darkest)' : null,
|
||||
color: 'var(--ifm-color-primary-dark)',
|
||||
boxShadow: 'var(--button-box-shadow)',
|
||||
"&:hover": {
|
||||
borderColor: state.isFocused ? 'var(--ifm-color-primary-darkest)' : null,
|
||||
boxShadow: 'var(--button-hover-box-shadow)',
|
||||
},
|
||||
}),
|
||||
input: (baseStyles: any, state: any) => ({
|
||||
...baseStyles,
|
||||
color: 'var(--ifm-color-primary-lightest)',
|
||||
}),
|
||||
singleValue: (baseStyles: any, state: any) => ({
|
||||
...baseStyles,
|
||||
color: 'var(--ifm-color-primary-lightest)',
|
||||
}),
|
||||
menu: (baseStyles: any, state: any) => ({
|
||||
...baseStyles,
|
||||
color: colorMode === 'dark' ? 'white' : 'black',
|
||||
backgroundColor: colorMode === 'dark' ? '#1b1b1d' : 'white',
|
||||
}),
|
||||
option: (baseStyles: any, state: { isFocused: any; }) => ({
|
||||
...baseStyles,
|
||||
color: 'var(--ifm-color-primary-lightest)',
|
||||
backgroundColor: state.isFocused ? 'var(--ifm-color-primary-darkest)' : null,
|
||||
"&:hover": {
|
||||
backgroundColor: 'var(--ifm-color-primary-darker)',
|
||||
},
|
||||
"&:active": {
|
||||
backgroundColor: 'var(--ifm-color-primary-darkest)',
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
interface CurrencySelectorProps {
|
||||
primaryCurrency: string;
|
||||
setPrimaryCurrency: (currency: string) => void;
|
||||
currencyOptions: { value: string, label: string }[];
|
||||
}
|
||||
|
||||
const CurrencySelector: React.FC<CurrencySelectorProps> = ({ primaryCurrency, setPrimaryCurrency, currencyOptions }) => {
|
||||
const { colorMode } = useColorMode();
|
||||
|
||||
const CurrencySelector: React.FC<CurrencySelectorProps> = ({ primaryCurrency, setPrimaryCurrency }) => {
|
||||
return (
|
||||
<div className={styles["currency-select-container"]}>
|
||||
<div className={styles["currency-select-box"]}>
|
||||
<label htmlFor="currency-select"><strong>Select Currency (or clear for Original):</strong></label>
|
||||
<Select
|
||||
id="currency-select"
|
||||
value={{ value: primaryCurrency, label: primaryCurrency }}
|
||||
onChange={(selectedOption) => setPrimaryCurrency(selectedOption ? selectedOption.value : null)}
|
||||
options={currencyOptions}
|
||||
isClearable={true}
|
||||
styles={getStyles(colorMode)}
|
||||
/>
|
||||
<label htmlFor="currency-select"><strong>Select Primary Currency</strong><br />The currency that all other prices are converted to. Leave blank to see original prices.</label>
|
||||
<CurrencySelect setCurrency={setPrimaryCurrency} currency={primaryCurrency} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
export const initialServiceData = [
|
||||
{ name: 'Torbox (Essential)', price: 33, duration: 365, currency: 'USD' },
|
||||
{ name: 'Torbox (Standard)', price: 55, duration: 365, currency: 'USD' },
|
||||
{ name: 'Torbox (Pro)', price: 110, duration: 365, currency: 'USD' },
|
||||
{ name: 'Real-Debrid', price: 16, duration: 180, currency: 'EUR', pointData: { pointsPerPlan: 800, pointsRequiredForReward: 1000, durationPerReward: 30 } },
|
||||
{ name: 'Debrid-Link', price: 25, duration: 300, currency: 'EUR' },
|
||||
{ name: 'AllDebrid', price: 24.99, duration: 300, currency: 'EUR', pointData: { pointsPerPlan: 140, pointsRequiredForReward: 150, durationPerReward: 30 } },
|
||||
{ name: 'Offcloud', price: 54.99, duration: 365, currency: 'USD' },
|
||||
{ name: 'Premiumize', price: 69.99, duration: 365, currency: 'EUR' },
|
||||
{ name: 'put.io (100GB)', price: 99, duration: 365, currency: 'USD' },
|
||||
{ name: 'put.io (1TB)', price: 199, duration: 365, currency: 'USD' },
|
||||
];
|
||||
@@ -0,0 +1,243 @@
|
||||
:root[data-theme="dark"] {
|
||||
--service-data-background-color: #2d2f33;
|
||||
}
|
||||
|
||||
:root[data-theme="light"] {
|
||||
--service-data-background-color: #dadde1;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes fadeOut {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes appearFromCenter {
|
||||
from {
|
||||
transform: scale(0);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes disappearToCenter {
|
||||
from {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
transform: scale(0);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-popup {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
animation: fadeIn 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
.settings-popup.fade-out {
|
||||
animation: fadeOut 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
background: var(--service-data-background-color);
|
||||
border-radius: var(--ifm-global-radius);
|
||||
padding: 20px;
|
||||
width: 80%;
|
||||
max-width: 600px;
|
||||
max-height: 80%;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
background: red;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
border-radius: var(--ifm-global-radius);
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.close-button:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 6px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.service-list {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.service-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--ifm-color-primary);
|
||||
gap: 5px;
|
||||
border: 1px solid #ccc;
|
||||
padding: 15px;
|
||||
border-radius: var(--ifm-global-radius);
|
||||
min-width: 220px;
|
||||
max-height: 1%;
|
||||
overflow-y: auto;
|
||||
transition: transform 0.5s ease-in-out;
|
||||
}
|
||||
.service-item.new-service {
|
||||
animation: appearFromCenter 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
.service-item.deleted-service {
|
||||
animation: disappearToCenter 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
.service-item.slide-left {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
.service-item.slide-right {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
|
||||
.add-new-service {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: transparent;
|
||||
flex-grow: 1;
|
||||
padding: 15px;
|
||||
border-radius: var(--ifm-global-radius);
|
||||
min-width: 220px;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
|
||||
.add-new-service-button {
|
||||
background: green;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 15px;
|
||||
cursor: pointer;
|
||||
border-radius: var(--ifm-global-radius);
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
|
||||
.add-new-service-button:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 6px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.add-new-service-button:active {
|
||||
transform: scale(0);
|
||||
}
|
||||
|
||||
.settings-button {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
position: fixed;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
.apply-button, .reset-button {
|
||||
background: green;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 15px;
|
||||
cursor: pointer;
|
||||
border-radius: var(--ifm-global-radius);
|
||||
margin: 10px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.apply-button:hover, .reset-button:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 6px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.reset-button {
|
||||
background: orange;
|
||||
}
|
||||
|
||||
.settings-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
select, input {
|
||||
padding: 10px;
|
||||
border-radius: var(--ifm-global-radius);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
transition: box-shadow 0.2s;
|
||||
}
|
||||
|
||||
select:focus, input:focus {
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.delete-button {
|
||||
background: red;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
border-radius: var(--ifm-global-radius);
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.delete-button:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 6px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.settings-content {
|
||||
width: 90%;
|
||||
max-width: 90%;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.service-item {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
import React, { useState } from 'react';
|
||||
import './Settings.css';
|
||||
import { availableCurrencies } from './CurrencyRates';
|
||||
import { initialServiceData } from './ServiceData';
|
||||
import { showToast } from '@site/src/components/Toasts';
|
||||
|
||||
interface Service {
|
||||
name: string;
|
||||
price: number;
|
||||
duration: number;
|
||||
currency: string;
|
||||
pointData?: {
|
||||
pointsPerPlan: number;
|
||||
pointsRequiredForReward: number;
|
||||
durationPerReward: number;
|
||||
},
|
||||
slideClass?: string;
|
||||
}
|
||||
|
||||
interface SettingsProps {
|
||||
serviceData: Service[];
|
||||
setServiceData: (data: Service[]) => void;
|
||||
closeSettings: () => void;
|
||||
primaryCurrency: string;
|
||||
setPrimaryCurrency: (currency: string) => void;
|
||||
}
|
||||
|
||||
const Settings: React.FC<SettingsProps> = ({ serviceData, setServiceData, closeSettings, primaryCurrency, setPrimaryCurrency }) => {
|
||||
const [tempServiceData, setTempServiceData] = useState<Service[]>([...serviceData]);
|
||||
const [isClosing, setIsClosing] = useState(false);
|
||||
const [newServiceIndex, setNewServiceIndex] = useState<number | null>(null);
|
||||
const [deletedServiceIndex, setDeletedServiceIndex] = useState<number | null>(null);
|
||||
|
||||
const handleAddService = () => {
|
||||
const newService: Service = {
|
||||
name: '',
|
||||
price: 0,
|
||||
duration: 0,
|
||||
currency: 'USD',
|
||||
|
||||
};
|
||||
setTempServiceData([...tempServiceData, newService]);
|
||||
setNewServiceIndex(tempServiceData.length);
|
||||
};
|
||||
|
||||
const handleUpdateService = (index: number, updatedService: Service) => {
|
||||
const updatedServices = [...tempServiceData];
|
||||
updatedServices[index] = updatedService;
|
||||
setTempServiceData(updatedServices);
|
||||
};
|
||||
|
||||
const handleDeleteService = (index: number) => {
|
||||
setDeletedServiceIndex(index);
|
||||
|
||||
// Temporarily add the 'slide-left' class to items to the right of the deleted one.
|
||||
const updatedServices = tempServiceData.map((service, i) => ({
|
||||
...service,
|
||||
slideClass: i > index ? 'slide-left' : '',
|
||||
}));
|
||||
|
||||
setTempServiceData(updatedServices);
|
||||
|
||||
// Delay removing the service to allow the animation to complete.
|
||||
setTimeout(() => {
|
||||
const filteredServices = tempServiceData.filter((_, i) => i !== index);
|
||||
setTempServiceData(filteredServices.map((service) => ({ ...service, slideClass: '' })));
|
||||
setDeletedServiceIndex(null);
|
||||
}, 500); // Match animation duration
|
||||
};
|
||||
|
||||
const handleApplyChanges = () => {
|
||||
if (tempServiceData.some((service) => !service.name || !service.price || !service.duration)) {
|
||||
showToast('All fields must be filled in', 'error');
|
||||
return;
|
||||
}
|
||||
if (tempServiceData.some((service) => service.price <= 0 || service.duration <= 0)) {
|
||||
showToast('Price and duration must be greater than 0', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
setServiceData(tempServiceData);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setTempServiceData(initialServiceData);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setIsClosing(true);
|
||||
setTimeout(() => {
|
||||
closeSettings();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`settings-popup ${isClosing ? 'fade-out' : ''}`}>
|
||||
<div className="settings-content">
|
||||
<button onClick={handleClose} className="close-button"></button>
|
||||
<h2>Service Data</h2>
|
||||
<div className="service-list">
|
||||
{tempServiceData.map((service, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`service-item ${service.slideClass} ${
|
||||
index === newServiceIndex ? 'new-service' : ''
|
||||
} ${index === deletedServiceIndex ? 'deleted-service' : ''}`}
|
||||
onAnimationEnd={() => {
|
||||
if (index === newServiceIndex) {
|
||||
setNewServiceIndex(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<label style={{ color: 'white' }}>Service Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={service.name}
|
||||
onChange={(e) => handleUpdateService(index, { ...service, name: e.target.value })}
|
||||
placeholder="Service Name"
|
||||
/>
|
||||
<label style={{ color: 'white' }}>Price</label>
|
||||
<input
|
||||
type="number"
|
||||
value={service.price}
|
||||
onChange={(e) => handleUpdateService(index, { ...service, price: parseFloat(e.target.value) })}
|
||||
placeholder="Price"
|
||||
/>
|
||||
<label style={{ color: 'white' }}>Duration</label>
|
||||
<input
|
||||
type="number"
|
||||
value={service.duration}
|
||||
onChange={(e) => handleUpdateService(index, { ...service, duration: parseInt(e.target.value) })}
|
||||
placeholder="Duration"
|
||||
/>
|
||||
<label style={{ color: 'white' }}>Currency</label>
|
||||
<select
|
||||
value={service.currency}
|
||||
onChange={(e) => handleUpdateService(index, { ...service, currency: e.target.value })}
|
||||
>
|
||||
{availableCurrencies.map((currency) => (
|
||||
<option key={currency} value={currency}>
|
||||
{currency}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button onClick={() => handleDeleteService(index)} className="delete-button">Delete</button>
|
||||
</div>
|
||||
))}
|
||||
<div className="service-item add-new-service">
|
||||
<button className="add-new-service-button" onClick={handleAddService}>+</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="settings-actions">
|
||||
<button onClick={handleReset} className="reset-button">Reset</button>
|
||||
<button onClick={handleApplyChanges} className="apply-button">Apply</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
@@ -1,24 +1,10 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import CurrencySelector from './CurrencySelector';
|
||||
import conversionRates from '@site/static/currency_rates.json';
|
||||
|
||||
const availableCurrencies = Object.keys(conversionRates['USD']).filter((currency) =>
|
||||
Object.keys(conversionRates['EUR']).includes(currency)
|
||||
);
|
||||
const currencyOptions = availableCurrencies.map((currency) => ({ value: currency, label: currency })).sort((a, b) => a.label.localeCompare(b.label));
|
||||
|
||||
const serviceData = [
|
||||
{ name: 'Torbox (Essential)', price: 33, duration: 365, currency: 'USD' },
|
||||
{ name: 'Torbox (Standard)', price: 55, duration: 365, currency: 'USD' },
|
||||
{ name: 'Torbox (Pro)', price: 110, duration: 365, currency: 'USD' },
|
||||
{ name: 'Real-Debrid', price: 16, duration: 180, currency: 'EUR', pointData: {pointsPerPlan: 800, pointsRequiredForReward: 1000, durationPerReward: 30} },
|
||||
{ name: 'Debrid-Link', price: 25, duration: 300, currency: 'EUR' },
|
||||
{ name: 'AllDebrid', price: 24.99, duration: 300, currency: 'EUR', pointData: {pointsPerPlan: 140, pointsRequiredForReward: 150, durationPerReward: 30} },
|
||||
{ name: 'Offcloud', price: 54.99, duration: 365, currency: 'USD' },
|
||||
{ name: 'Premiumize', price: 69.99, duration: 365, currency: 'EUR' },
|
||||
{ name: 'put.io (100GB)', price: 99, duration: 365, currency: 'USD' },
|
||||
{ name: 'put.io (1TB)', price: 199, duration: 365, currency: 'USD' },
|
||||
];
|
||||
import SettingsIcon from '@site/static/img/settings-icon.svg'
|
||||
import Settings from './Settings';
|
||||
import { convertPrice, formatPrice } from './CurrencyRates';
|
||||
import { initialServiceData } from './ServiceData';
|
||||
import styles from './styles.module.css';
|
||||
|
||||
interface Service {
|
||||
name: string;
|
||||
@@ -52,12 +38,6 @@ interface FormattedService extends CalculatedService {
|
||||
formattedPlanDuration: JSX.Element;
|
||||
}
|
||||
|
||||
const convertPrice = (price: number, fromCurrency: string, toCurrency: string): number => {
|
||||
if (fromCurrency === toCurrency || !toCurrency) return price;
|
||||
const rate = conversionRates[fromCurrency]?.[toCurrency];
|
||||
return rate ? price * rate : price;
|
||||
};
|
||||
|
||||
const calculatePrices = (service: Service, primaryCurrency: string): CalculatedService[] => {
|
||||
const entries = [];
|
||||
const baseCurrency = primaryCurrency || service.currency;
|
||||
@@ -105,10 +85,6 @@ const calculatePrices = (service: Service, primaryCurrency: string): CalculatedS
|
||||
return entries;
|
||||
};
|
||||
|
||||
const formatPrice = (price: number, currency: string) => {
|
||||
return Intl.NumberFormat(undefined, { style: 'currency', currency }).format(price);
|
||||
};
|
||||
|
||||
const formatServiceData = (data: CalculatedService[], primaryCurrency: string): FormattedService[] => {
|
||||
return data.map(service => ({
|
||||
...service,
|
||||
@@ -145,8 +121,10 @@ const formatServiceData = (data: CalculatedService[], primaryCurrency: string):
|
||||
};
|
||||
|
||||
export default function DebridCostComparisonTable({ excludeServices }: { excludeServices?: string[]; }): JSX.Element {
|
||||
const [primaryCurrency, setPrimaryCurrency] = useState<string>('GBP');
|
||||
const [primaryCurrency, setPrimaryCurrency] = useState<string>('');
|
||||
const [formattedData, setFormattedData] = useState<FormattedService[]>([]);
|
||||
const [serviceData, setServiceData] = useState<Service[]>(initialServiceData);
|
||||
const [showSettings, setShowSettings] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
let filteredServices = serviceData;
|
||||
@@ -166,15 +144,18 @@ export default function DebridCostComparisonTable({ excludeServices }: { exclude
|
||||
|
||||
const formatted = formatServiceData(data, primaryCurrency);
|
||||
setFormattedData(formatted);
|
||||
}, [primaryCurrency, excludeServices]);
|
||||
}, [primaryCurrency, excludeServices, serviceData]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CurrencySelector
|
||||
primaryCurrency={primaryCurrency}
|
||||
setPrimaryCurrency={setPrimaryCurrency}
|
||||
currencyOptions={currencyOptions}
|
||||
/>
|
||||
{showSettings && <Settings serviceData={serviceData} setServiceData={setServiceData} closeSettings={() => setShowSettings(false)} primaryCurrency={primaryCurrency} setPrimaryCurrency={setPrimaryCurrency} />}
|
||||
<button onClick={() => setShowSettings(!showSettings)} className={styles.settingsButton} >
|
||||
<b>Configure Table Data</b>
|
||||
</button>
|
||||
<CurrencySelector
|
||||
primaryCurrency={primaryCurrency}
|
||||
setPrimaryCurrency={setPrimaryCurrency}
|
||||
/>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -199,6 +180,8 @@ export default function DebridCostComparisonTable({ excludeServices }: { exclude
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -19,3 +19,26 @@
|
||||
.currency-select {
|
||||
color: var(--ifm-color-primary-darkest)
|
||||
}
|
||||
|
||||
.settingsButton {
|
||||
background: var(--ifm-color-primary);
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 20px;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 15px;
|
||||
cursor: pointer;
|
||||
align-self: flex-end;
|
||||
border-radius: var(--ifm-global-radius);
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.2s, box-shadow 0.2s, opacity 0.2s;
|
||||
}
|
||||
|
||||
|
||||
.settingsButton:hover {
|
||||
transform: scale(1.01) translateY(-2px);
|
||||
opacity: 0.9;
|
||||
box-shadow: var(--button-hover-box-shadow);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user