diff --git a/index.html b/index.html
index 891a938..d8c4fb6 100644
--- a/index.html
+++ b/index.html
@@ -2,9 +2,9 @@
-
+
- Vite + React
+ D2Loadouts
diff --git a/package-lock.json b/package-lock.json
index 25e83dd..44d50c2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,6 +15,7 @@
"@mui/system": "^5.15.20",
"@reduxjs/toolkit": "^2.2.6",
"@tanstack/react-table": "^8.17.3",
+ "@types/react-beautiful-dnd": "^13.1.8",
"@types/react-slick": "^0.23.13",
"axios": "^1.7.2",
"dexie": "^4.0.8",
@@ -22,7 +23,9 @@
"dotenv": "^16.4.5",
"framer-motion": "^11.3.29",
"heap-js": "^2.5.0",
+ "lz-string": "^1.5.0",
"react": "^18.2.0",
+ "react-beautiful-dnd": "^13.1.1",
"react-dom": "^18.2.0",
"react-redux": "^9.1.2",
"react-router-dom": "^6.23.1",
@@ -4796,6 +4799,15 @@
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
"dev": true
},
+ "node_modules/@types/hoist-non-react-statics": {
+ "version": "3.3.5",
+ "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz",
+ "integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==",
+ "dependencies": {
+ "@types/react": "*",
+ "hoist-non-react-statics": "^3.3.0"
+ }
+ },
"node_modules/@types/istanbul-lib-coverage": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
@@ -4856,6 +4868,14 @@
"csstype": "^3.0.2"
}
},
+ "node_modules/@types/react-beautiful-dnd": {
+ "version": "13.1.8",
+ "resolved": "https://registry.npmjs.org/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.8.tgz",
+ "integrity": "sha512-E3TyFsro9pQuK4r8S/OL6G99eq7p8v29sX0PM7oT8Z+PJfZvSQTx4zTQbUJ+QZXioAF0e7TGBEcA1XhYhCweyQ==",
+ "dependencies": {
+ "@types/react": "*"
+ }
+ },
"node_modules/@types/react-dom": {
"version": "18.3.0",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz",
@@ -4874,6 +4894,25 @@
"@types/react": "*"
}
},
+ "node_modules/@types/react-redux": {
+ "version": "7.1.33",
+ "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.33.tgz",
+ "integrity": "sha512-NF8m5AjWCkert+fosDsN3hAlHzpjSiXlVy9EgQEmLoBhaNXbmyeGs/aj5dQzKuF+/q+S7JQagorGDW8pJ28Hmg==",
+ "dependencies": {
+ "@types/hoist-non-react-statics": "^3.3.0",
+ "@types/react": "*",
+ "hoist-non-react-statics": "^3.3.0",
+ "redux": "^4.0.0"
+ }
+ },
+ "node_modules/@types/react-redux/node_modules/redux": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
+ "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
+ "dependencies": {
+ "@babel/runtime": "^7.9.2"
+ }
+ },
"node_modules/@types/react-slick": {
"version": "0.23.13",
"resolved": "https://registry.npmjs.org/@types/react-slick/-/react-slick-0.23.13.tgz",
@@ -5968,6 +6007,14 @@
"node": ">= 8"
}
},
+ "node_modules/css-box-model": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz",
+ "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==",
+ "dependencies": {
+ "tiny-invariant": "^1.0.6"
+ }
+ },
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
@@ -9183,6 +9230,14 @@
"yallist": "^3.0.2"
}
},
+ "node_modules/lz-string": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
+ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
+ "bin": {
+ "lz-string": "bin/bin.js"
+ }
+ },
"node_modules/make-dir": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
@@ -10746,6 +10801,11 @@
}
]
},
+ "node_modules/raf-schd": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz",
+ "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ=="
+ },
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@@ -10766,6 +10826,61 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-beautiful-dnd": {
+ "version": "13.1.1",
+ "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz",
+ "integrity": "sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==",
+ "dependencies": {
+ "@babel/runtime": "^7.9.2",
+ "css-box-model": "^1.2.0",
+ "memoize-one": "^5.1.1",
+ "raf-schd": "^4.0.2",
+ "react-redux": "^7.2.0",
+ "redux": "^4.0.4",
+ "use-memo-one": "^1.1.1"
+ },
+ "peerDependencies": {
+ "react": "^16.8.5 || ^17.0.0 || ^18.0.0",
+ "react-dom": "^16.8.5 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/react-beautiful-dnd/node_modules/react-is": {
+ "version": "17.0.2",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
+ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
+ },
+ "node_modules/react-beautiful-dnd/node_modules/react-redux": {
+ "version": "7.2.9",
+ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz",
+ "integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==",
+ "dependencies": {
+ "@babel/runtime": "^7.15.4",
+ "@types/react-redux": "^7.1.20",
+ "hoist-non-react-statics": "^3.3.2",
+ "loose-envify": "^1.4.0",
+ "prop-types": "^15.7.2",
+ "react-is": "^17.0.2"
+ },
+ "peerDependencies": {
+ "react": "^16.8.3 || ^17 || ^18"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ },
+ "react-native": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-beautiful-dnd/node_modules/redux": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
+ "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
+ "dependencies": {
+ "@babel/runtime": "^7.9.2"
+ }
+ },
"node_modules/react-devtools-core": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-5.3.1.tgz",
@@ -12233,6 +12348,11 @@
"safe-buffer": "~5.1.0"
}
},
+ "node_modules/tiny-invariant": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
+ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="
+ },
"node_modules/tmpl": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
@@ -12511,6 +12631,14 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/use-memo-one": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz",
+ "integrity": "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/use-scramble": {
"version": "2.2.15",
"resolved": "https://registry.npmjs.org/use-scramble/-/use-scramble-2.2.15.tgz",
diff --git a/package.json b/package.json
index d506eb4..8899bdf 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,7 @@
"@mui/system": "^5.15.20",
"@reduxjs/toolkit": "^2.2.6",
"@tanstack/react-table": "^8.17.3",
+ "@types/react-beautiful-dnd": "^13.1.8",
"@types/react-slick": "^0.23.13",
"axios": "^1.7.2",
"dexie": "^4.0.8",
@@ -24,7 +25,9 @@
"dotenv": "^16.4.5",
"framer-motion": "^11.3.29",
"heap-js": "^2.5.0",
+ "lz-string": "^1.5.0",
"react": "^18.2.0",
+ "react-beautiful-dnd": "^13.1.1",
"react-dom": "^18.2.0",
"react-redux": "^9.1.2",
"react-router-dom": "^6.23.1",
diff --git a/src/components/LoadoutCustomization.tsx b/src/components/LoadoutCustomization.tsx
index 24fac35..364bd12 100644
--- a/src/components/LoadoutCustomization.tsx
+++ b/src/components/LoadoutCustomization.tsx
@@ -6,6 +6,7 @@ import ModCustomization from '../features/armor/components/ModCustomization';
import EquipLoadout from '../features/loadouts/components/EquipLoadout';
import AbilitiesModification from '../features/subclass/AbilitiesModification';
import { ManifestSubclass } from '../types/manifest-types';
+import ShareLoadout from '../features/loadouts/components/ShareLoadout';
interface LoadoutCustomizationProps {
onBackClick: () => void;
@@ -71,7 +72,7 @@ const LoadoutCustomization: React.FC = ({
- SHARE LOADOUT BUTTON ?
+
diff --git a/src/features/loadouts/components/ShareLoadout.tsx b/src/features/loadouts/components/ShareLoadout.tsx
new file mode 100644
index 0000000..6c5c00b
--- /dev/null
+++ b/src/features/loadouts/components/ShareLoadout.tsx
@@ -0,0 +1,276 @@
+import React, { useState } from 'react';
+import { useSelector } from 'react-redux';
+import { RootState } from '../../../store/index';
+import { compressToEncodedURIComponent, decompressFromEncodedURIComponent } from 'lz-string';
+import {
+ Button,
+ TextField,
+ Box,
+ List,
+ ListItem,
+ ListItemIcon,
+ ListItemText,
+ Dialog,
+ DialogTitle,
+ DialogContent,
+ DialogActions,
+ IconButton,
+} from '@mui/material';
+import CloseIcon from '@mui/icons-material/Close';
+import { styled } from '@mui/material/styles';
+import { DragDropContext, Droppable, Draggable, DropResult } from 'react-beautiful-dnd';
+
+const StyledDialog = styled(Dialog)(({ theme }) => ({
+ '& .MuiDialog-paper': {
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
+ backdropFilter: 'blur(10px)',
+ color: 'white',
+ fontFamily: 'Helvetica, Arial, sans-serif',
+ borderRadius: 0,
+ },
+}));
+
+const TransparentButton = styled(Button)(({ theme }) => ({
+ background: 'transparent',
+ color: 'white',
+ padding: theme.spacing(1, 2),
+ fontFamily: 'Helvetica, Arial, sans-serif',
+ '&:hover': {
+ background: 'rgba(255, 255, 255, 0.1)',
+ },
+ borderRadius: 0,
+ border: '1px solid white',
+}));
+
+const StyledTextField = styled(TextField)(({ theme }) => ({
+ '& .MuiOutlinedInput-root': {
+ color: 'white',
+ fontFamily: 'Helvetica, Arial, sans-serif',
+ '& fieldset': {
+ borderColor: 'white',
+ borderRadius: 0,
+ },
+ '&:hover fieldset': {
+ borderColor: 'white',
+ },
+ '&.Mui-focused fieldset': {
+ borderColor: 'white',
+ },
+ },
+ '& .MuiInputLabel-root': {
+ color: 'white',
+ fontFamily: 'Helvetica, Arial, sans-serif',
+ },
+}));
+
+const StatIcon = styled('img')({
+ width: 24,
+ height: 24,
+ marginRight: 10,
+});
+
+const StyledList = styled(List)({
+ width: '100%',
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+});
+
+const StyledListItem = styled(ListItem)({
+ backgroundColor: 'rgba(255, 255, 255, 0.1)',
+ marginBottom: '4px',
+ width: '200px',
+ padding: '4px 8px',
+ borderRadius: 0,
+ border: '1px solid white',
+ '&:hover': {
+ backgroundColor: 'rgba(255, 255, 255, 0.2)',
+ },
+});
+
+const statIcons: Record = {
+ mobility:
+ 'https://www.bungie.net/common/destiny2_content/icons/e26e0e93a9daf4fdd21bf64eb9246340.png',
+ resilience:
+ 'https://www.bungie.net/common/destiny2_content/icons/202ecc1c6febeb6b97dafc856e863140.png',
+ recovery:
+ 'https://www.bungie.net/common/destiny2_content/icons/128eee4ee7fc127851ab32eac6ca91cf.png',
+ discipline:
+ 'https://www.bungie.net/common/destiny2_content/icons/79be2d4adef6a19203f7385e5c63b45b.png',
+ intellect:
+ 'https://www.bungie.net/common/destiny2_content/icons/d1c154469670e9a592c9d4cbdcae5764.png',
+ strength:
+ 'https://www.bungie.net/common/destiny2_content/icons/ea5af04ccd6a3470a44fd7bb0f66e2f7.png',
+};
+
+const ShareLoadout: React.FC = () => {
+ const [open, setOpen] = useState(false);
+ const [shareLink, setShareLink] = useState('');
+ const [parsedLink, setParsedLink] = useState('');
+ const loadoutState = useSelector((state: RootState) => ({
+ helmetMods: state.loadoutConfig.loadout.helmetMods,
+ gauntletMods: state.loadoutConfig.loadout.gauntletMods,
+ chestArmorMods: state.loadoutConfig.loadout.chestArmorMods,
+ legArmorMods: state.loadoutConfig.loadout.legArmorMods,
+ subclassConfig: state.loadoutConfig.loadout.subclassConfig,
+ selectedValues: state.dashboard.selectedValues,
+ selectedExoticItemHash: state.dashboard.selectedExoticItemHash,
+ }));
+
+ const [statPriority, setStatPriority] = useState([
+ 'mobility',
+ 'resilience',
+ 'recovery',
+ 'discipline',
+ 'intellect',
+ 'strength',
+ ]);
+
+ const handleOpen = () => setOpen(true);
+ const handleClose = () => setOpen(false);
+
+ const onDragEnd = (result: DropResult) => {
+ if (!result.destination) return;
+
+ const items = Array.from(statPriority);
+ const [reorderedItem] = items.splice(result.source.index, 1);
+ items.splice(result.destination.index, 0, reorderedItem);
+
+ setStatPriority(items);
+ };
+
+ const generateShareLink = () => {
+ const dataToShare = {
+ ...loadoutState,
+ statPriority,
+ };
+ const compressedData = compressToEncodedURIComponent(JSON.stringify(dataToShare));
+ const shareableLink = `${window.location.origin}/loadout?data=${compressedData}`;
+ setShareLink(shareableLink);
+ };
+
+ const parseLink = () => {
+ try {
+ const url = new URL(shareLink);
+ const compressedData = url.searchParams.get('data');
+ if (!compressedData) {
+ throw new Error('Invalid loadout link');
+ }
+ const decompressedData = decompressFromEncodedURIComponent(compressedData);
+ if (!decompressedData) {
+ throw new Error('Failed to decompress loadout data');
+ }
+ const parsedData = JSON.parse(decompressedData);
+ setParsedLink(JSON.stringify(parsedData, null, 2));
+ console.log('Parsed loadout:', parsedData);
+ } catch (error) {
+ console.error('Error parsing loadout link:', error);
+ setParsedLink('Error parsing link');
+ }
+ };
+
+ const copyToClipboard = () => {
+ navigator.clipboard.writeText(shareLink).then(() => {
+ alert('Link copied to clipboard!');
+ });
+ };
+
+ return (
+ <>
+ Share Loadout
+
+
+ Share Loadout
+
+
+
+
+
+
+ Prioritize Stats : drag and drop stats based on which stat matters the most
+
+
+ {(provided) => (
+
+ {statPriority.map((stat, index) => (
+
+ {(provided, snapshot) => (
+
+
+
+
+
+
+ )}
+
+ ))}
+ {provided.placeholder}
+
+ )}
+
+
+
+
+ Generate Share Link
+
+ {shareLink && (
+
+
+
+
+ Copy Link
+
+ Parse Link
+
+
+ )}
+ {parsedLink && (
+
+
+
+ )}
+
+
+ Close
+
+
+ >
+ );
+};
+
+export default ShareLoadout;
diff --git a/src/features/loadouts/components/manualTestParseLoadoutLink.ts b/src/features/loadouts/components/manualTestParseLoadoutLink.ts
new file mode 100644
index 0000000..8e0518a
--- /dev/null
+++ b/src/features/loadouts/components/manualTestParseLoadoutLink.ts
@@ -0,0 +1,42 @@
+import { decompressFromEncodedURIComponent } from 'lz-string';
+
+// The parseLoadoutLink function
+const parseLoadoutLink = (link: string) => {
+ const url = new URL(link);
+ const compressedData = url.searchParams.get('data');
+
+ if (!compressedData) {
+ throw new Error('Invalid loadout link');
+ }
+
+ const decompressedData = decompressFromEncodedURIComponent(compressedData);
+
+ if (!decompressedData) {
+ throw new Error('Failed to decompress loadout data');
+ }
+
+ return JSON.parse(decompressedData);
+};
+
+// Function to test parseLoadoutLink with a manually provided hash
+function testWithManualHash(hash: string) {
+ // Construct the full URL with the provided hash
+ const fullUrl = `http://example.com/loadout?data=${hash}`;
+
+ console.log('Testing with URL:', fullUrl);
+
+ try {
+ // Attempt to parse the loadout link
+ const parsedLoadout = parseLoadoutLink(fullUrl);
+ console.log('Successfully parsed loadout:');
+ console.log(JSON.stringify(parsedLoadout, null, 2)); // Pretty print the result
+ } catch (error) {
+ console.error('Error parsing loadout link:', error);
+ }
+}
+
+// REPLACE THIS WITH YOUR ACTUAL GENERATED HASH
+const manuallyProvidedHash = 'https://localhost:5173/loadout?data=N4IgFgpgNgthAuBZA9gEwM4gFwG1QEt4IYAJAQ3TGwEYBOADgAYA2a+gVnoHYAaEABwgAnANYA5MnGwgQfQaIAiEdAGMh+fvHzIAdtNkDhIgJIrd+virJEA5siEBPbACYALN3rOuAZnbs++OgA8gDuOhCo2PBCAK4QfDqSENIAojCaDgAEKKiZAMrIKiIIBvhmeljg8PD86FgA9PUARjE6NvgQAHTh8PVmMDC69ajKWjoOzgD65UQ6vWW66PVMqM4Q1GTOZFzMAGYjm1zszk0QTdSu1Iz0rqgqu-SdAFb8NgYQ4UI2DgDCyOjwbCMSzIKBQCAqLRNcHkSjYAC01AAvjwCERSBQqFgrlwmEwdv5DKIJFJKgZ5CIlKp1JptBUZHIjKZzGTLNYIHZHC5aNRnNQuFdGN4AsEwhEorF4iBEqSQGkMtk0PlCsVAQFytIwNVag1mq12l0en1kAMhiMAfhxlMZh95uUluwyA8jvRmCpaM4VMwfGRtvRfK5vKh6PtnMH9s9Xu9Pt8-gCgSCwRCoTDMQjkaiQIRiLCsTi8YwCYziUkLETKco1BotCyGeXmfSDFZbPYnFhnDy+QLGEKRaFwpEsNE4glS5V5fAsjllUUSura1qanVGi02h1ughjaadMNRpaJtNdLM7Yt6o7nZw3R6vT6-QGgyHVuHUJG3nwPsJY-9AVhgSAzEmkL4NCEC5umKJojmabYoWBZFuWJLJKy5ZUlWtK1uSTIashzYcq23K8vygrClmooDhKI7SmOcrpJOiq5AUs5qlm2FVEuuqrgaG69P0gw7uaYwHjacz1AsOgOk63CXu6nret4vq4vez5ho+r7Rp+vzfgm-6guCQEgWBWCIhBWbooZlw+B6gbOCRFKIWWFKoTSNaNsWJisU27Kcm23hcD4-LeIwHZ9mKg7DlKMpITRCrToxqqlKxi46iu+rrkavFmnuVqHnMtqifa9TOIw1DeN4+zMGQjCoLsFVNL6FxNKc7CNd46wMOwzCdPwbTqV8mnxr+iZ6SmoHQcZAC6fA2GQrTwOCSBoJguCQRicLYgwLBsJwvAIdRmGKJWzl0g5WEYWyLZcu27i4l4viEoE-bikOkqjrKE5TkqcVzixC7asueproam4ZfxWVCUeeViUsKxrBsWw7PsECHMcpznJc1y3PcjwvG+IAfn1cY-n+AHDcBqZrcZmbZqtWLeJ4xWuFwjO0G59nIY5h3Vsd7Ona5-5efh7adkRPYkQ9oUURF1HvfRM7xfO9JJf9IQq50nFpcDJp8buFrZcJJ7ifUtC7F29BMCo3hNIw9xkN4KjnFwZAqK4Hq0GQtC0Os2NRu+Mb9UTQ3JmTo0UxmK2GXTRUXEzXAs7tsr7RW1Jc2d9YeedeGXR2hHdr2pGPWFL1UW9tEfQxKrfWJmp-bqKshGrqVAzxWuZbr4O5SJUNGyb-Jm9blvW7stv2-yTsux27ue2wam+xphPaSTQcGWNYemVBa2RwzMdx3Ze1uU5Kd8xSDZlrh3kEV2xEheRz2UZFqSl7LX3MVXlRK7Xqvq03W7awJ+7WhDTuBVjam3NgPG2dsHZj1dpPL2M88Z+3noNHSgERqGUpuHaCFlvBWW8DZVme8UKc3QkfXmp8BaXV8v5HwQU47ixvuFV6UUZaxQri-RKNcUqA24j-VugkAEdwNksKOpVyqVWqrVeqrhGoQGak0VqdAOCdW6rjfGX4BrE10kvcmWIJqWEgACAAgkIQYQgchLTwGvGmNANqsA4NwAhCd97EJcidUQJ8cIULbG4Dwt0-DXyeow4uzDH6sKYglX67EuFcXSi3UGbcBHHnyqeGG6xNjbD2AcLwKMzgXCuDcO4Dx4FqP9gvLR+kdHgSpmZaCbAhSBncL4RxUVE4HxIW49yqcz6C2zpfUWATC532lqEz6bCImK04fUOuDduGxO3DrfhOUkld2YKsm4NVgzUFkVwM4bovSMF2O6XYTQOrUGYPQdYxTEFaWQYvCpIddGr2poZOp3gGn+kJLvJxRDk7tJ5u49O-MLreOFrnMWZFAlF3vuOEZ5dwkK2rlEqZn9G48JBgs-+SzIYFVWec1wGz6BbPYDspoezmAHKOSc1g5zLk416uogOKDSbL1DiZZ5tSmBvLeR85pHS2muP+Z0vm3Ss6gqvvnCWt8pYlxiqM+FP0JlIumV-NFcSMV60AUI+ouL1nME2ds3ZKh9mHONlSs5FzqBXLnjczRqDg7oKeTUta2DcH4Pji05xvyBV1mPoCkVPk-LeACrQgZksmEP1lXC+WCrEXJQBjEzW8y-4asEckw2IiyqoAqlVGqZA6pkAak1FqbUlFdR6rPAmNrA73IdUiSaIBwQ2GMaY8x2BLHsrWooza9idpfI9T8tC3rE4eLrP6lw11PA+H8RKhhULhmRrlpXDhSKVVzN-mDRJ2KUlVVhukhGWSjgnFyejApWMrWVo0dWtBK82VOqxM4ZwzAGDeHOdQagvLBX8u5j6shnjgUXxFnnehkKhkyromE6Nr82JxuVaitdfDMX6zTUsHkxLvSNWoLsHB5xVnXAFJZdgtxyUHJfeehlZS7Ussebe9e97H3Ptfe+91fKXHfuHX6rxAGwWhqleGmFC7n7jNjcrFFszE3roSVioBp5UM7C4BhrDtAcPku4AFWghGs09iw51OlFbyO3PKde1l1TaMuHo-6RjH6f0HS9WxtyI7PL-qFjncVwHBnSpCQJsZCK36TNg2J5uSaN1Sa1bJ9D5xFPKbw2pjTxHtNkdKQZyjlSjKOtM1gF1bg8G2SMGzazSdB12bTl0zjWAqFBpocFGdIGPMRvA3KyDy642rvEwhlNyyCoZrETmyRBbpFFvkSWjqZbVHXMvUy7RDzwL1vQDEJoKgoAUHQH8HQux8BvCwKAWb83FvoCWgQcS8AyA6BUBAYwg46zUggB8SgyAfx1lQJIMgNgIAABUHCCG0m57AQ8oDoClAtpbZYO1Yj-NCusUGQAohAI9mAz23sfaQkx2b8hsCgD7Sx2zqdfUlac3+b7WBfv-YCHe7S4OhNkmhxQQQkILGgBKT8Mg-AnaEDbH+DHn7WPY9-aO0r+OIWDiJ1KEHZPCGQ5MgzpnLPJzaQ5-lr93OAW48zmznjQuSfpbB2Ljyda+C7CEM9uAcw6cgEGE0fAUBWc5G0kIZQFuOgnYgNb5BtuzAADdhAOGd3+VAgQVAaEt+Eb3ARcqoODyAAEtu2jwDAOHuXrSuekKV8KvnauyB-eF6T5B5OfMyBMmb+3k5w+2-QPbj4p3i8QmQB7xw4ffeqAD5aJ3aAvuh+GuHyPHwbAx7j7lwhHMsdJ6FeQvHaeM8a+sdn7XGF8-IHN5bovLeXd28t+X5vg4-yu+r57uvfvG9B6X-jtvyYO-RC7z3w-VmE+D46Q5jO59kEE-V1YwyWvvni8zAXhfXvL8gBL2Xx3Svd3HfX-evf3fgQPdfVvIgMPX-TvaPWPX-ePT1QrRXYfP9FXL7AXH7dPYnF-aCN-ftD-UAL-K3X-f-VfQA8gqvGvH-DfPgMA-fKAx-Y-SEU-KPbvRA+g5jTnG-QVO-IFTAx-bAwnXAzPTXPjCHHXetQHPbQxefVnbAHQGIMEPgOAcECAeQwvNsZQ1Q03bfI3eALQ7-JQlQqAKaKPMgEYUwsEaHf7YaCIAANXTziH2z-xXwd1OxoB7D4C31oO8J9z3wgKbwCJDxgOGmwECj4HgM4JcD-FIJl3bEYDsOgGTAiBSAAA87syhjAs9dCoAkQgA';
+
+// Run the test with the manually provided hash
+testWithManualHash(manuallyProvidedHash);
\ No newline at end of file
diff --git a/src/features/loadouts/components/parseLoadoutLink.ts b/src/features/loadouts/components/parseLoadoutLink.ts
new file mode 100644
index 0000000..f48f292
--- /dev/null
+++ b/src/features/loadouts/components/parseLoadoutLink.ts
@@ -0,0 +1,18 @@
+import { decompressFromEncodedURIComponent } from 'lz-string';
+
+export const parseLoadoutLink = (link: string) => {
+ const url = new URL(link);
+ const compressedData = url.searchParams.get('data');
+
+ if (!compressedData) {
+ throw new Error('Invalid loadout link');
+ }
+
+ const decompressedData = decompressFromEncodedURIComponent(compressedData);
+
+ if (!decompressedData) {
+ throw new Error('Failed to decompress loadout data');
+ }
+
+ return JSON.parse(decompressedData);
+};
\ No newline at end of file