Enhance UI with dark mode support and update README with TODO list. Added PCB components to BOM and improved styling for various components to support dark mode. Updated Tailwind configuration to enable dark mode.
Some checks failed
Build and Publish Docker Image / build-and-push (push) Has been cancelled
Some checks failed
Build and Publish Docker Image / build-and-push (push) Has been cancelled
This commit is contained in:
25
README.md
25
README.md
@@ -43,6 +43,31 @@ The OSSM Configurator is a React-based single-page application built with Vite.
|
|||||||
- **Tailwind CSS** - Styling
|
- **Tailwind CSS** - Styling
|
||||||
- **JSZip** - For generating downloadable BOM packages
|
- **JSZip** - For generating downloadable BOM packages
|
||||||
|
|
||||||
|
## TODO
|
||||||
|
- [X] Dark Mode [Completed]
|
||||||
|
- [ ] Finalize Actuator Components and mapping to BOM [In Progress]
|
||||||
|
- [ ] Finalize Stand Components and mapping to BOM
|
||||||
|
- [ ] Finalize PCB Components and mapping to BOM
|
||||||
|
- [ ] Finalize Toy Mounts Components and mapping to BOM
|
||||||
|
- [ ] Finalize Remote Control Components and mapping to BOM
|
||||||
|
- [ ] Finalize Mounting Components and mapping to BOM
|
||||||
|
- [ ] Finalize Other Components and mapping to BOM
|
||||||
|
- [ ] Finalize Colors and mapping to BOM
|
||||||
|
- [ ] Finalize Pricing and mapping to BOM
|
||||||
|
- [ ] Finalize BOM Export and mapping to BOM
|
||||||
|
- [ ] Finalize BOM Import and mapping to BOM
|
||||||
|
- [ ] Finalize Storage and sharing of BOMs
|
||||||
|
- [ ] Add references to original hardware files and designs
|
||||||
|
- [ ] Add Readme/assembly instructions for each component
|
||||||
|
- [ ] Add FAQ and troubleshooting guide
|
||||||
|
- [ ] Add support for multiple languages
|
||||||
|
- [ ] Add support for multiple currencies
|
||||||
|
- [ ] Add support for multiple payment methods
|
||||||
|
- [ ] Add support for multiple shipping methods
|
||||||
|
- [ ] Add support for multiple shipping countries
|
||||||
|
- [ ] Add support for multiple shipping regions
|
||||||
|
- [ ] Add support for multiple shipping cities
|
||||||
|
- [ ] Add 3D render of final product with all components and options selected and coloured [If possible]
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import MainPage from './components/MainPage';
|
import MainPage from './components/MainPage';
|
||||||
import Wizard from './components/Wizard';
|
import Wizard from './components/Wizard';
|
||||||
|
import ThemeToggle from './components/ThemeToggle';
|
||||||
import partsData from './data/index.js';
|
import partsData from './data/index.js';
|
||||||
import { getSharedConfig } from './utils/shareService';
|
import { getSharedConfig } from './utils/shareService';
|
||||||
|
|
||||||
@@ -145,16 +146,24 @@ function App() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!buildType) {
|
if (!buildType) {
|
||||||
return <MainPage onSelectBuildType={handleSelectBuildType} />;
|
return (
|
||||||
|
<>
|
||||||
|
<ThemeToggle />
|
||||||
|
<MainPage onSelectBuildType={handleSelectBuildType} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wizard
|
<>
|
||||||
buildType={buildType}
|
<ThemeToggle />
|
||||||
initialConfig={config}
|
<Wizard
|
||||||
updateConfig={updateConfig}
|
buildType={buildType}
|
||||||
onBackToMain={handleBackToMain}
|
initialConfig={config}
|
||||||
/>
|
updateConfig={updateConfig}
|
||||||
|
onBackToMain={handleBackToMain}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -129,6 +129,19 @@ export default function BOMSummary({ config }) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Include PCB mount parts if a PCB mount is selected
|
||||||
|
if (config.pcbMount) {
|
||||||
|
const pcbMountId = config.pcbMount.id;
|
||||||
|
const pcbMountComponent = partsData.components?.[pcbMountId];
|
||||||
|
if (pcbMountComponent?.printedParts) {
|
||||||
|
pcbMountComponent.printedParts.forEach((part) => {
|
||||||
|
if (part.required) {
|
||||||
|
parts.push({ ...part, category: 'PCB Mount' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Include toy mount parts if any toy mounts are selected
|
// Include toy mount parts if any toy mounts are selected
|
||||||
if (config.toyMountOptions && config.toyMountOptions.length > 0) {
|
if (config.toyMountOptions && config.toyMountOptions.length > 0) {
|
||||||
if (partsData.components?.toyMounts?.printedParts) {
|
if (partsData.components?.toyMounts?.printedParts) {
|
||||||
@@ -274,6 +287,7 @@ export default function BOMSummary({ config }) {
|
|||||||
if (config.standFeet) selectedOptionIds.add(config.standFeet.id);
|
if (config.standFeet) selectedOptionIds.add(config.standFeet.id);
|
||||||
if (config.mount) selectedOptionIds.add(config.mount.id);
|
if (config.mount) selectedOptionIds.add(config.mount.id);
|
||||||
if (config.cover) selectedOptionIds.add(config.cover.id);
|
if (config.cover) selectedOptionIds.add(config.cover.id);
|
||||||
|
if (config.pcbMount) selectedOptionIds.add(config.pcbMount.id);
|
||||||
if (config.remoteKnob) selectedOptionIds.add(config.remoteKnob.id);
|
if (config.remoteKnob) selectedOptionIds.add(config.remoteKnob.id);
|
||||||
if (config.toyMountOptions && config.toyMountOptions.length > 0) {
|
if (config.toyMountOptions && config.toyMountOptions.length > 0) {
|
||||||
config.toyMountOptions.forEach(opt => selectedOptionIds.add(opt.id));
|
config.toyMountOptions.forEach(opt => selectedOptionIds.add(opt.id));
|
||||||
@@ -540,6 +554,7 @@ export default function BOMSummary({ config }) {
|
|||||||
if (config.standFeet) selectedOptionIds.add(config.standFeet.id);
|
if (config.standFeet) selectedOptionIds.add(config.standFeet.id);
|
||||||
if (config.mount) selectedOptionIds.add(config.mount.id);
|
if (config.mount) selectedOptionIds.add(config.mount.id);
|
||||||
if (config.cover) selectedOptionIds.add(config.cover.id);
|
if (config.cover) selectedOptionIds.add(config.cover.id);
|
||||||
|
if (config.pcbMount) selectedOptionIds.add(config.pcbMount.id);
|
||||||
if (config.remoteKnob) selectedOptionIds.add(config.remoteKnob.id);
|
if (config.remoteKnob) selectedOptionIds.add(config.remoteKnob.id);
|
||||||
if (config.toyMountOptions && config.toyMountOptions.length > 0) {
|
if (config.toyMountOptions && config.toyMountOptions.length > 0) {
|
||||||
config.toyMountOptions.forEach(opt => selectedOptionIds.add(opt.id));
|
config.toyMountOptions.forEach(opt => selectedOptionIds.add(opt.id));
|
||||||
@@ -650,7 +665,7 @@ export default function BOMSummary({ config }) {
|
|||||||
|
|
||||||
// Define main sections and their subcategories
|
// Define main sections and their subcategories
|
||||||
const mainSections = {
|
const mainSections = {
|
||||||
'Actuator + Mount': ['Actuator Body', 'Mount', 'Cover'],
|
'Actuator + Mount': ['Actuator Body', 'Mount', 'Cover', 'PCB Mount'],
|
||||||
'Stand': ['Stand', 'Stand Hinges', 'Stand Feet', 'Stand Crossbar Supports'],
|
'Stand': ['Stand', 'Stand Hinges', 'Stand Feet', 'Stand Crossbar Supports'],
|
||||||
'Remote': ['Remote Body', 'Remote Knobs'],
|
'Remote': ['Remote Body', 'Remote Knobs'],
|
||||||
};
|
};
|
||||||
@@ -669,13 +684,13 @@ export default function BOMSummary({ config }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-bold mb-4">BOM Summary</h2>
|
<h2 className="text-2xl font-bold mb-4 text-gray-900 dark:text-white">BOM Summary</h2>
|
||||||
<p className="text-gray-600 mb-6">
|
<p className="text-gray-600 dark:text-gray-300 mb-6">
|
||||||
Review your configuration and bill of materials.
|
Review your configuration and bill of materials.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Tab Navigation */}
|
{/* Tab Navigation */}
|
||||||
<div className="border-b border-gray-200 mb-6">
|
<div className="border-b border-gray-200 dark:border-gray-700 mb-6">
|
||||||
<nav className="-mb-px flex space-x-8">
|
<nav className="-mb-px flex space-x-8">
|
||||||
{tabs.map((tab) => (
|
{tabs.map((tab) => (
|
||||||
<button
|
<button
|
||||||
@@ -685,8 +700,8 @@ export default function BOMSummary({ config }) {
|
|||||||
py-4 px-1 border-b-2 font-medium text-sm transition-colors
|
py-4 px-1 border-b-2 font-medium text-sm transition-colors
|
||||||
${
|
${
|
||||||
activeTab === tab.id
|
activeTab === tab.id
|
||||||
? 'border-blue-500 text-blue-600'
|
? 'border-blue-500 dark:border-blue-400 text-blue-600 dark:text-blue-400'
|
||||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600'
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
@@ -703,8 +718,8 @@ export default function BOMSummary({ config }) {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Hardware (Motor & Power Supply) */}
|
{/* Hardware (Motor & Power Supply) */}
|
||||||
{(config.motor || config.powerSupply) && (
|
{(config.motor || config.powerSupply) && (
|
||||||
<div className="border-b border-gray-200 pb-4">
|
<div className="border-b border-gray-200 dark:border-gray-700 pb-4">
|
||||||
<h3 className="text-lg font-semibold mb-4">Hardware</h3>
|
<h3 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Hardware</h3>
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4">
|
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4">
|
||||||
{config.motor && (
|
{config.motor && (
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
@@ -712,14 +727,14 @@ export default function BOMSummary({ config }) {
|
|||||||
<img
|
<img
|
||||||
src={config.motor.image}
|
src={config.motor.image}
|
||||||
alt={config.motor.name}
|
alt={config.motor.name}
|
||||||
className="h-32 w-32 object-contain rounded-lg bg-gray-100 mb-2"
|
className="h-32 w-32 object-contain rounded-lg bg-gray-100 dark:bg-gray-700 mb-2"
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
e.target.style.display = 'none';
|
e.target.style.display = 'none';
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<span className="text-xs text-center text-gray-700 font-medium">{config.motor.name}</span>
|
<span className="text-xs text-center text-gray-700 dark:text-gray-300 font-medium">{config.motor.name}</span>
|
||||||
<span className="text-xs text-center text-gray-600 mt-1">{formatPrice(config.motor.price)}</span>
|
<span className="text-xs text-center text-gray-600 dark:text-gray-400 mt-1">{formatPrice(config.motor.price)}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{config.powerSupply && (
|
{config.powerSupply && (
|
||||||
@@ -728,14 +743,14 @@ export default function BOMSummary({ config }) {
|
|||||||
<img
|
<img
|
||||||
src={config.powerSupply.image}
|
src={config.powerSupply.image}
|
||||||
alt={config.powerSupply.name}
|
alt={config.powerSupply.name}
|
||||||
className="h-32 w-32 object-contain rounded-lg bg-gray-100 mb-2"
|
className="h-32 w-32 object-contain rounded-lg bg-gray-100 dark:bg-gray-700 mb-2"
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
e.target.style.display = 'none';
|
e.target.style.display = 'none';
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<span className="text-xs text-center text-gray-700 font-medium">{config.powerSupply.name}</span>
|
<span className="text-xs text-center text-gray-700 dark:text-gray-300 font-medium">{config.powerSupply.name}</span>
|
||||||
<span className="text-xs text-center text-gray-600 mt-1">{formatPrice(config.powerSupply.price)}</span>
|
<span className="text-xs text-center text-gray-600 dark:text-gray-400 mt-1">{formatPrice(config.powerSupply.price)}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -744,20 +759,20 @@ export default function BOMSummary({ config }) {
|
|||||||
|
|
||||||
{/* Filament Usage */}
|
{/* Filament Usage */}
|
||||||
{(filamentTotals.total > 0 || totalTime !== '0m') && (
|
{(filamentTotals.total > 0 || totalTime !== '0m') && (
|
||||||
<div className="border-b border-gray-200 pb-4">
|
<div className="border-b border-gray-200 dark:border-gray-700 pb-4">
|
||||||
<h3 className="text-lg font-semibold mb-2">Filament Usage</h3>
|
<h3 className="text-lg font-semibold mb-2 text-gray-900 dark:text-white">Filament Usage</h3>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{filamentTotals.total > 0 && (
|
{filamentTotals.total > 0 && (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="font-semibold text-gray-700">Total Filament:</span>
|
<span className="font-semibold text-gray-700 dark:text-gray-300">Total Filament:</span>
|
||||||
<span className="font-bold text-gray-900">{Math.round(filamentTotals.total)}g</span>
|
<span className="font-bold text-gray-900 dark:text-white">{Math.round(filamentTotals.total)}g</span>
|
||||||
</div>
|
</div>
|
||||||
{filamentTotals.primary > 0 && (
|
{filamentTotals.primary > 0 && (
|
||||||
<div className="flex justify-between items-center text-sm text-gray-600 ml-4">
|
<div className="flex justify-between items-center text-sm text-gray-600 dark:text-gray-400 ml-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div
|
<div
|
||||||
className="w-4 h-4 rounded-full border border-gray-300"
|
className="w-4 h-4 rounded-full border border-gray-300 dark:border-gray-600"
|
||||||
style={{ backgroundColor: getColorHex(config.primaryColor, 'primary') }}
|
style={{ backgroundColor: getColorHex(config.primaryColor, 'primary') }}
|
||||||
/>
|
/>
|
||||||
<span>Primary ({getColorName(config.primaryColor, 'primary')}):</span>
|
<span>Primary ({getColorName(config.primaryColor, 'primary')}):</span>
|
||||||
@@ -766,10 +781,10 @@ export default function BOMSummary({ config }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{filamentTotals.secondary > 0 && (
|
{filamentTotals.secondary > 0 && (
|
||||||
<div className="flex justify-between items-center text-sm text-gray-600 ml-4">
|
<div className="flex justify-between items-center text-sm text-gray-600 dark:text-gray-400 ml-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div
|
<div
|
||||||
className="w-4 h-4 rounded-full border border-gray-300"
|
className="w-4 h-4 rounded-full border border-gray-300 dark:border-gray-600"
|
||||||
style={{ backgroundColor: getColorHex(config.accentColor, 'accent') }}
|
style={{ backgroundColor: getColorHex(config.accentColor, 'accent') }}
|
||||||
/>
|
/>
|
||||||
<span>Secondary ({getColorName(config.accentColor, 'accent')}):</span>
|
<span>Secondary ({getColorName(config.accentColor, 'accent')}):</span>
|
||||||
@@ -780,9 +795,9 @@ export default function BOMSummary({ config }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{totalTime !== '0m' && (
|
{totalTime !== '0m' && (
|
||||||
<div className="flex justify-between items-center pt-2 border-t border-gray-100">
|
<div className="flex justify-between items-center pt-2 border-t border-gray-100 dark:border-gray-800">
|
||||||
<span className="font-semibold text-gray-700">Total Printing Time:</span>
|
<span className="font-semibold text-gray-700 dark:text-gray-300">Total Printing Time:</span>
|
||||||
<span className="font-bold text-gray-900">{totalTime}</span>
|
<span className="font-bold text-gray-900 dark:text-white">{totalTime}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -790,11 +805,11 @@ export default function BOMSummary({ config }) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Selected Options/Kit */}
|
{/* Selected Options/Kit */}
|
||||||
{(config.mount || config.cover || config.standHinge || config.standFeet ||
|
{(config.mount || config.cover || config.pcbMount || config.standHinge || config.standFeet ||
|
||||||
(config.standCrossbarSupports && config.standCrossbarSupports.length > 0) ||
|
(config.standCrossbarSupports && config.standCrossbarSupports.length > 0) ||
|
||||||
(config.remoteType || config.remote?.id)) && (
|
(config.remoteType || config.remote?.id)) && (
|
||||||
<div className="border-b border-gray-200 pb-4">
|
<div className="border-b border-gray-200 dark:border-gray-700 pb-4">
|
||||||
<h3 className="text-lg font-semibold mb-4">Selected Options</h3>
|
<h3 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Selected Options</h3>
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4">
|
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4">
|
||||||
{config.mount && (
|
{config.mount && (
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
@@ -808,7 +823,7 @@ export default function BOMSummary({ config }) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<span className="text-xs text-center text-gray-700 font-medium">{config.mount.name}</span>
|
<span className="text-xs text-center text-gray-700 dark:text-gray-300 font-medium">{config.mount.name}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{config.cover && (
|
{config.cover && (
|
||||||
@@ -823,7 +838,22 @@ export default function BOMSummary({ config }) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<span className="text-xs text-center text-gray-700 font-medium">{config.cover.name}</span>
|
<span className="text-xs text-center text-gray-700 dark:text-gray-300 font-medium">{config.cover.name}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{config.pcbMount && (
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
{config.pcbMount.image && (
|
||||||
|
<img
|
||||||
|
src={config.pcbMount.image}
|
||||||
|
alt={config.pcbMount.name}
|
||||||
|
className="h-32 w-32 object-contain rounded-lg bg-gray-100 mb-2"
|
||||||
|
onError={(e) => {
|
||||||
|
e.target.style.display = 'none';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<span className="text-xs text-center text-gray-700 dark:text-gray-300 font-medium">{config.pcbMount.name}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{config.standHinge && (
|
{config.standHinge && (
|
||||||
@@ -838,7 +868,7 @@ export default function BOMSummary({ config }) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<span className="text-xs text-center text-gray-700 font-medium">{config.standHinge.name}</span>
|
<span className="text-xs text-center text-gray-700 dark:text-gray-300 font-medium">{config.standHinge.name}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{config.standFeet && (
|
{config.standFeet && (
|
||||||
@@ -853,7 +883,7 @@ export default function BOMSummary({ config }) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<span className="text-xs text-center text-gray-700 font-medium">{config.standFeet.name}</span>
|
<span className="text-xs text-center text-gray-700 dark:text-gray-300 font-medium">{config.standFeet.name}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{config.standCrossbarSupports && config.standCrossbarSupports.length > 0 && (
|
{config.standCrossbarSupports && config.standCrossbarSupports.length > 0 && (
|
||||||
@@ -864,13 +894,13 @@ export default function BOMSummary({ config }) {
|
|||||||
<img
|
<img
|
||||||
src={support.image}
|
src={support.image}
|
||||||
alt={support.name}
|
alt={support.name}
|
||||||
className="h-32 w-32 object-contain rounded-lg bg-gray-100 mb-2"
|
className="h-32 w-32 object-contain rounded-lg bg-gray-100 dark:bg-gray-700 mb-2"
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
e.target.style.display = 'none';
|
e.target.style.display = 'none';
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<span className="text-xs text-center text-gray-700 font-medium">{support.name}</span>
|
<span className="text-xs text-center text-gray-700 dark:text-gray-300 font-medium">{support.name}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
@@ -890,7 +920,7 @@ export default function BOMSummary({ config }) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<span className="text-xs text-center text-gray-700 font-medium">{remoteSystem.name}</span>
|
<span className="text-xs text-center text-gray-700 dark:text-gray-300 font-medium">{remoteSystem.name}</span>
|
||||||
</div>
|
</div>
|
||||||
) : null;
|
) : null;
|
||||||
})()}
|
})()}
|
||||||
@@ -900,8 +930,8 @@ export default function BOMSummary({ config }) {
|
|||||||
|
|
||||||
{/* Toy Mounts */}
|
{/* Toy Mounts */}
|
||||||
{config.toyMountOptions && config.toyMountOptions.length > 0 && (
|
{config.toyMountOptions && config.toyMountOptions.length > 0 && (
|
||||||
<div className="border-b border-gray-200 pb-4">
|
<div className="border-b border-gray-200 dark:border-gray-700 pb-4">
|
||||||
<h3 className="text-lg font-semibold mb-4">Toy Mounts</h3>
|
<h3 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Toy Mounts</h3>
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4">
|
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4">
|
||||||
{config.toyMountOptions.map((toyMount) => (
|
{config.toyMountOptions.map((toyMount) => (
|
||||||
<div key={toyMount.id} className="flex flex-col items-center">
|
<div key={toyMount.id} className="flex flex-col items-center">
|
||||||
@@ -915,9 +945,9 @@ export default function BOMSummary({ config }) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<span className="text-xs text-center text-gray-700 font-medium">{toyMount.name}</span>
|
<span className="text-xs text-center text-gray-700 dark:text-gray-300 font-medium">{toyMount.name}</span>
|
||||||
{toyMount.description && (
|
{toyMount.description && (
|
||||||
<span className="text-xs text-center text-gray-500 mt-1">{toyMount.description}</span>
|
<span className="text-xs text-center text-gray-500 dark:text-gray-400 mt-1">{toyMount.description}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -926,10 +956,10 @@ export default function BOMSummary({ config }) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Total */}
|
{/* Total */}
|
||||||
<div className="pt-4 border-t-2 border-gray-300">
|
<div className="pt-4 border-t-2 border-gray-300 dark:border-gray-700">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<h3 className="text-xl font-bold">Total Hardware Cost</h3>
|
<h3 className="text-xl font-bold text-gray-900 dark:text-white">Total Hardware Cost</h3>
|
||||||
<p className="text-2xl font-bold text-blue-600">${total.toFixed(2)}</p>
|
<p className="text-2xl font-bold text-blue-600 dark:text-blue-400">${total.toFixed(2)}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -940,8 +970,8 @@ export default function BOMSummary({ config }) {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{printedParts.length > 0 ? (
|
{printedParts.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
<div className="border-b border-gray-200 pb-4">
|
<div className="border-b border-gray-200 dark:border-gray-700 pb-4">
|
||||||
<h3 className="text-lg font-semibold mb-4">Required Printed Parts</h3>
|
<h3 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Required Printed Parts</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{Object.entries(mainSections).map(([mainSectionName, subcategories]) => {
|
{Object.entries(mainSections).map(([mainSectionName, subcategories]) => {
|
||||||
@@ -950,8 +980,8 @@ export default function BOMSummary({ config }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={mainSectionName} className="border-l-2 border-blue-200 pl-4">
|
<div key={mainSectionName} className="border-l-2 border-blue-200 dark:border-blue-700 pl-4">
|
||||||
<h4 className="text-lg font-semibold text-gray-800 mb-3">{mainSectionName}</h4>
|
<h4 className="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-3">{mainSectionName}</h4>
|
||||||
<div className="space-y-4 ml-2">
|
<div className="space-y-4 ml-2">
|
||||||
{subcategories.map((category) => {
|
{subcategories.map((category) => {
|
||||||
const parts = partsByCategory[category];
|
const parts = partsByCategory[category];
|
||||||
@@ -962,26 +992,26 @@ export default function BOMSummary({ config }) {
|
|||||||
return (
|
return (
|
||||||
<div key={category}>
|
<div key={category}>
|
||||||
<div className="flex items-center gap-2 mb-3">
|
<div className="flex items-center gap-2 mb-3">
|
||||||
<h5 className="text-md font-medium text-gray-700">{category}</h5>
|
<h5 className="text-md font-medium text-gray-700 dark:text-gray-300">{category}</h5>
|
||||||
{parts.some(p => p.replacesActuatorMiddle) && (
|
{parts.some(p => p.replacesActuatorMiddle) && (
|
||||||
<span className="text-xs text-blue-600 bg-blue-50 px-2 py-1 rounded">
|
<span className="text-xs text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/30 px-2 py-1 rounded">
|
||||||
Replaces standard ossm-actuator-body-middle
|
Replaces standard ossm-actuator-body-middle
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="min-w-full divide-y divide-gray-200 border border-gray-200 rounded-lg">
|
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700 border border-gray-200 dark:border-gray-700 rounded-lg">
|
||||||
<thead className="bg-gray-50">
|
<thead className="bg-gray-50 dark:bg-gray-800">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">Part Name</th>
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Part Name</th>
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">Color</th>
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Color</th>
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">Description</th>
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Description</th>
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">File Path</th>
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">File Path</th>
|
||||||
<th className="px-4 py-3 text-right text-xs font-medium text-gray-700 uppercase tracking-wider">Quantity</th>
|
<th className="px-4 py-3 text-right text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Quantity</th>
|
||||||
<th className="px-4 py-3 text-right text-xs font-medium text-gray-700 uppercase tracking-wider">Filament</th>
|
<th className="px-4 py-3 text-right text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Filament</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="bg-white divide-y divide-gray-200">
|
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
{parts.map((part) => {
|
{parts.map((part) => {
|
||||||
const partColour = part.colour || 'primary';
|
const partColour = part.colour || 'primary';
|
||||||
const colorHex = getColorHex(
|
const colorHex = getColorHex(
|
||||||
@@ -994,51 +1024,51 @@ export default function BOMSummary({ config }) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr key={part.id} className="hover:bg-gray-50">
|
<tr key={part.id} className="hover:bg-gray-50 dark:hover:bg-gray-700/50">
|
||||||
<td className="px-4 py-3 whitespace-nowrap">
|
<td className="px-4 py-3 whitespace-nowrap">
|
||||||
<p className="text-sm font-medium text-gray-900">{part.name}</p>
|
<p className="text-sm font-medium text-gray-900 dark:text-white">{part.name}</p>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 whitespace-nowrap">
|
<td className="px-4 py-3 whitespace-nowrap">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div
|
<div
|
||||||
className="w-4 h-4 rounded-full border border-gray-300"
|
className="w-4 h-4 rounded-full border border-gray-300 dark:border-gray-600"
|
||||||
style={{ backgroundColor: colorHex }}
|
style={{ backgroundColor: colorHex }}
|
||||||
title={`${partColour === 'primary' ? 'Primary' : 'Secondary'} color: ${colorName}`}
|
title={`${partColour === 'primary' ? 'Primary' : 'Secondary'} color: ${colorName}`}
|
||||||
/>
|
/>
|
||||||
<span className="text-xs text-gray-600 capitalize">{partColour}</span>
|
<span className="text-xs text-gray-600 dark:text-gray-400 capitalize">{partColour}</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
<p className="text-sm text-gray-600">{part.description || '-'}</p>
|
<p className="text-sm text-gray-600 dark:text-gray-300">{part.description || '-'}</p>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
{part.isHardwareOnly ? (
|
{part.isHardwareOnly ? (
|
||||||
<span className="text-xs text-blue-600 italic">Hardware only</span>
|
<span className="text-xs text-blue-600 dark:text-blue-400 italic">Hardware only</span>
|
||||||
) : part.filePath ? (
|
) : part.filePath ? (
|
||||||
<p className="text-xs text-gray-500 font-mono">{part.filePath}</p>
|
<p className="text-xs text-gray-500 dark:text-gray-400 font-mono">{part.filePath}</p>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-gray-400">-</span>
|
<span className="text-gray-400 dark:text-gray-500">-</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 whitespace-nowrap text-right">
|
<td className="px-4 py-3 whitespace-nowrap text-right">
|
||||||
<p className="text-sm font-medium text-gray-700">{part.quantity || 1}</p>
|
<p className="text-sm font-medium text-gray-700 dark:text-gray-300">{part.quantity || 1}</p>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 whitespace-nowrap text-right">
|
<td className="px-4 py-3 whitespace-nowrap text-right">
|
||||||
{part.isHardwareOnly ? (
|
{part.isHardwareOnly ? (
|
||||||
<span className="text-xs text-blue-600">-</span>
|
<span className="text-xs text-blue-600 dark:text-blue-400">-</span>
|
||||||
) : part.filamentEstimate !== undefined && part.filamentEstimate > 0 ? (
|
) : part.filamentEstimate !== undefined && part.filamentEstimate > 0 ? (
|
||||||
<p className="text-sm font-medium text-gray-700">
|
<p className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
{typeof part.filamentEstimate === 'number'
|
{typeof part.filamentEstimate === 'number'
|
||||||
? (part.filamentEstimate * (part.quantity || 1)).toFixed(1)
|
? (part.filamentEstimate * (part.quantity || 1)).toFixed(1)
|
||||||
: part.filamentEstimate}g
|
: part.filamentEstimate}g
|
||||||
{(part.quantity || 1) > 1 && (
|
{(part.quantity || 1) > 1 && (
|
||||||
<span className="text-xs text-gray-500 ml-1">
|
<span className="text-xs text-gray-500 dark:text-gray-400 ml-1">
|
||||||
({(typeof part.filamentEstimate === 'number' ? part.filamentEstimate : parseFloat(part.filamentEstimate.replace(/[~g]/g, '').trim()) || 0).toFixed(1)}g × {part.quantity})
|
({(typeof part.filamentEstimate === 'number' ? part.filamentEstimate : parseFloat(part.filamentEstimate.replace(/[~g]/g, '').trim()) || 0).toFixed(1)}g × {part.quantity})
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-gray-400">-</span>
|
<span className="text-gray-400 dark:text-gray-500">-</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -1065,9 +1095,9 @@ export default function BOMSummary({ config }) {
|
|||||||
return (
|
return (
|
||||||
<div key={category}>
|
<div key={category}>
|
||||||
<div className="flex items-center gap-2 mb-3">
|
<div className="flex items-center gap-2 mb-3">
|
||||||
<h4 className="text-md font-medium text-gray-700">{category}</h4>
|
<h4 className="text-md font-medium text-gray-700 dark:text-gray-300">{category}</h4>
|
||||||
{parts.some(p => p.replacesActuatorMiddle) && (
|
{parts.some(p => p.replacesActuatorMiddle) && (
|
||||||
<span className="text-xs text-blue-600 bg-blue-50 px-2 py-1 rounded">
|
<span className="text-xs text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/30 px-2 py-1 rounded">
|
||||||
Replaces standard ossm-actuator-body-middle
|
Replaces standard ossm-actuator-body-middle
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -1120,7 +1150,7 @@ export default function BOMSummary({ config }) {
|
|||||||
) : part.filePath ? (
|
) : part.filePath ? (
|
||||||
<p className="text-xs text-gray-500 font-mono">{part.filePath}</p>
|
<p className="text-xs text-gray-500 font-mono">{part.filePath}</p>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-gray-400">-</span>
|
<span className="text-gray-400 dark:text-gray-500">-</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 whitespace-nowrap text-right">
|
<td className="px-4 py-3 whitespace-nowrap text-right">
|
||||||
@@ -1133,13 +1163,13 @@ export default function BOMSummary({ config }) {
|
|||||||
? (part.filamentEstimate * (part.quantity || 1)).toFixed(1)
|
? (part.filamentEstimate * (part.quantity || 1)).toFixed(1)
|
||||||
: part.filamentEstimate}g
|
: part.filamentEstimate}g
|
||||||
{(part.quantity || 1) > 1 && (
|
{(part.quantity || 1) > 1 && (
|
||||||
<span className="text-xs text-gray-500 ml-1">
|
<span className="text-xs text-gray-500 dark:text-gray-400 ml-1">
|
||||||
({(typeof part.filamentEstimate === 'number' ? part.filamentEstimate : parseFloat(part.filamentEstimate.replace(/[~g]/g, '').trim()) || 0).toFixed(1)}g × {part.quantity})
|
({(typeof part.filamentEstimate === 'number' ? part.filamentEstimate : parseFloat(part.filamentEstimate.replace(/[~g]/g, '').trim()) || 0).toFixed(1)}g × {part.quantity})
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-gray-400">-</span>
|
<span className="text-gray-400 dark:text-gray-500">-</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -1152,21 +1182,21 @@ export default function BOMSummary({ config }) {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{(filamentTotals.total > 0 || totalTime !== '0m') && (
|
{(filamentTotals.total > 0 || totalTime !== '0m') && (
|
||||||
<div className="mt-4 pt-4 border-t border-gray-200 space-y-2">
|
<div className="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700 space-y-2">
|
||||||
{filamentTotals.total > 0 && (
|
{filamentTotals.total > 0 && (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="font-semibold text-gray-700">Total Filament Estimate:</span>
|
<span className="font-semibold text-gray-700 dark:text-gray-300">Total Filament Estimate:</span>
|
||||||
<span className="font-bold text-gray-900">{Math.round(filamentTotals.total)}g</span>
|
<span className="font-bold text-gray-900 dark:text-white">{Math.round(filamentTotals.total)}g</span>
|
||||||
</div>
|
</div>
|
||||||
{filamentTotals.primary > 0 && (
|
{filamentTotals.primary > 0 && (
|
||||||
<div className="flex justify-between items-center text-sm text-gray-600 ml-4">
|
<div className="flex justify-between items-center text-sm text-gray-600 dark:text-gray-400 ml-4">
|
||||||
<span>Primary ({getColorName(config.primaryColor, 'primary')}):</span>
|
<span>Primary ({getColorName(config.primaryColor, 'primary')}):</span>
|
||||||
<span>{Math.round(filamentTotals.primary)}g</span>
|
<span>{Math.round(filamentTotals.primary)}g</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{filamentTotals.secondary > 0 && (
|
{filamentTotals.secondary > 0 && (
|
||||||
<div className="flex justify-between items-center text-sm text-gray-600 ml-4">
|
<div className="flex justify-between items-center text-sm text-gray-600 dark:text-gray-400 ml-4">
|
||||||
<span>Secondary ({getColorName(config.accentColor, 'accent')}):</span>
|
<span>Secondary ({getColorName(config.accentColor, 'accent')}):</span>
|
||||||
<span>{Math.round(filamentTotals.secondary)}g</span>
|
<span>{Math.round(filamentTotals.secondary)}g</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -1174,9 +1204,9 @@ export default function BOMSummary({ config }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{totalTime !== '0m' && (
|
{totalTime !== '0m' && (
|
||||||
<div className="flex justify-between items-center pt-2 border-t border-gray-100">
|
<div className="flex justify-between items-center pt-2 border-t border-gray-100 dark:border-gray-800">
|
||||||
<span className="font-semibold text-gray-700">Total Printing Time:</span>
|
<span className="font-semibold text-gray-700 dark:text-gray-300">Total Printing Time:</span>
|
||||||
<span className="font-bold text-gray-900">{totalTime}</span>
|
<span className="font-bold text-gray-900 dark:text-white">{totalTime}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -1184,7 +1214,7 @@ export default function BOMSummary({ config }) {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center py-12 text-gray-500">
|
<div className="text-center py-12 text-gray-500 dark:text-gray-400">
|
||||||
<p>No printed parts required for this configuration.</p>
|
<p>No printed parts required for this configuration.</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -1196,16 +1226,16 @@ export default function BOMSummary({ config }) {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{hardwareParts.length > 0 ? (
|
{hardwareParts.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
<div className="border-b border-gray-200 pb-4">
|
<div className="border-b border-gray-200 dark:border-gray-700 pb-4">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<h3 className="text-lg font-semibold">Required Hardware Parts</h3>
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">Required Hardware Parts</h3>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => setHardwareViewMode('unified')}
|
onClick={() => setHardwareViewMode('unified')}
|
||||||
className={`px-3 py-1.5 text-sm font-medium rounded-lg transition-colors ${
|
className={`px-3 py-1.5 text-sm font-medium rounded-lg transition-colors ${
|
||||||
hardwareViewMode === 'unified'
|
hardwareViewMode === 'unified'
|
||||||
? 'bg-blue-600 text-white'
|
? 'bg-blue-600 dark:bg-blue-500 text-white'
|
||||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
: 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Unified View
|
Unified View
|
||||||
@@ -1214,8 +1244,8 @@ export default function BOMSummary({ config }) {
|
|||||||
onClick={() => setHardwareViewMode('expanded')}
|
onClick={() => setHardwareViewMode('expanded')}
|
||||||
className={`px-3 py-1.5 text-sm font-medium rounded-lg transition-colors ${
|
className={`px-3 py-1.5 text-sm font-medium rounded-lg transition-colors ${
|
||||||
hardwareViewMode === 'expanded'
|
hardwareViewMode === 'expanded'
|
||||||
? 'bg-blue-600 text-white'
|
? 'bg-blue-600 dark:bg-blue-500 text-white'
|
||||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
: 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Expanded View
|
Expanded View
|
||||||
@@ -1237,34 +1267,34 @@ export default function BOMSummary({ config }) {
|
|||||||
return indexA - indexB;
|
return indexA - indexB;
|
||||||
}).map(([type, parts]) => (
|
}).map(([type, parts]) => (
|
||||||
<div key={type}>
|
<div key={type}>
|
||||||
<h4 className="text-md font-medium text-gray-700 mb-3">{type}</h4>
|
<h4 className="text-md font-medium text-gray-700 dark:text-gray-300 mb-3">{type}</h4>
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="min-w-full divide-y divide-gray-200 border border-gray-200 rounded-lg">
|
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700 border border-gray-200 dark:border-gray-700 rounded-lg">
|
||||||
<thead className="bg-gray-50">
|
<thead className="bg-gray-50 dark:bg-gray-800">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">Part Name</th>
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Part Name</th>
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">Description</th>
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Description</th>
|
||||||
<th className="px-4 py-3 text-right text-xs font-medium text-gray-700 uppercase tracking-wider">Quantity</th>
|
<th className="px-4 py-3 text-right text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Quantity</th>
|
||||||
<th className="px-4 py-3 text-right text-xs font-medium text-gray-700 uppercase tracking-wider">Price</th>
|
<th className="px-4 py-3 text-right text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Price</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="bg-white divide-y divide-gray-200">
|
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
{parts.map((part) => (
|
{parts.map((part) => (
|
||||||
<tr key={part.id} className="hover:bg-gray-50">
|
<tr key={part.id} className="hover:bg-gray-50 dark:hover:bg-gray-700/50">
|
||||||
<td className="px-4 py-3 whitespace-nowrap">
|
<td className="px-4 py-3 whitespace-nowrap">
|
||||||
<p className="text-sm font-medium text-gray-900">{part.name}</p>
|
<p className="text-sm font-medium text-gray-900 dark:text-white">{part.name}</p>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
<p className="text-sm text-gray-600">{part.description || '-'}</p>
|
<p className="text-sm text-gray-600 dark:text-gray-300">{part.description || '-'}</p>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 whitespace-nowrap text-right">
|
<td className="px-4 py-3 whitespace-nowrap text-right">
|
||||||
<p className="text-sm font-medium text-gray-700">{part.quantity || 1}</p>
|
<p className="text-sm font-medium text-gray-700 dark:text-gray-300">{part.quantity || 1}</p>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 whitespace-nowrap text-right">
|
<td className="px-4 py-3 whitespace-nowrap text-right">
|
||||||
{part.price && part.price > 0 ? (
|
{part.price && part.price > 0 ? (
|
||||||
<p className="text-sm font-medium text-gray-700">{formatPrice(part.price)}</p>
|
<p className="text-sm font-medium text-gray-700 dark:text-gray-300">{formatPrice(part.price)}</p>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-gray-400">-</span>
|
<span className="text-gray-400 dark:text-gray-500">-</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -1278,40 +1308,40 @@ export default function BOMSummary({ config }) {
|
|||||||
// Expanded view: Group by component BOMs (shows hardware breakdown by component)
|
// Expanded view: Group by component BOMs (shows hardware breakdown by component)
|
||||||
expandedHardwareByComponent.map(({ component, parts }) => (
|
expandedHardwareByComponent.map(({ component, parts }) => (
|
||||||
<div key={component}>
|
<div key={component}>
|
||||||
<h4 className="text-md font-medium text-gray-700 mb-3">{component}</h4>
|
<h4 className="text-md font-medium text-gray-700 dark:text-gray-300 mb-3">{component}</h4>
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="min-w-full divide-y divide-gray-200 border border-gray-200 rounded-lg">
|
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700 border border-gray-200 dark:border-gray-700 rounded-lg">
|
||||||
<thead className="bg-gray-50">
|
<thead className="bg-gray-50 dark:bg-gray-800">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">Part Name</th>
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Part Name</th>
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">Description</th>
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Description</th>
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">Type</th>
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Type</th>
|
||||||
<th className="px-4 py-3 text-right text-xs font-medium text-gray-700 uppercase tracking-wider">Quantity</th>
|
<th className="px-4 py-3 text-right text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Quantity</th>
|
||||||
<th className="px-4 py-3 text-right text-xs font-medium text-gray-700 uppercase tracking-wider">Price</th>
|
<th className="px-4 py-3 text-right text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Price</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="bg-white divide-y divide-gray-200">
|
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
{parts.map((part) => (
|
{parts.map((part) => (
|
||||||
<tr key={part.id} className="hover:bg-gray-50">
|
<tr key={part.id} className="hover:bg-gray-50 dark:hover:bg-gray-700/50">
|
||||||
<td className="px-4 py-3 whitespace-nowrap">
|
<td className="px-4 py-3 whitespace-nowrap">
|
||||||
<p className="text-sm font-medium text-gray-900">{part.name}</p>
|
<p className="text-sm font-medium text-gray-900 dark:text-white">{part.name}</p>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
<p className="text-sm text-gray-600">{part.description || '-'}</p>
|
<p className="text-sm text-gray-600 dark:text-gray-300">{part.description || '-'}</p>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 whitespace-nowrap">
|
<td className="px-4 py-3 whitespace-nowrap">
|
||||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-300">
|
||||||
{part.hardwareType || 'Other Hardware'}
|
{part.hardwareType || 'Other Hardware'}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 whitespace-nowrap text-right">
|
<td className="px-4 py-3 whitespace-nowrap text-right">
|
||||||
<p className="text-sm font-medium text-gray-700">{part.quantity || 1}</p>
|
<p className="text-sm font-medium text-gray-700 dark:text-gray-300">{part.quantity || 1}</p>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 whitespace-nowrap text-right">
|
<td className="px-4 py-3 whitespace-nowrap text-right">
|
||||||
{part.price && part.price > 0 ? (
|
{part.price && part.price > 0 ? (
|
||||||
<p className="text-sm font-medium text-gray-700">{formatPrice(part.price)}</p>
|
<p className="text-sm font-medium text-gray-700 dark:text-gray-300">{formatPrice(part.price)}</p>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-gray-400">-</span>
|
<span className="text-gray-400 dark:text-gray-500">-</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -1325,7 +1355,7 @@ export default function BOMSummary({ config }) {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center py-12 text-gray-500">
|
<div className="text-center py-12 text-gray-500 dark:text-gray-400">
|
||||||
<p>No hardware parts required for this configuration.</p>
|
<p>No hardware parts required for this configuration.</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -1352,7 +1382,7 @@ export default function BOMSummary({ config }) {
|
|||||||
alert('Error creating share link. Please try again.');
|
alert('Error creating share link. Please try again.');
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="w-full px-6 py-3 bg-purple-600 text-white rounded-lg font-medium hover:bg-purple-700 transition-colors"
|
className="w-full px-6 py-3 bg-purple-600 dark:bg-purple-500 text-white rounded-lg font-medium hover:bg-purple-700 dark:hover:bg-purple-600 transition-colors"
|
||||||
>
|
>
|
||||||
Share Link (7 days)
|
Share Link (7 days)
|
||||||
</button>
|
</button>
|
||||||
@@ -1502,7 +1532,7 @@ export default function BOMSummary({ config }) {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={isExportingZip}
|
disabled={isExportingZip}
|
||||||
className="w-full px-6 py-3 bg-green-600 text-white rounded-lg font-medium hover:bg-green-700 transition-colors disabled:bg-gray-400 disabled:cursor-not-allowed"
|
className="w-full px-6 py-3 bg-green-600 dark:bg-green-500 text-white rounded-lg font-medium hover:bg-green-700 dark:hover:bg-green-600 transition-colors disabled:bg-gray-400 dark:disabled:bg-gray-600 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
{isExportingZip ? (
|
{isExportingZip ? (
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
@@ -1512,14 +1542,14 @@ export default function BOMSummary({ config }) {
|
|||||||
<div className="flex justify-between text-xs mb-1">
|
<div className="flex justify-between text-xs mb-1">
|
||||||
<span>{zipProgress.current}%</span>
|
<span>{zipProgress.current}%</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
|
||||||
<div
|
<div
|
||||||
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
className="bg-blue-600 dark:bg-blue-500 h-2 rounded-full transition-all duration-300"
|
||||||
style={{ width: `${zipProgress.current}%` }}
|
style={{ width: `${zipProgress.current}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{zipProgress.currentFile && (
|
{zipProgress.currentFile && (
|
||||||
<p className="text-xs text-gray-600 mt-1 truncate">{zipProgress.currentFile}</p>
|
<p className="text-xs text-gray-600 dark:text-gray-400 mt-1 truncate">{zipProgress.currentFile}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -1531,7 +1561,7 @@ export default function BOMSummary({ config }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Legacy export buttons (hidden but kept for backward compatibility) */}
|
{/* Legacy export buttons (hidden but kept for backward compatibility) */}
|
||||||
<div className="text-xs text-gray-500 text-center">
|
<div className="text-xs text-gray-500 dark:text-gray-400 text-center">
|
||||||
Export includes: Overview (Markdown), BOM (Excel), Print List (Excel), and Print Files (organized by component/color)
|
Export includes: Overview (Markdown), BOM (Excel), Print List (Excel), and Print Files (organized by component/color)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,21 +6,21 @@ export default function MainPage({ onSelectBuildType }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 py-8">
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8">
|
||||||
<div className="max-w-4xl mx-auto px-4">
|
<div className="max-w-4xl mx-auto px-4">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-12 text-center">
|
<div className="mb-12 text-center">
|
||||||
<h1 className="text-4xl font-bold text-gray-900 mb-2">
|
<h1 className="text-4xl font-bold text-gray-900 dark:text-white mb-2">
|
||||||
OSSM Configurator
|
OSSM Configurator
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-600">
|
<p className="text-gray-600 dark:text-gray-300">
|
||||||
Configure your Open Source Sex Machine
|
Configure your Open Source Sex Machine
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Build Type Selection */}
|
{/* Build Type Selection */}
|
||||||
<div className="bg-white rounded-lg shadow-lg p-8">
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-8">
|
||||||
<h2 className="text-2xl font-bold text-gray-900 mb-6 text-center">
|
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-6 text-center">
|
||||||
Select Your Build Type
|
Select Your Build Type
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
@@ -28,11 +28,11 @@ export default function MainPage({ onSelectBuildType }) {
|
|||||||
{/* New Build - RAD Kit */}
|
{/* New Build - RAD Kit */}
|
||||||
<button
|
<button
|
||||||
onClick={() => handleSelect('rad-kit')}
|
onClick={() => handleSelect('rad-kit')}
|
||||||
className="flex flex-col items-center p-6 border-2 border-gray-200 rounded-lg hover:border-blue-500 hover:bg-blue-50 transition-all duration-200 group"
|
className="flex flex-col items-center p-6 border-2 border-gray-200 dark:border-gray-700 rounded-lg hover:border-blue-500 dark:hover:border-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/20 transition-all duration-200 group"
|
||||||
>
|
>
|
||||||
<div className="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center mb-4 group-hover:bg-blue-200 transition-colors">
|
<div className="w-16 h-16 bg-blue-100 dark:bg-blue-900/30 rounded-full flex items-center justify-center mb-4 group-hover:bg-blue-200 dark:group-hover:bg-blue-900/50 transition-colors">
|
||||||
<svg
|
<svg
|
||||||
className="w-8 h-8 text-blue-600"
|
className="w-8 h-8 text-blue-600 dark:text-blue-400"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@@ -45,13 +45,13 @@ export default function MainPage({ onSelectBuildType }) {
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-xl font-semibold text-gray-900 mb-2">
|
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">
|
||||||
New Build
|
New Build
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm font-medium text-blue-600 mb-3">
|
<p className="text-sm font-medium text-blue-600 dark:text-blue-400 mb-3">
|
||||||
Kit from RAD
|
Kit from RAD
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-gray-600 text-center">
|
<p className="text-sm text-gray-600 dark:text-gray-300 text-center">
|
||||||
Pre-configured kit with all required parts. Jump straight to the summary.
|
Pre-configured kit with all required parts. Jump straight to the summary.
|
||||||
</p>
|
</p>
|
||||||
</button>
|
</button>
|
||||||
@@ -59,11 +59,11 @@ export default function MainPage({ onSelectBuildType }) {
|
|||||||
{/* New Build - Self Source */}
|
{/* New Build - Self Source */}
|
||||||
<button
|
<button
|
||||||
onClick={() => handleSelect('self-source')}
|
onClick={() => handleSelect('self-source')}
|
||||||
className="flex flex-col items-center p-6 border-2 border-gray-200 rounded-lg hover:border-green-500 hover:bg-green-50 transition-all duration-200 group"
|
className="flex flex-col items-center p-6 border-2 border-gray-200 dark:border-gray-700 rounded-lg hover:border-green-500 dark:hover:border-green-400 hover:bg-green-50 dark:hover:bg-green-900/20 transition-all duration-200 group"
|
||||||
>
|
>
|
||||||
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mb-4 group-hover:bg-green-200 transition-colors">
|
<div className="w-16 h-16 bg-green-100 dark:bg-green-900/30 rounded-full flex items-center justify-center mb-4 group-hover:bg-green-200 dark:group-hover:bg-green-900/50 transition-colors">
|
||||||
<svg
|
<svg
|
||||||
className="w-8 h-8 text-green-600"
|
className="w-8 h-8 text-green-600 dark:text-green-400"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@@ -76,13 +76,13 @@ export default function MainPage({ onSelectBuildType }) {
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-xl font-semibold text-gray-900 mb-2">
|
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">
|
||||||
New Build
|
New Build
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm font-medium text-green-600 mb-3">
|
<p className="text-sm font-medium text-green-600 dark:text-green-400 mb-3">
|
||||||
Self Source
|
Self Source
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-gray-600 text-center">
|
<p className="text-sm text-gray-600 dark:text-gray-300 text-center">
|
||||||
Go through the full wizard to select and customize all components.
|
Go through the full wizard to select and customize all components.
|
||||||
</p>
|
</p>
|
||||||
</button>
|
</button>
|
||||||
@@ -90,11 +90,11 @@ export default function MainPage({ onSelectBuildType }) {
|
|||||||
{/* Upgrade */}
|
{/* Upgrade */}
|
||||||
<button
|
<button
|
||||||
onClick={() => handleSelect('upgrade')}
|
onClick={() => handleSelect('upgrade')}
|
||||||
className="flex flex-col items-center p-6 border-2 border-gray-200 rounded-lg hover:border-purple-500 hover:bg-purple-50 transition-all duration-200 group"
|
className="flex flex-col items-center p-6 border-2 border-gray-200 dark:border-gray-700 rounded-lg hover:border-purple-500 dark:hover:border-purple-400 hover:bg-purple-50 dark:hover:bg-purple-900/20 transition-all duration-200 group"
|
||||||
>
|
>
|
||||||
<div className="w-16 h-16 bg-purple-100 rounded-full flex items-center justify-center mb-4 group-hover:bg-purple-200 transition-colors">
|
<div className="w-16 h-16 bg-purple-100 dark:bg-purple-900/30 rounded-full flex items-center justify-center mb-4 group-hover:bg-purple-200 dark:group-hover:bg-purple-900/50 transition-colors">
|
||||||
<svg
|
<svg
|
||||||
className="w-8 h-8 text-purple-600"
|
className="w-8 h-8 text-purple-600 dark:text-purple-400"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@@ -107,13 +107,13 @@ export default function MainPage({ onSelectBuildType }) {
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-xl font-semibold text-gray-900 mb-2">
|
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">
|
||||||
Upgrade / Mod
|
Upgrade / Mod
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm font-medium text-purple-600 mb-3">
|
<p className="text-sm font-medium text-purple-600 dark:text-purple-400 mb-3">
|
||||||
Add Modifications
|
Add Modifications
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-gray-600 text-center">
|
<p className="text-sm text-gray-600 dark:text-gray-300 text-center">
|
||||||
Browse and select upgrade components and modifications for your existing build.
|
Browse and select upgrade components and modifications for your existing build.
|
||||||
</p>
|
</p>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
45
website/src/components/ThemeToggle.jsx
Normal file
45
website/src/components/ThemeToggle.jsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { useTheme } from '../contexts/ThemeContext';
|
||||||
|
|
||||||
|
export default function ThemeToggle() {
|
||||||
|
const { theme, toggleTheme } = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={toggleTheme}
|
||||||
|
className="fixed top-4 right-4 z-50 p-3 rounded-full bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 shadow-lg hover:shadow-xl transition-all duration-200 hover:scale-110"
|
||||||
|
aria-label="Toggle theme"
|
||||||
|
>
|
||||||
|
{theme === 'dark' ? (
|
||||||
|
// Sun icon for light mode
|
||||||
|
<svg
|
||||||
|
className="w-6 h-6 text-yellow-500"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
// Moon icon for dark mode
|
||||||
|
<svg
|
||||||
|
className="w-6 h-6 text-gray-700"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -158,14 +158,14 @@ export default function Wizard({ buildType = 'self-source', initialConfig, updat
|
|||||||
}, [buildType, currentStep]);
|
}, [buildType, currentStep]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 py-8">
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8">
|
||||||
<div className="max-w-4xl mx-auto px-4">
|
<div className="max-w-4xl mx-auto px-4">
|
||||||
{/* Back Button */}
|
{/* Back Button */}
|
||||||
{onBackToMain && (
|
{onBackToMain && (
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<button
|
<button
|
||||||
onClick={onBackToMain}
|
onClick={onBackToMain}
|
||||||
className="text-blue-600 hover:text-blue-800 font-medium flex items-center gap-2"
|
className="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 font-medium flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
className="w-5 h-5"
|
className="w-5 h-5"
|
||||||
@@ -187,17 +187,17 @@ export default function Wizard({ buildType = 'self-source', initialConfig, updat
|
|||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-8 text-center">
|
<div className="mb-8 text-center">
|
||||||
<h1 className="text-4xl font-bold text-gray-900 mb-2">
|
<h1 className="text-4xl font-bold text-gray-900 dark:text-white mb-2">
|
||||||
OSSM Configurator
|
OSSM Configurator
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-600">
|
<p className="text-gray-600 dark:text-gray-300">
|
||||||
{buildType === 'upgrade'
|
{buildType === 'upgrade'
|
||||||
? 'Select upgrade components and modifications'
|
? 'Select upgrade components and modifications'
|
||||||
: 'Configure your Open Source Sex Machine'}
|
: 'Configure your Open Source Sex Machine'}
|
||||||
</p>
|
</p>
|
||||||
{buildType === 'upgrade' && (
|
{buildType === 'upgrade' && (
|
||||||
<div className="mt-4 bg-purple-50 border border-purple-200 rounded-lg p-3 inline-block">
|
<div className="mt-4 bg-purple-50 dark:bg-purple-900/30 border border-purple-200 dark:border-purple-700 rounded-lg p-3 inline-block">
|
||||||
<p className="text-purple-800 text-sm font-medium">
|
<p className="text-purple-800 dark:text-purple-300 text-sm font-medium">
|
||||||
Upgrade Mode: Only modification and upgrade components are shown
|
Upgrade Mode: Only modification and upgrade components are shown
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -214,10 +214,10 @@ export default function Wizard({ buildType = 'self-source', initialConfig, updat
|
|||||||
onClick={() => goToStep(index)}
|
onClick={() => goToStep(index)}
|
||||||
className={`w-10 h-10 rounded-full flex items-center justify-center font-semibold flex-shrink-0 z-10 ${
|
className={`w-10 h-10 rounded-full flex items-center justify-center font-semibold flex-shrink-0 z-10 ${
|
||||||
index === currentStep
|
index === currentStep
|
||||||
? 'bg-blue-600 text-white'
|
? 'bg-blue-600 dark:bg-blue-500 text-white'
|
||||||
: index < currentStep
|
: index < currentStep
|
||||||
? 'bg-green-500 text-white'
|
? 'bg-green-500 dark:bg-green-600 text-white'
|
||||||
: 'bg-gray-200 text-gray-500'
|
: 'bg-gray-200 dark:bg-gray-700 text-gray-500 dark:text-gray-400'
|
||||||
} ${
|
} ${
|
||||||
index <= currentStep
|
index <= currentStep
|
||||||
? 'cursor-pointer hover:opacity-80'
|
? 'cursor-pointer hover:opacity-80'
|
||||||
@@ -231,7 +231,7 @@ export default function Wizard({ buildType = 'self-source', initialConfig, updat
|
|||||||
{index < filteredSteps.length - 1 && (
|
{index < filteredSteps.length - 1 && (
|
||||||
<div
|
<div
|
||||||
className={`absolute top-5 left-1/2 h-1 ${
|
className={`absolute top-5 left-1/2 h-1 ${
|
||||||
index < currentStep ? 'bg-green-500' : 'bg-gray-200'
|
index < currentStep ? 'bg-green-500 dark:bg-green-600' : 'bg-gray-200 dark:bg-gray-700'
|
||||||
}`}
|
}`}
|
||||||
style={{
|
style={{
|
||||||
width: 'calc(100% - 40px)',
|
width: 'calc(100% - 40px)',
|
||||||
@@ -244,8 +244,8 @@ export default function Wizard({ buildType = 'self-source', initialConfig, updat
|
|||||||
onClick={() => goToStep(index)}
|
onClick={() => goToStep(index)}
|
||||||
className={`mt-2 text-sm font-medium text-center ${
|
className={`mt-2 text-sm font-medium text-center ${
|
||||||
index <= currentStep
|
index <= currentStep
|
||||||
? 'text-blue-600 cursor-pointer hover:text-blue-800'
|
? 'text-blue-600 dark:text-blue-400 cursor-pointer hover:text-blue-800 dark:hover:text-blue-300'
|
||||||
: 'text-gray-400 cursor-not-allowed'
|
: 'text-gray-400 dark:text-gray-500 cursor-not-allowed'
|
||||||
}`}
|
}`}
|
||||||
disabled={!canNavigateToStep(index)}
|
disabled={!canNavigateToStep(index)}
|
||||||
>
|
>
|
||||||
@@ -257,7 +257,7 @@ export default function Wizard({ buildType = 'self-source', initialConfig, updat
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Step Content */}
|
{/* Step Content */}
|
||||||
<div className="bg-white rounded-lg shadow-lg p-6 md:p-8 mb-6">
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6 md:p-8 mb-6">
|
||||||
<CurrentStepComponent
|
<CurrentStepComponent
|
||||||
config={config}
|
config={config}
|
||||||
updateConfig={updateConfig}
|
updateConfig={updateConfig}
|
||||||
@@ -275,8 +275,8 @@ export default function Wizard({ buildType = 'self-source', initialConfig, updat
|
|||||||
disabled={currentStep === 0}
|
disabled={currentStep === 0}
|
||||||
className={`px-6 py-2 rounded-lg font-medium ${
|
className={`px-6 py-2 rounded-lg font-medium ${
|
||||||
currentStep === 0
|
currentStep === 0
|
||||||
? 'bg-gray-200 text-gray-400 cursor-not-allowed'
|
? 'bg-gray-200 dark:bg-gray-700 text-gray-400 dark:text-gray-500 cursor-not-allowed'
|
||||||
: 'bg-gray-600 text-white hover:bg-gray-700'
|
: 'bg-gray-600 dark:bg-gray-700 text-white hover:bg-gray-700 dark:hover:bg-gray-600'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Previous
|
Previous
|
||||||
@@ -287,8 +287,8 @@ export default function Wizard({ buildType = 'self-source', initialConfig, updat
|
|||||||
disabled={!canProceedToNextStep()}
|
disabled={!canProceedToNextStep()}
|
||||||
className={`px-6 py-2 rounded-lg font-medium ${
|
className={`px-6 py-2 rounded-lg font-medium ${
|
||||||
canProceedToNextStep()
|
canProceedToNextStep()
|
||||||
? 'bg-blue-600 text-white hover:bg-blue-700'
|
? 'bg-blue-600 dark:bg-blue-500 text-white hover:bg-blue-700 dark:hover:bg-blue-600'
|
||||||
: 'bg-gray-200 text-gray-400 cursor-not-allowed'
|
: 'bg-gray-200 dark:bg-gray-700 text-gray-400 dark:text-gray-500 cursor-not-allowed'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Next
|
Next
|
||||||
|
|||||||
@@ -11,15 +11,15 @@ export default function ColorsStep({ config, updateConfig }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-bold mb-4">Select Colors</h2>
|
<h2 className="text-2xl font-bold mb-4 text-gray-900 dark:text-white">Select Colors</h2>
|
||||||
<p className="text-gray-600 mb-6">
|
<p className="text-gray-600 dark:text-gray-300 mb-6">
|
||||||
Choose primary and accent colors for your OSSM build.
|
Choose primary and accent colors for your OSSM build.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
{/* Primary Color */}
|
{/* Primary Color */}
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-xl font-semibold mb-4">Primary Color</h3>
|
<h3 className="text-xl font-semibold mb-4 text-gray-900 dark:text-white">Primary Color</h3>
|
||||||
<div className="grid grid-cols-3 md:grid-cols-6 gap-4">
|
<div className="grid grid-cols-3 md:grid-cols-6 gap-4">
|
||||||
{partsData.colors.primary.map((color) => (
|
{partsData.colors.primary.map((color) => (
|
||||||
<button
|
<button
|
||||||
@@ -27,19 +27,19 @@ export default function ColorsStep({ config, updateConfig }) {
|
|||||||
onClick={() => handlePrimaryColorSelect(color)}
|
onClick={() => handlePrimaryColorSelect(color)}
|
||||||
className={`flex flex-col items-center p-4 border-2 rounded-lg transition-all ${
|
className={`flex flex-col items-center p-4 border-2 rounded-lg transition-all ${
|
||||||
config.primaryColor === color.id
|
config.primaryColor === color.id
|
||||||
? 'border-blue-600 bg-blue-50'
|
? 'border-blue-600 dark:border-blue-500 bg-blue-50 dark:bg-blue-900/30'
|
||||||
: 'border-gray-200 hover:border-gray-300'
|
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="w-16 h-16 rounded-full mb-2 border-2 border-gray-300"
|
className="w-16 h-16 rounded-full mb-2 border-2 border-gray-300 dark:border-gray-600"
|
||||||
style={{ backgroundColor: color.hex }}
|
style={{ backgroundColor: color.hex }}
|
||||||
/>
|
/>
|
||||||
<span className="text-sm font-medium text-gray-700">
|
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
{color.name}
|
{color.name}
|
||||||
</span>
|
</span>
|
||||||
{config.primaryColor === color.id && (
|
{config.primaryColor === color.id && (
|
||||||
<div className="mt-1 w-5 h-5 bg-blue-600 rounded-full flex items-center justify-center">
|
<div className="mt-1 w-5 h-5 bg-blue-600 dark:bg-blue-500 rounded-full flex items-center justify-center">
|
||||||
<svg
|
<svg
|
||||||
className="w-3 h-3 text-white"
|
className="w-3 h-3 text-white"
|
||||||
fill="none"
|
fill="none"
|
||||||
@@ -62,7 +62,7 @@ export default function ColorsStep({ config, updateConfig }) {
|
|||||||
|
|
||||||
{/* Accent Color */}
|
{/* Accent Color */}
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-xl font-semibold mb-4">Accent Color</h3>
|
<h3 className="text-xl font-semibold mb-4 text-gray-900 dark:text-white">Accent Color</h3>
|
||||||
<div className="grid grid-cols-3 md:grid-cols-6 gap-4">
|
<div className="grid grid-cols-3 md:grid-cols-6 gap-4">
|
||||||
{partsData.colors.accent.map((color) => (
|
{partsData.colors.accent.map((color) => (
|
||||||
<button
|
<button
|
||||||
@@ -70,19 +70,19 @@ export default function ColorsStep({ config, updateConfig }) {
|
|||||||
onClick={() => handleAccentColorSelect(color)}
|
onClick={() => handleAccentColorSelect(color)}
|
||||||
className={`flex flex-col items-center p-4 border-2 rounded-lg transition-all ${
|
className={`flex flex-col items-center p-4 border-2 rounded-lg transition-all ${
|
||||||
config.accentColor === color.id
|
config.accentColor === color.id
|
||||||
? 'border-blue-600 bg-blue-50'
|
? 'border-blue-600 dark:border-blue-500 bg-blue-50 dark:bg-blue-900/30'
|
||||||
: 'border-gray-200 hover:border-gray-300'
|
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="w-16 h-16 rounded-full mb-2 border-2 border-gray-300"
|
className="w-16 h-16 rounded-full mb-2 border-2 border-gray-300 dark:border-gray-600"
|
||||||
style={{ backgroundColor: color.hex }}
|
style={{ backgroundColor: color.hex }}
|
||||||
/>
|
/>
|
||||||
<span className="text-sm font-medium text-gray-700">
|
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
{color.name}
|
{color.name}
|
||||||
</span>
|
</span>
|
||||||
{config.accentColor === color.id && (
|
{config.accentColor === color.id && (
|
||||||
<div className="mt-1 w-5 h-5 bg-blue-600 rounded-full flex items-center justify-center">
|
<div className="mt-1 w-5 h-5 bg-blue-600 dark:bg-blue-500 rounded-full flex items-center justify-center">
|
||||||
<svg
|
<svg
|
||||||
className="w-3 h-3 text-white"
|
className="w-3 h-3 text-white"
|
||||||
fill="none"
|
fill="none"
|
||||||
|
|||||||
@@ -18,15 +18,15 @@ export default function MotorStep({ config, updateConfig }) {
|
|||||||
onClick={() => handleSelect(motor)}
|
onClick={() => handleSelect(motor)}
|
||||||
className={`${isSlightlyLarger ? 'p-5' : 'p-4'} border-2 rounded-lg text-left transition-all ${
|
className={`${isSlightlyLarger ? 'p-5' : 'p-4'} border-2 rounded-lg text-left transition-all ${
|
||||||
selectedMotorId === motor.id
|
selectedMotorId === motor.id
|
||||||
? 'border-blue-600 bg-blue-50 shadow-lg'
|
? 'border-blue-600 dark:border-blue-500 bg-blue-50 dark:bg-blue-900/30 shadow-lg'
|
||||||
: motor.recommended
|
: motor.recommended
|
||||||
? 'border-green-500 bg-green-50 hover:border-green-600 hover:bg-green-100'
|
? 'border-green-500 dark:border-green-600 bg-green-50 dark:bg-green-900/30 hover:border-green-600 dark:hover:border-green-500 hover:bg-green-100 dark:hover:bg-green-900/40'
|
||||||
: 'border-gray-200 hover:border-gray-300 hover:bg-gray-50'
|
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700/50'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{motor.recommended && (
|
{motor.recommended && (
|
||||||
<div className="mb-3 flex items-center gap-2">
|
<div className="mb-3 flex items-center gap-2">
|
||||||
<span className="inline-flex items-center px-3 py-1 text-xs font-semibold text-green-800 bg-green-200 rounded-full">
|
<span className="inline-flex items-center px-3 py-1 text-xs font-semibold text-green-800 dark:text-green-300 bg-green-200 dark:bg-green-900/50 rounded-full">
|
||||||
⭐ Recommended
|
⭐ Recommended
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -36,7 +36,7 @@ export default function MotorStep({ config, updateConfig }) {
|
|||||||
<img
|
<img
|
||||||
src={motor.image}
|
src={motor.image}
|
||||||
alt={motor.name}
|
alt={motor.name}
|
||||||
className={`${isSlightlyLarger ? 'h-32 w-32' : 'h-24 w-24'} object-contain rounded-lg bg-gray-100`}
|
className={`${isSlightlyLarger ? 'h-32 w-32' : 'h-24 w-24'} object-contain rounded-lg bg-gray-100 dark:bg-gray-700`}
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
e.target.style.display = 'none';
|
e.target.style.display = 'none';
|
||||||
}}
|
}}
|
||||||
@@ -44,11 +44,11 @@ export default function MotorStep({ config, updateConfig }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex items-start justify-between mb-2">
|
<div className="flex items-start justify-between mb-2">
|
||||||
<h3 className={`${isSlightlyLarger ? 'text-lg' : 'text-base'} font-semibold text-gray-900`}>
|
<h3 className={`${isSlightlyLarger ? 'text-lg' : 'text-base'} font-semibold text-gray-900 dark:text-white`}>
|
||||||
{motor.name}
|
{motor.name}
|
||||||
</h3>
|
</h3>
|
||||||
{selectedMotorId === motor.id && (
|
{selectedMotorId === motor.id && (
|
||||||
<div className="w-6 h-6 bg-blue-600 rounded-full flex items-center justify-center flex-shrink-0">
|
<div className="w-6 h-6 bg-blue-600 dark:bg-blue-500 rounded-full flex items-center justify-center flex-shrink-0">
|
||||||
<svg
|
<svg
|
||||||
className="w-4 h-4 text-white"
|
className="w-4 h-4 text-white"
|
||||||
fill="none"
|
fill="none"
|
||||||
@@ -65,29 +65,29 @@ export default function MotorStep({ config, updateConfig }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className={`${isSlightlyLarger ? 'text-sm' : 'text-sm'} text-gray-600 mb-3`}>{motor.description}</p>
|
<p className={`${isSlightlyLarger ? 'text-sm' : 'text-sm'} text-gray-600 dark:text-gray-300 mb-3`}>{motor.description}</p>
|
||||||
<div className={`flex ${isSlightlyLarger ? 'gap-4' : 'gap-3'} text-sm`}>
|
<div className={`flex ${isSlightlyLarger ? 'gap-4' : 'gap-3'} text-sm`}>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-gray-500">Speed:</span>{' '}
|
<span className="text-gray-500 dark:text-gray-400">Speed:</span>{' '}
|
||||||
<span className="font-medium">{motor.speed}</span>
|
<span className="font-medium text-gray-900 dark:text-white">{motor.speed}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-gray-500">Wattage:</span>{' '}
|
<span className="text-gray-500 dark:text-gray-400">Wattage:</span>{' '}
|
||||||
<span className="font-medium">{motor.wattage}</span>
|
<span className="font-medium text-gray-900 dark:text-white">{motor.wattage}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-gray-500">Gear Count:</span>{' '}
|
<span className="text-gray-500 dark:text-gray-400">Gear Count:</span>{' '}
|
||||||
<span className="font-medium">{motor.gear_count}</span>
|
<span className="font-medium text-gray-900 dark:text-white">{motor.gear_count}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={`${isSlightlyLarger ? 'mt-3' : 'mt-2'} flex items-center justify-between`}>
|
<div className={`${isSlightlyLarger ? 'mt-3' : 'mt-2'} flex items-center justify-between`}>
|
||||||
<div className={`${isSlightlyLarger ? 'text-lg' : 'text-lg'} font-bold text-blue-600`}>
|
<div className={`${isSlightlyLarger ? 'text-lg' : 'text-lg'} font-bold text-blue-600 dark:text-blue-400`}>
|
||||||
{formatPrice(motor.price)}
|
{formatPrice(motor.price)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{motor.links && motor.links.length > 0 && (
|
{motor.links && motor.links.length > 0 && (
|
||||||
<div className={`${isSlightlyLarger ? 'mt-3' : 'mt-2'} pt-3 border-t border-gray-200`}>
|
<div className={`${isSlightlyLarger ? 'mt-3' : 'mt-2'} pt-3 border-t border-gray-200 dark:border-gray-700`}>
|
||||||
<p className="text-xs text-gray-500 mb-2">Buy from:</p>
|
<p className="text-xs text-gray-500 dark:text-gray-400 mb-2">Buy from:</p>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{motor.links.map((link, index) => (
|
{motor.links.map((link, index) => (
|
||||||
<a
|
<a
|
||||||
@@ -95,7 +95,7 @@ export default function MotorStep({ config, updateConfig }) {
|
|||||||
href={link.link}
|
href={link.link}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="inline-flex items-center px-3 py-1.5 text-xs font-medium text-blue-700 bg-blue-50 border border-blue-200 rounded-md hover:bg-blue-100 hover:text-blue-800 transition-colors"
|
className="inline-flex items-center px-3 py-1.5 text-xs font-medium text-blue-700 dark:text-blue-300 bg-blue-50 dark:bg-blue-900/30 border border-blue-200 dark:border-blue-800 rounded-md hover:bg-blue-100 dark:hover:bg-blue-900/50 hover:text-blue-800 dark:hover:text-blue-200 transition-colors"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
@@ -122,8 +122,8 @@ export default function MotorStep({ config, updateConfig }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-bold mb-4">Select Motor</h2>
|
<h2 className="text-2xl font-bold mb-4 text-gray-900 dark:text-white">Select Motor</h2>
|
||||||
<p className="text-gray-600 mb-6">
|
<p className="text-gray-600 dark:text-gray-300 mb-6">
|
||||||
Choose the stepper motor for your OSSM build.
|
Choose the stepper motor for your OSSM build.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -136,7 +136,7 @@ export default function MotorStep({ config, updateConfig }) {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-semibold mb-4 text-gray-700">Recommended Options</h3>
|
<h3 className="text-lg font-semibold mb-4 text-gray-700 dark:text-gray-300">Recommended Options</h3>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
{recommendedMotors.map((motor) => renderMotorCard(motor, true, false))}
|
{recommendedMotors.map((motor) => renderMotorCard(motor, true, false))}
|
||||||
</div>
|
</div>
|
||||||
@@ -148,7 +148,7 @@ export default function MotorStep({ config, updateConfig }) {
|
|||||||
{/* Other Motors - Smaller Grid */}
|
{/* Other Motors - Smaller Grid */}
|
||||||
{otherMotors.length > 0 && (
|
{otherMotors.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-semibold mb-4 text-gray-700">Other Options</h3>
|
<h3 className="text-lg font-semibold mb-4 text-gray-700 dark:text-gray-300">Other Options</h3>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
{otherMotors.map((motor) => renderMotorCard(motor, false, false))}
|
{otherMotors.map((motor) => renderMotorCard(motor, false, false))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -169,8 +169,8 @@ export default function OptionsStep({ config, updateConfig, buildType }) {
|
|||||||
onClick={() => handleOptionClick(option, mainSectionId, subSectionId)}
|
onClick={() => handleOptionClick(option, mainSectionId, subSectionId)}
|
||||||
className={`p-4 border-2 rounded-lg text-left transition-all w-full ${
|
className={`p-4 border-2 rounded-lg text-left transition-all w-full ${
|
||||||
isSelected
|
isSelected
|
||||||
? 'border-blue-600 bg-blue-50'
|
? 'border-blue-600 dark:border-blue-500 bg-blue-50 dark:bg-blue-900/30'
|
||||||
: 'border-gray-200 hover:border-gray-300 hover:bg-gray-50'
|
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700/50'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{option.image && (
|
{option.image && (
|
||||||
@@ -178,7 +178,7 @@ export default function OptionsStep({ config, updateConfig, buildType }) {
|
|||||||
<img
|
<img
|
||||||
src={option.image}
|
src={option.image}
|
||||||
alt={option.name}
|
alt={option.name}
|
||||||
className="h-48 w-48 object-contain rounded-lg bg-gray-100"
|
className="h-48 w-48 object-contain rounded-lg bg-gray-100 dark:bg-gray-700"
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
e.target.style.display = 'none';
|
e.target.style.display = 'none';
|
||||||
}}
|
}}
|
||||||
@@ -187,15 +187,15 @@ export default function OptionsStep({ config, updateConfig, buildType }) {
|
|||||||
)}
|
)}
|
||||||
<div className="flex items-start justify-between mb-2">
|
<div className="flex items-start justify-between mb-2">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h4 className="font-semibold text-gray-900 mb-1">
|
<h4 className="font-semibold text-gray-900 dark:text-white mb-1">
|
||||||
{option.name}
|
{option.name}
|
||||||
</h4>
|
</h4>
|
||||||
{option.description && (
|
{option.description && (
|
||||||
<p className="text-sm text-gray-600">{option.description}</p>
|
<p className="text-sm text-gray-600 dark:text-gray-300">{option.description}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{isSelected && (
|
{isSelected && (
|
||||||
<div className="w-6 h-6 bg-blue-600 rounded-full flex items-center justify-center flex-shrink-0 ml-2">
|
<div className="w-6 h-6 bg-blue-600 dark:bg-blue-500 rounded-full flex items-center justify-center flex-shrink-0 ml-2">
|
||||||
{isMultiSelect ? (
|
{isMultiSelect ? (
|
||||||
<span className="text-white text-sm font-bold">✓</span>
|
<span className="text-white text-sm font-bold">✓</span>
|
||||||
) : (
|
) : (
|
||||||
@@ -219,14 +219,14 @@ export default function OptionsStep({ config, updateConfig, buildType }) {
|
|||||||
<div className="flex flex-wrap gap-4 text-sm mt-3">
|
<div className="flex flex-wrap gap-4 text-sm mt-3">
|
||||||
{option.filamentEstimate && (
|
{option.filamentEstimate && (
|
||||||
<div>
|
<div>
|
||||||
<span className="text-gray-500">Filament:</span>{' '}
|
<span className="text-gray-500 dark:text-gray-400">Filament:</span>{' '}
|
||||||
<span className="font-medium">{option.filamentEstimate}</span>
|
<span className="font-medium text-gray-900 dark:text-white">{option.filamentEstimate}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{option.hardwareCost !== undefined && (
|
{option.hardwareCost !== undefined && (
|
||||||
<div>
|
<div>
|
||||||
<span className="text-gray-500">Hardware:</span>{' '}
|
<span className="text-gray-500 dark:text-gray-400">Hardware:</span>{' '}
|
||||||
<span className="font-medium">
|
<span className="font-medium text-gray-900 dark:text-white">
|
||||||
{formatPrice(option.hardwareCost)}
|
{formatPrice(option.hardwareCost)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -243,16 +243,16 @@ export default function OptionsStep({ config, updateConfig, buildType }) {
|
|||||||
const isExpanded = expandedSubSections[subSectionKey] !== false && (!hasSelection || expandedSubSections[subSectionKey] === true);
|
const isExpanded = expandedSubSections[subSectionKey] !== false && (!hasSelection || expandedSubSections[subSectionKey] === true);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={subSectionId} className="border border-gray-200 rounded-lg overflow-hidden">
|
<div key={subSectionId} className="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||||
<button
|
<button
|
||||||
onClick={() => toggleSubSection(subSectionKey)}
|
onClick={() => toggleSubSection(subSectionKey)}
|
||||||
className="w-full px-4 py-3 bg-gray-50 hover:bg-gray-100 transition-colors flex items-center justify-between"
|
className="w-full px-4 py-3 bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors flex items-center justify-between"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<h4 className="font-semibold text-gray-800">{subSection.title}</h4>
|
<h4 className="font-semibold text-gray-800 dark:text-gray-200">{subSection.title}</h4>
|
||||||
{hasSelection && (
|
{hasSelection && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-4 h-4 bg-green-500 rounded-full flex items-center justify-center">
|
<div className="w-4 h-4 bg-green-500 dark:bg-green-600 rounded-full flex items-center justify-center">
|
||||||
<svg
|
<svg
|
||||||
className="w-2.5 h-2.5 text-white"
|
className="w-2.5 h-2.5 text-white"
|
||||||
fill="none"
|
fill="none"
|
||||||
@@ -267,14 +267,14 @@ export default function OptionsStep({ config, updateConfig, buildType }) {
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm text-gray-600">
|
<span className="text-sm text-gray-600 dark:text-gray-300">
|
||||||
{selectedOptions.map((opt) => opt.name).join(', ')}
|
{selectedOptions.map((opt) => opt.name).join(', ')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<svg
|
<svg
|
||||||
className={`w-4 h-4 text-gray-500 transition-transform ${
|
className={`w-4 h-4 text-gray-500 dark:text-gray-400 transition-transform ${
|
||||||
isExpanded ? 'transform rotate-180' : ''
|
isExpanded ? 'transform rotate-180' : ''
|
||||||
}`}
|
}`}
|
||||||
fill="none"
|
fill="none"
|
||||||
@@ -290,7 +290,7 @@ export default function OptionsStep({ config, updateConfig, buildType }) {
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
{isExpanded && subSection.options && subSection.options.length > 0 && (
|
{isExpanded && subSection.options && subSection.options.length > 0 && (
|
||||||
<div className="p-4 bg-white">
|
<div className="p-4 bg-white dark:bg-gray-800">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
{subSection.options.map((option) =>
|
{subSection.options.map((option) =>
|
||||||
renderOptionCard(option, mainSectionId, subSectionId, subSection, subSection.isMultiSelect)
|
renderOptionCard(option, mainSectionId, subSectionId, subSection, subSection.isMultiSelect)
|
||||||
@@ -309,25 +309,25 @@ export default function OptionsStep({ config, updateConfig, buildType }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={mainSectionId} className={`border-2 rounded-lg overflow-hidden mb-4 ${
|
<div key={mainSectionId} className={`border-2 rounded-lg overflow-hidden mb-4 ${
|
||||||
isComplete ? 'border-green-500' : 'border-gray-300'
|
isComplete ? 'border-green-500 dark:border-green-600' : 'border-gray-300 dark:border-gray-700'
|
||||||
}`}>
|
}`}>
|
||||||
<button
|
<button
|
||||||
onClick={() => toggleMainSection(mainSectionId)}
|
onClick={() => toggleMainSection(mainSectionId)}
|
||||||
className={`w-full px-6 py-4 transition-colors flex items-center justify-between ${
|
className={`w-full px-6 py-4 transition-colors flex items-center justify-between ${
|
||||||
isComplete
|
isComplete
|
||||||
? 'bg-green-50 hover:bg-green-100'
|
? 'bg-green-50 dark:bg-green-900/30 hover:bg-green-100 dark:hover:bg-green-900/40'
|
||||||
: 'bg-gray-100 hover:bg-gray-200'
|
: 'bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<h3 className={`text-xl font-bold ${
|
<h3 className={`text-xl font-bold ${
|
||||||
isComplete ? 'text-green-900' : 'text-gray-900'
|
isComplete ? 'text-green-900 dark:text-green-300' : 'text-gray-900 dark:text-white'
|
||||||
}`}>
|
}`}>
|
||||||
{mainSection.title}
|
{mainSection.title}
|
||||||
</h3>
|
</h3>
|
||||||
{isComplete && (
|
{isComplete && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-6 h-6 bg-green-500 rounded-full flex items-center justify-center">
|
<div className="w-6 h-6 bg-green-500 dark:bg-green-600 rounded-full flex items-center justify-center">
|
||||||
<svg
|
<svg
|
||||||
className="w-4 h-4 text-white"
|
className="w-4 h-4 text-white"
|
||||||
fill="none"
|
fill="none"
|
||||||
@@ -342,14 +342,14 @@ export default function OptionsStep({ config, updateConfig, buildType }) {
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm font-medium text-green-700">Complete</span>
|
<span className="text-sm font-medium text-green-700 dark:text-green-300">Complete</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<svg
|
<svg
|
||||||
className={`w-6 h-6 transition-transform ${
|
className={`w-6 h-6 transition-transform ${
|
||||||
isExpanded ? 'transform rotate-180' : ''
|
isExpanded ? 'transform rotate-180' : ''
|
||||||
} ${isComplete ? 'text-green-600' : 'text-gray-600'}`}
|
} ${isComplete ? 'text-green-600 dark:text-green-400' : 'text-gray-600 dark:text-gray-400'}`}
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@@ -363,7 +363,7 @@ export default function OptionsStep({ config, updateConfig, buildType }) {
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
<div className="p-4 space-y-4 bg-white">
|
<div className="p-4 space-y-4 bg-white dark:bg-gray-800">
|
||||||
{subSections.map(([subSectionId, subSection]) =>
|
{subSections.map(([subSectionId, subSection]) =>
|
||||||
renderSubSection(mainSectionId, subSectionId, subSection)
|
renderSubSection(mainSectionId, subSectionId, subSection)
|
||||||
)}
|
)}
|
||||||
@@ -419,18 +419,18 @@ export default function OptionsStep({ config, updateConfig, buildType }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-bold mb-4">
|
<h2 className="text-2xl font-bold mb-4 text-gray-900 dark:text-white">
|
||||||
{buildType === 'upgrade' ? 'Select Upgrades & Modifications' : 'Select Options'}
|
{buildType === 'upgrade' ? 'Select Upgrades & Modifications' : 'Select Options'}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-gray-600 mb-6">
|
<p className="text-gray-600 dark:text-gray-300 mb-6">
|
||||||
{buildType === 'upgrade'
|
{buildType === 'upgrade'
|
||||||
? 'Choose upgrade components and modifications for your existing build.'
|
? 'Choose upgrade components and modifications for your existing build.'
|
||||||
: 'Choose your preferred mounting options and accessories.'}
|
: 'Choose your preferred mounting options and accessories.'}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{sectionsToRender.length === 0 && buildType === 'upgrade' && (
|
{sectionsToRender.length === 0 && buildType === 'upgrade' && (
|
||||||
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-6">
|
<div className="bg-yellow-50 dark:bg-yellow-900/30 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4 mb-6">
|
||||||
<p className="text-yellow-800">
|
<p className="text-yellow-800 dark:text-yellow-300">
|
||||||
No upgrade components available. All components are base modules.
|
No upgrade components available. All components are base modules.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,14 +17,14 @@ export default function PowerSupplyStep({ config, updateConfig }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-bold mb-4">Select Power Supply</h2>
|
<h2 className="text-2xl font-bold mb-4 text-gray-900 dark:text-white">Select Power Supply</h2>
|
||||||
<p className="text-gray-600 mb-6">
|
<p className="text-gray-600 dark:text-gray-300 mb-6">
|
||||||
Choose a compatible power supply for your selected motor.
|
Choose a compatible power supply for your selected motor.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{selectedMotorId && (
|
{selectedMotorId && (
|
||||||
<div className="mb-4 p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
<div className="mb-4 p-4 bg-blue-50 dark:bg-blue-900/30 border border-blue-200 dark:border-blue-800 rounded-lg">
|
||||||
<p className="text-sm text-blue-800">
|
<p className="text-sm text-blue-800 dark:text-blue-300">
|
||||||
Showing power supplies compatible with:{' '}
|
Showing power supplies compatible with:{' '}
|
||||||
<span className="font-semibold">
|
<span className="font-semibold">
|
||||||
{config.motor?.name || 'Selected Motor'}
|
{config.motor?.name || 'Selected Motor'}
|
||||||
@@ -40,8 +40,8 @@ export default function PowerSupplyStep({ config, updateConfig }) {
|
|||||||
onClick={() => handleSelect(powerSupply)}
|
onClick={() => handleSelect(powerSupply)}
|
||||||
className={`p-6 border-2 rounded-lg text-left transition-all ${
|
className={`p-6 border-2 rounded-lg text-left transition-all ${
|
||||||
selectedPowerSupplyId === powerSupply.id
|
selectedPowerSupplyId === powerSupply.id
|
||||||
? 'border-blue-600 bg-blue-50'
|
? 'border-blue-600 dark:border-blue-500 bg-blue-50 dark:bg-blue-900/30'
|
||||||
: 'border-gray-200 hover:border-gray-300 hover:bg-gray-50'
|
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700/50'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{powerSupply.image && (
|
{powerSupply.image && (
|
||||||
@@ -49,7 +49,7 @@ export default function PowerSupplyStep({ config, updateConfig }) {
|
|||||||
<img
|
<img
|
||||||
src={powerSupply.image}
|
src={powerSupply.image}
|
||||||
alt={powerSupply.name}
|
alt={powerSupply.name}
|
||||||
className="h-32 w-32 object-contain rounded-lg bg-gray-100"
|
className="h-32 w-32 object-contain rounded-lg bg-gray-100 dark:bg-gray-700"
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
e.target.style.display = 'none';
|
e.target.style.display = 'none';
|
||||||
}}
|
}}
|
||||||
@@ -57,11 +57,11 @@ export default function PowerSupplyStep({ config, updateConfig }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex items-start justify-between mb-2">
|
<div className="flex items-start justify-between mb-2">
|
||||||
<h3 className="text-lg font-semibold text-gray-900">
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
{powerSupply.name}
|
{powerSupply.name}
|
||||||
</h3>
|
</h3>
|
||||||
{selectedPowerSupplyId === powerSupply.id && (
|
{selectedPowerSupplyId === powerSupply.id && (
|
||||||
<div className="w-6 h-6 bg-blue-600 rounded-full flex items-center justify-center flex-shrink-0">
|
<div className="w-6 h-6 bg-blue-600 dark:bg-blue-500 rounded-full flex items-center justify-center flex-shrink-0">
|
||||||
<svg
|
<svg
|
||||||
className="w-4 h-4 text-white"
|
className="w-4 h-4 text-white"
|
||||||
fill="none"
|
fill="none"
|
||||||
@@ -78,27 +78,27 @@ export default function PowerSupplyStep({ config, updateConfig }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-600 mb-3">
|
<p className="text-sm text-gray-600 dark:text-gray-300 mb-3">
|
||||||
{powerSupply.description}
|
{powerSupply.description}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex gap-4 text-sm">
|
<div className="flex gap-4 text-sm">
|
||||||
<div>
|
<div>
|
||||||
<span className="text-gray-500">Voltage:</span>{' '}
|
<span className="text-gray-500 dark:text-gray-400">Voltage:</span>{' '}
|
||||||
<span className="font-medium">{powerSupply.voltage}</span>
|
<span className="font-medium text-gray-900 dark:text-white">{powerSupply.voltage}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-gray-500">Current:</span>{' '}
|
<span className="text-gray-500 dark:text-gray-400">Current:</span>{' '}
|
||||||
<span className="font-medium">{powerSupply.current}</span>
|
<span className="font-medium text-gray-900 dark:text-white">{powerSupply.current}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 flex items-center justify-between">
|
<div className="mt-3 flex items-center justify-between">
|
||||||
<div className="text-lg font-bold text-blue-600">
|
<div className="text-lg font-bold text-blue-600 dark:text-blue-400">
|
||||||
{formatPrice(powerSupply.price)}
|
{formatPrice(powerSupply.price)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{powerSupply.links && powerSupply.links.length > 0 && (
|
{powerSupply.links && powerSupply.links.length > 0 && (
|
||||||
<div className="mt-3 pt-3 border-t border-gray-200">
|
<div className="mt-3 pt-3 border-t border-gray-200 dark:border-gray-700">
|
||||||
<p className="text-xs text-gray-500 mb-2">Buy from:</p>
|
<p className="text-xs text-gray-500 dark:text-gray-400 mb-2">Buy from:</p>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{powerSupply.links.map((link, index) => (
|
{powerSupply.links.map((link, index) => (
|
||||||
<a
|
<a
|
||||||
@@ -106,7 +106,7 @@ export default function PowerSupplyStep({ config, updateConfig }) {
|
|||||||
href={link.link}
|
href={link.link}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="inline-flex items-center px-3 py-1.5 text-xs font-medium text-blue-700 bg-blue-50 border border-blue-200 rounded-md hover:bg-blue-100 hover:text-blue-800 transition-colors"
|
className="inline-flex items-center px-3 py-1.5 text-xs font-medium text-blue-700 dark:text-blue-300 bg-blue-50 dark:bg-blue-900/30 border border-blue-200 dark:border-blue-800 rounded-md hover:bg-blue-100 dark:hover:bg-blue-900/50 hover:text-blue-800 dark:hover:text-blue-200 transition-colors"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
@@ -90,8 +90,8 @@ export default function RemoteStep({ config, updateConfig, buildType }) {
|
|||||||
onClick={() => handleRemoteSelect(remote.id)}
|
onClick={() => handleRemoteSelect(remote.id)}
|
||||||
className={`p-4 border-2 rounded-lg text-left transition-all w-full ${
|
className={`p-4 border-2 rounded-lg text-left transition-all w-full ${
|
||||||
isSelected
|
isSelected
|
||||||
? 'border-blue-600 bg-blue-50'
|
? 'border-blue-600 dark:border-blue-500 bg-blue-50 dark:bg-blue-900/30'
|
||||||
: 'border-gray-200 hover:border-gray-300 hover:bg-gray-50'
|
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700/50'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{imagePath && (
|
{imagePath && (
|
||||||
@@ -99,7 +99,7 @@ export default function RemoteStep({ config, updateConfig, buildType }) {
|
|||||||
<img
|
<img
|
||||||
src={imagePath}
|
src={imagePath}
|
||||||
alt={remote.name}
|
alt={remote.name}
|
||||||
className="h-48 w-48 object-contain rounded-lg bg-gray-100"
|
className="h-48 w-48 object-contain rounded-lg bg-gray-100 dark:bg-gray-700"
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
e.target.style.display = 'none';
|
e.target.style.display = 'none';
|
||||||
}}
|
}}
|
||||||
@@ -108,13 +108,13 @@ export default function RemoteStep({ config, updateConfig, buildType }) {
|
|||||||
)}
|
)}
|
||||||
<div className="flex items-start justify-between mb-2">
|
<div className="flex items-start justify-between mb-2">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h4 className="font-semibold text-gray-900 mb-1">
|
<h4 className="font-semibold text-gray-900 dark:text-white mb-1">
|
||||||
{remote.name}
|
{remote.name}
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-sm text-gray-600">{remote.description}</p>
|
<p className="text-sm text-gray-600 dark:text-gray-300">{remote.description}</p>
|
||||||
</div>
|
</div>
|
||||||
{isSelected && (
|
{isSelected && (
|
||||||
<div className="w-6 h-6 bg-blue-600 rounded-full flex items-center justify-center flex-shrink-0 ml-2">
|
<div className="w-6 h-6 bg-blue-600 dark:bg-blue-500 rounded-full flex items-center justify-center flex-shrink-0 ml-2">
|
||||||
<svg
|
<svg
|
||||||
className="w-4 h-4 text-white"
|
className="w-4 h-4 text-white"
|
||||||
fill="none"
|
fill="none"
|
||||||
@@ -143,14 +143,14 @@ export default function RemoteStep({ config, updateConfig, buildType }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-4 mb-6">
|
<div className="mt-4 mb-6">
|
||||||
<h3 className="text-lg font-semibold mb-3">PCB Purchase Source</h3>
|
<h3 className="text-lg font-semibold mb-3 text-gray-900 dark:text-white">PCB Purchase Source</h3>
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<button
|
<button
|
||||||
onClick={() => handlePCBSelect('rad')}
|
onClick={() => handlePCBSelect('rad')}
|
||||||
className={`px-4 py-2 border-2 rounded-lg transition-all ${
|
className={`px-4 py-2 border-2 rounded-lg transition-all ${
|
||||||
selectedRemotePCB === 'rad'
|
selectedRemotePCB === 'rad'
|
||||||
? 'border-blue-600 bg-blue-50 text-blue-900 font-medium'
|
? 'border-blue-600 dark:border-blue-500 bg-blue-50 dark:bg-blue-900/30 text-blue-900 dark:text-blue-300 font-medium'
|
||||||
: 'border-gray-200 hover:border-gray-300'
|
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Purchase from RAD
|
Purchase from RAD
|
||||||
@@ -159,8 +159,8 @@ export default function RemoteStep({ config, updateConfig, buildType }) {
|
|||||||
onClick={() => handlePCBSelect('pcbway')}
|
onClick={() => handlePCBSelect('pcbway')}
|
||||||
className={`px-4 py-2 border-2 rounded-lg transition-all ${
|
className={`px-4 py-2 border-2 rounded-lg transition-all ${
|
||||||
selectedRemotePCB === 'pcbway'
|
selectedRemotePCB === 'pcbway'
|
||||||
? 'border-blue-600 bg-blue-50 text-blue-900 font-medium'
|
? 'border-blue-600 dark:border-blue-500 bg-blue-50 dark:bg-blue-900/30 text-blue-900 dark:text-blue-300 font-medium'
|
||||||
: 'border-gray-200 hover:border-gray-300'
|
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Self-source with PCBWay
|
Self-source with PCBWay
|
||||||
@@ -179,21 +179,21 @@ export default function RemoteStep({ config, updateConfig, buildType }) {
|
|||||||
onClick={() => handleKnobSelect(knob)}
|
onClick={() => handleKnobSelect(knob)}
|
||||||
className={`p-4 border-2 rounded-lg text-left transition-all w-full ${
|
className={`p-4 border-2 rounded-lg text-left transition-all w-full ${
|
||||||
isSelected
|
isSelected
|
||||||
? 'border-blue-600 bg-blue-50'
|
? 'border-blue-600 dark:border-blue-500 bg-blue-50 dark:bg-blue-900/30'
|
||||||
: 'border-gray-200 hover:border-gray-300 hover:bg-gray-50'
|
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700/50'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between mb-2">
|
<div className="flex items-start justify-between mb-2">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h4 className="font-semibold text-gray-900 mb-1">
|
<h4 className="font-semibold text-gray-900 dark:text-white mb-1">
|
||||||
{knob.name}
|
{knob.name}
|
||||||
</h4>
|
</h4>
|
||||||
{knob.description && (
|
{knob.description && (
|
||||||
<p className="text-sm text-gray-600">{knob.description}</p>
|
<p className="text-sm text-gray-600 dark:text-gray-300">{knob.description}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{isSelected && (
|
{isSelected && (
|
||||||
<div className="w-6 h-6 bg-blue-600 rounded-full flex items-center justify-center flex-shrink-0 ml-2">
|
<div className="w-6 h-6 bg-blue-600 dark:bg-blue-500 rounded-full flex items-center justify-center flex-shrink-0 ml-2">
|
||||||
<svg
|
<svg
|
||||||
className="w-4 h-4 text-white"
|
className="w-4 h-4 text-white"
|
||||||
fill="none"
|
fill="none"
|
||||||
@@ -213,8 +213,8 @@ export default function RemoteStep({ config, updateConfig, buildType }) {
|
|||||||
<div className="flex flex-wrap gap-4 text-sm mt-3">
|
<div className="flex flex-wrap gap-4 text-sm mt-3">
|
||||||
{knob.filamentEstimate && (
|
{knob.filamentEstimate && (
|
||||||
<div>
|
<div>
|
||||||
<span className="text-gray-500">Filament:</span>{' '}
|
<span className="text-gray-500 dark:text-gray-400">Filament:</span>{' '}
|
||||||
<span className="font-medium">{knob.filamentEstimate}</span>
|
<span className="font-medium text-gray-900 dark:text-white">{knob.filamentEstimate}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -236,8 +236,8 @@ export default function RemoteStep({ config, updateConfig, buildType }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-bold mb-4">Select Remote Control</h2>
|
<h2 className="text-2xl font-bold mb-4 text-gray-900 dark:text-white">Select Remote Control</h2>
|
||||||
<p className="text-gray-600 mb-6">
|
<p className="text-gray-600 dark:text-gray-300 mb-6">
|
||||||
Choose your remote control system and knob option.
|
Choose your remote control system and knob option.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -254,16 +254,16 @@ export default function RemoteStep({ config, updateConfig, buildType }) {
|
|||||||
|
|
||||||
{/* Knobs Selection (only shown when remote is selected) */}
|
{/* Knobs Selection (only shown when remote is selected) */}
|
||||||
{hasRemoteSelected && availableKnobs.length > 0 && (
|
{hasRemoteSelected && availableKnobs.length > 0 && (
|
||||||
<div className="border border-gray-200 rounded-lg overflow-hidden">
|
<div className="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||||
<button
|
<button
|
||||||
onClick={() => setExpandedKnobs(!expandedKnobs)}
|
onClick={() => setExpandedKnobs(!expandedKnobs)}
|
||||||
className="w-full px-4 py-3 bg-gray-50 hover:bg-gray-100 transition-colors flex items-center justify-between"
|
className="w-full px-4 py-3 bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors flex items-center justify-between"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<h3 className="text-lg font-semibold text-gray-800">Remote Knobs</h3>
|
<h3 className="text-lg font-semibold text-gray-800 dark:text-gray-200">Remote Knobs</h3>
|
||||||
{hasKnobSelected && (
|
{hasKnobSelected && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-4 h-4 bg-green-500 rounded-full flex items-center justify-center">
|
<div className="w-4 h-4 bg-green-500 dark:bg-green-600 rounded-full flex items-center justify-center">
|
||||||
<svg
|
<svg
|
||||||
className="w-2.5 h-2.5 text-white"
|
className="w-2.5 h-2.5 text-white"
|
||||||
fill="none"
|
fill="none"
|
||||||
@@ -278,14 +278,14 @@ export default function RemoteStep({ config, updateConfig, buildType }) {
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm text-gray-600">
|
<span className="text-sm text-gray-600 dark:text-gray-300">
|
||||||
{config.remoteKnob?.name}
|
{config.remoteKnob?.name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<svg
|
<svg
|
||||||
className={`w-4 h-4 text-gray-500 transition-transform ${
|
className={`w-4 h-4 text-gray-500 dark:text-gray-400 transition-transform ${
|
||||||
expandedKnobs ? 'transform rotate-180' : ''
|
expandedKnobs ? 'transform rotate-180' : ''
|
||||||
}`}
|
}`}
|
||||||
fill="none"
|
fill="none"
|
||||||
@@ -301,7 +301,7 @@ export default function RemoteStep({ config, updateConfig, buildType }) {
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
{expandedKnobs && (
|
{expandedKnobs && (
|
||||||
<div className="p-4 bg-white">
|
<div className="p-4 bg-white dark:bg-gray-800">
|
||||||
<div className="grid grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
{availableKnobs.map((knob) => renderKnobCard(knob))}
|
{availableKnobs.map((knob) => renderKnobCard(knob))}
|
||||||
</div>
|
</div>
|
||||||
@@ -311,8 +311,8 @@ export default function RemoteStep({ config, updateConfig, buildType }) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{!hasRemoteSelected && (
|
{!hasRemoteSelected && (
|
||||||
<div className="mt-6 bg-yellow-50 border border-yellow-200 rounded-lg p-4">
|
<div className="mt-6 bg-yellow-50 dark:bg-yellow-900/30 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4">
|
||||||
<p className="text-yellow-800 text-sm">
|
<p className="text-yellow-800 dark:text-yellow-300 text-sm">
|
||||||
<strong>Note:</strong> Please select a remote control system to continue.
|
<strong>Note:</strong> Please select a remote control system to continue.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -51,8 +51,8 @@ export default function ToyMountStep({ config, updateConfig }) {
|
|||||||
onClick={() => handleOptionClick(option, subSectionId, subSection)}
|
onClick={() => handleOptionClick(option, subSectionId, subSection)}
|
||||||
className={`p-4 border-2 rounded-lg text-left transition-all w-full ${
|
className={`p-4 border-2 rounded-lg text-left transition-all w-full ${
|
||||||
isSelected
|
isSelected
|
||||||
? 'border-blue-600 bg-blue-50'
|
? 'border-blue-600 dark:border-blue-500 bg-blue-50 dark:bg-blue-900/30'
|
||||||
: 'border-gray-200 hover:border-gray-300 hover:bg-gray-50'
|
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700/50'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
@@ -61,7 +61,7 @@ export default function ToyMountStep({ config, updateConfig }) {
|
|||||||
<img
|
<img
|
||||||
src={option.image}
|
src={option.image}
|
||||||
alt={option.name}
|
alt={option.name}
|
||||||
className="h-24 w-24 object-contain rounded-lg bg-gray-100"
|
className="h-24 w-24 object-contain rounded-lg bg-gray-100 dark:bg-gray-700"
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
e.target.style.display = 'none';
|
e.target.style.display = 'none';
|
||||||
}}
|
}}
|
||||||
@@ -71,15 +71,15 @@ export default function ToyMountStep({ config, updateConfig }) {
|
|||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-start justify-between mb-2">
|
<div className="flex items-start justify-between mb-2">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h4 className="font-semibold text-gray-900 mb-1">
|
<h4 className="font-semibold text-gray-900 dark:text-white mb-1">
|
||||||
{option.name}
|
{option.name}
|
||||||
</h4>
|
</h4>
|
||||||
{option.description && (
|
{option.description && (
|
||||||
<p className="text-sm text-gray-600">{option.description}</p>
|
<p className="text-sm text-gray-600 dark:text-gray-300">{option.description}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{isSelected && (
|
{isSelected && (
|
||||||
<div className="w-6 h-6 bg-blue-600 rounded-full flex items-center justify-center flex-shrink-0 ml-2">
|
<div className="w-6 h-6 bg-blue-600 dark:bg-blue-500 rounded-full flex items-center justify-center flex-shrink-0 ml-2">
|
||||||
<span className="text-white text-sm font-bold">✓</span>
|
<span className="text-white text-sm font-bold">✓</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -87,14 +87,14 @@ export default function ToyMountStep({ config, updateConfig }) {
|
|||||||
<div className="flex flex-wrap gap-4 text-sm mt-3">
|
<div className="flex flex-wrap gap-4 text-sm mt-3">
|
||||||
{option.filamentEstimate && (
|
{option.filamentEstimate && (
|
||||||
<div>
|
<div>
|
||||||
<span className="text-gray-500">Filament:</span>{' '}
|
<span className="text-gray-500 dark:text-gray-400">Filament:</span>{' '}
|
||||||
<span className="font-medium">{option.filamentEstimate}</span>
|
<span className="font-medium text-gray-900 dark:text-white">{option.filamentEstimate}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{option.hardwareCost !== undefined && (
|
{option.hardwareCost !== undefined && (
|
||||||
<div>
|
<div>
|
||||||
<span className="text-gray-500">Hardware:</span>{' '}
|
<span className="text-gray-500 dark:text-gray-400">Hardware:</span>{' '}
|
||||||
<span className="font-medium">
|
<span className="font-medium text-gray-900 dark:text-white">
|
||||||
{formatPrice(option.hardwareCost)}
|
{formatPrice(option.hardwareCost)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -113,16 +113,16 @@ export default function ToyMountStep({ config, updateConfig }) {
|
|||||||
const isExpanded = expandedSubSections[subSectionKey] !== false && (!hasSelection || expandedSubSections[subSectionKey] === true);
|
const isExpanded = expandedSubSections[subSectionKey] !== false && (!hasSelection || expandedSubSections[subSectionKey] === true);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={subSectionId} className="border border-gray-200 rounded-lg overflow-hidden">
|
<div key={subSectionId} className="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||||
<button
|
<button
|
||||||
onClick={() => toggleSubSection(subSectionKey)}
|
onClick={() => toggleSubSection(subSectionKey)}
|
||||||
className="w-full px-4 py-3 bg-gray-50 hover:bg-gray-100 transition-colors flex items-center justify-between"
|
className="w-full px-4 py-3 bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors flex items-center justify-between"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<h4 className="font-semibold text-gray-800">{subSection.title}</h4>
|
<h4 className="font-semibold text-gray-800 dark:text-gray-200">{subSection.title}</h4>
|
||||||
{hasSelection && (
|
{hasSelection && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-4 h-4 bg-green-500 rounded-full flex items-center justify-center">
|
<div className="w-4 h-4 bg-green-500 dark:bg-green-600 rounded-full flex items-center justify-center">
|
||||||
<svg
|
<svg
|
||||||
className="w-2.5 h-2.5 text-white"
|
className="w-2.5 h-2.5 text-white"
|
||||||
fill="none"
|
fill="none"
|
||||||
@@ -137,14 +137,14 @@ export default function ToyMountStep({ config, updateConfig }) {
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm text-gray-600">
|
<span className="text-sm text-gray-600 dark:text-gray-300">
|
||||||
{selectedOptions.map((opt) => opt.name).join(', ')}
|
{selectedOptions.map((opt) => opt.name).join(', ')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<svg
|
<svg
|
||||||
className={`w-4 h-4 text-gray-500 transition-transform ${
|
className={`w-4 h-4 text-gray-500 dark:text-gray-400 transition-transform ${
|
||||||
isExpanded ? 'transform rotate-180' : ''
|
isExpanded ? 'transform rotate-180' : ''
|
||||||
}`}
|
}`}
|
||||||
fill="none"
|
fill="none"
|
||||||
@@ -160,7 +160,7 @@ export default function ToyMountStep({ config, updateConfig }) {
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
{isExpanded && subSection.options && subSection.options.length > 0 && (
|
{isExpanded && subSection.options && subSection.options.length > 0 && (
|
||||||
<div className="p-4 space-y-3 bg-white">
|
<div className="p-4 space-y-3 bg-white dark:bg-gray-800">
|
||||||
{subSection.options.map((option) =>
|
{subSection.options.map((option) =>
|
||||||
renderOptionCard(option, subSectionId, subSection)
|
renderOptionCard(option, subSectionId, subSection)
|
||||||
)}
|
)}
|
||||||
@@ -176,8 +176,8 @@ export default function ToyMountStep({ config, updateConfig }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-bold mb-4">Select Toy Mounts</h2>
|
<h2 className="text-2xl font-bold mb-4 text-gray-900 dark:text-white">Select Toy Mounts</h2>
|
||||||
<p className="text-gray-600 mb-6">
|
<p className="text-gray-600 dark:text-gray-300 mb-6">
|
||||||
Choose your preferred toy mount options. You can select multiple options from different categories.
|
Choose your preferred toy mount options. You can select multiple options from different categories.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -190,8 +190,8 @@ export default function ToyMountStep({ config, updateConfig }) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{!hasSelection && (
|
{!hasSelection && (
|
||||||
<div className="mt-6 bg-yellow-50 border border-yellow-200 rounded-lg p-4">
|
<div className="mt-6 bg-yellow-50 dark:bg-yellow-900/30 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4">
|
||||||
<p className="text-yellow-800 text-sm">
|
<p className="text-yellow-800 dark:text-yellow-300 text-sm">
|
||||||
<strong>Note:</strong> At least one toy mount option is recommended for your build.
|
<strong>Note:</strong> At least one toy mount option is recommended for your build.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
75
website/src/contexts/ThemeContext.jsx
Normal file
75
website/src/contexts/ThemeContext.jsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { createContext, useContext, useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
const ThemeContext = createContext();
|
||||||
|
|
||||||
|
export const useTheme = () => {
|
||||||
|
const context = useContext(ThemeContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useTheme must be used within a ThemeProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ThemeProvider = ({ children }) => {
|
||||||
|
const [theme, setTheme] = useState(() => {
|
||||||
|
// Check localStorage first
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const savedTheme = localStorage.getItem('theme');
|
||||||
|
if (savedTheme === 'light' || savedTheme === 'dark') {
|
||||||
|
return savedTheme;
|
||||||
|
}
|
||||||
|
// Check system preference only if no saved preference
|
||||||
|
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||||
|
return 'dark';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 'light';
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Apply theme to document
|
||||||
|
if (typeof document !== 'undefined') {
|
||||||
|
if (theme === 'dark') {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Save to localStorage
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
localStorage.setItem('theme', theme);
|
||||||
|
}
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
|
// Listen for system theme changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
|
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
|
const handleChange = (e) => {
|
||||||
|
// Only update if user hasn't manually set a preference
|
||||||
|
// If theme is saved in localStorage, it means user has manually set it
|
||||||
|
const savedTheme = localStorage.getItem('theme');
|
||||||
|
if (!savedTheme || savedTheme === 'null' || savedTheme === 'undefined') {
|
||||||
|
setTheme(e.matches ? 'dark' : 'light');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
mediaQuery.addEventListener('change', handleChange);
|
||||||
|
return () => mediaQuery.removeEventListener('change', handleChange);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const toggleTheme = () => {
|
||||||
|
setTheme((prevTheme) => {
|
||||||
|
const newTheme = prevTheme === 'light' ? 'dark' : 'light';
|
||||||
|
// Immediately save to localStorage to prevent system preference from overriding
|
||||||
|
localStorage.setItem('theme', newTheme);
|
||||||
|
return newTheme;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeContext.Provider value={{ theme, toggleTheme, setTheme }}>
|
||||||
|
{children}
|
||||||
|
</ThemeContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
75
website/src/data/components/pcb.json
Normal file
75
website/src/data/components/pcb.json
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
{
|
||||||
|
"3030-mount": {
|
||||||
|
"category": "PCB Mount",
|
||||||
|
"type": "base",
|
||||||
|
"printedParts": [
|
||||||
|
{
|
||||||
|
"id": "ossm-pcb-3030-mount",
|
||||||
|
"name": "PCB 3030 Mount",
|
||||||
|
"description": "PCB mount for 3030 extrusion",
|
||||||
|
"filamentEstimate": 15,
|
||||||
|
"timeEstimate": "45m",
|
||||||
|
"colour": "primary",
|
||||||
|
"required": true,
|
||||||
|
"filePath": "OSSM - PCB - 3030 Mount.stl",
|
||||||
|
"url": "https://github.com/KinkyMakers/OSSM-hardware/blob/main/Printed%20Parts/PCB/OSSM%20-%20PCB%20-%203030%20Mount.stl?raw=true"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ossm-pcb-3030-mount-cover",
|
||||||
|
"name": "PCB 3030 Mount Cover",
|
||||||
|
"description": "Cover for the 3030 mount",
|
||||||
|
"filamentEstimate": 15,
|
||||||
|
"timeEstimate": "45m",
|
||||||
|
"colour": "primary",
|
||||||
|
"required": true,
|
||||||
|
"filePath": "OSSM - PCB - 3030 Mount Cover.stl",
|
||||||
|
"url": "https://github.com/KinkyMakers/OSSM-hardware/blob/main/Printed%20Parts/PCB/OSSM%20-%20PCB%20-%203030%20Mount%20Cover.stl?raw=true"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"hardwareParts": [
|
||||||
|
{
|
||||||
|
"id": "hardware-fasteners-m6x12-shcs",
|
||||||
|
"required": true,
|
||||||
|
"quantity": 4,
|
||||||
|
"relatedParts": [
|
||||||
|
"ossm-pcb-3030-mount"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "hardware-fasteners-m6-t-nuts",
|
||||||
|
"required": true,
|
||||||
|
"quantity": 4,
|
||||||
|
"relatedParts": [
|
||||||
|
"ossm-pcb-3030-mount"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"aio-cover-mount": {
|
||||||
|
"category": "PCB Mount",
|
||||||
|
"type": "base",
|
||||||
|
"printedParts": [
|
||||||
|
{
|
||||||
|
"id": "ossm-pcb-aio-cover-mount",
|
||||||
|
"name": "PCB AIO Cover Mount",
|
||||||
|
"description": "All-in-one cover mount on the actuator",
|
||||||
|
"filamentEstimate": 20,
|
||||||
|
"timeEstimate": "1h",
|
||||||
|
"colour": "primary",
|
||||||
|
"required": true,
|
||||||
|
"filePath": "OSSM - PCB - AIO Cover Mount.stl",
|
||||||
|
"url": "https://github.com/KinkyMakers/OSSM-hardware/blob/main/Printed%20Parts/PCB/OSSM%20-%20PCB%20-%20AIO%20Cover%20Mount.stl?raw=true"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"hardwareParts": [
|
||||||
|
{
|
||||||
|
"id": "hardware-fasteners-m3x8-shcs",
|
||||||
|
"required": true,
|
||||||
|
"quantity": 4,
|
||||||
|
"relatedParts": [
|
||||||
|
"ossm-pcb-aio-cover-mount"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,7 +18,8 @@
|
|||||||
"colour": "primary",
|
"colour": "primary",
|
||||||
"required": true,
|
"required": true,
|
||||||
"filePath": "OSSM - Stand - Pivot Plate.stl",
|
"filePath": "OSSM - Stand - Pivot Plate.stl",
|
||||||
"url": "https://github.com/KinkyMakers/OSSM-hardware/blob/main/Printed%20Parts/Stand/OSSM%20-%20Stand%20-%203030%20Extrusion%20Base%20-%20Pivot%20Plate%20Left.stl?raw=true"
|
"url": "https://github.com/KinkyMakers/OSSM-hardware/blob/main/Printed%20Parts/Stand/OSSM%20-%20Stand%20-%203030%20Extrusion%20Base%20-%20Pivot%20Plate%20Left.stl?raw=true",
|
||||||
|
"quantity": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "pivot-plate-right",
|
"id": "pivot-plate-right",
|
||||||
@@ -28,7 +29,19 @@
|
|||||||
"colour": "primary",
|
"colour": "primary",
|
||||||
"required": true,
|
"required": true,
|
||||||
"filePath": "OSSM - Stand - Pivot Plate.stl",
|
"filePath": "OSSM - Stand - Pivot Plate.stl",
|
||||||
"url": "https://github.com/KinkyMakers/OSSM-hardware/blob/main/Printed%20Parts/Stand/OSSM%20-%20Stand%20-%203030%20Extrusion%20Base%20-%20Pivot%20Plate%20Right.stl?raw=true"
|
"url": "https://github.com/KinkyMakers/OSSM-hardware/blob/main/Printed%20Parts/Stand/OSSM%20-%20Stand%20-%203030%20Extrusion%20Base%20-%20Pivot%20Plate%20Right.stl?raw=true",
|
||||||
|
"quantity": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "handle-spacer",
|
||||||
|
"name": "Handle Spacer",
|
||||||
|
"description": "Handle spacer for the stand",
|
||||||
|
"filamentEstimate": 20,
|
||||||
|
"colour": "primary",
|
||||||
|
"required": true,
|
||||||
|
"filePath": "OSSM - Stand - Pivot Spacer.stl",
|
||||||
|
"url": "https://github.com/KinkyMakers/OSSM-hardware/blob/main/Printed%20Parts/Stand/OSSM%20-%20Stand%20-%203030%20Extrusion%20Base%20-%20Handle%20Spacer.stl?raw=true",
|
||||||
|
"quantity": 8
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"hardwareParts": [
|
"hardwareParts": [
|
||||||
@@ -67,7 +80,8 @@
|
|||||||
"description": "Reinforced 3030 hinges for PitClamp",
|
"description": "Reinforced 3030 hinges for PitClamp",
|
||||||
"filamentEstimate": 200,
|
"filamentEstimate": 200,
|
||||||
"colour": "primary",
|
"colour": "primary",
|
||||||
"required": true
|
"required": true,
|
||||||
|
"quantity": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"hardwareParts": [
|
"hardwareParts": [
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import standComponents from './components/stand.json';
|
|||||||
import mountingComponents from './components/mounting.json';
|
import mountingComponents from './components/mounting.json';
|
||||||
import toyMountsComponents from './components/toyMounts.json';
|
import toyMountsComponents from './components/toyMounts.json';
|
||||||
import remoteComponents from './components/remote.json';
|
import remoteComponents from './components/remote.json';
|
||||||
|
import pcbComponents from './components/pcb.json';
|
||||||
|
|
||||||
// Create a hardware lookup map from hardware.json
|
// Create a hardware lookup map from hardware.json
|
||||||
const hardwareLookup = new Map();
|
const hardwareLookup = new Map();
|
||||||
@@ -92,6 +93,7 @@ const rawComponents = {
|
|||||||
...mountingComponents,
|
...mountingComponents,
|
||||||
...toyMountsComponents,
|
...toyMountsComponents,
|
||||||
...remoteComponents,
|
...remoteComponents,
|
||||||
|
...pcbComponents,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Resolve hardware references
|
// Resolve hardware references
|
||||||
|
|||||||
@@ -2,9 +2,12 @@ import { StrictMode } from 'react'
|
|||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
import App from './App.jsx'
|
import App from './App.jsx'
|
||||||
|
import { ThemeProvider } from './contexts/ThemeContext'
|
||||||
|
|
||||||
createRoot(document.getElementById('root')).render(
|
createRoot(document.getElementById('root')).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<App />
|
<ThemeProvider>
|
||||||
|
<App />
|
||||||
|
</ThemeProvider>
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export default {
|
|||||||
"./index.html",
|
"./index.html",
|
||||||
"./src/**/*.{js,ts,jsx,tsx}",
|
"./src/**/*.{js,ts,jsx,tsx}",
|
||||||
],
|
],
|
||||||
|
darkMode: 'class',
|
||||||
theme: {
|
theme: {
|
||||||
extend: {},
|
extend: {},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user