diff --git a/src/components/app/app.tsx b/src/components/app/app.tsx
index 0658c57..aa4d138 100644
--- a/src/components/app/app.tsx
+++ b/src/components/app/app.tsx
@@ -9,6 +9,7 @@ import { StoreConsumer } from '@/components/store-consumer';
import { Buttons } from '@/components/buttons';
import { Categories } from '@/components/categories';
import { ScrollToTop } from '@/components/scroll-to-top';
+import { SharedModal } from '@/components/modals/shared';
import { Menu } from '@/components/menu/menu';
import { SnackbarProvider } from '@/contexts/snackbar';
@@ -61,6 +62,7 @@ export function App() {
+
);
diff --git a/src/components/modals/share-link/share-link.tsx b/src/components/modals/share-link/share-link.tsx
index 368c5eb..7134ecb 100644
--- a/src/components/modals/share-link/share-link.tsx
+++ b/src/components/modals/share-link/share-link.tsx
@@ -1,4 +1,4 @@
-import { useMemo } from 'react';
+import { useMemo, useEffect, useState } from 'react';
import { IoCopyOutline, IoCheckmark } from 'react-icons/io5/index';
import { Modal } from '@/components/modal';
@@ -14,6 +14,7 @@ interface ShareLinkModalProps {
}
export function ShareLinkModal({ onClose, show }: ShareLinkModalProps) {
+ const [isMounted, setIsMounted] = useState(false);
const sounds = useSoundStore(state => state.sounds);
const { copy, copying } = useCopy();
@@ -38,8 +39,15 @@ export function ShareLinkModal({ onClose, show }: ShareLinkModalProps) {
}, [selected]);
const url = useMemo(() => {
- return `https://moodist.app/?share=${encodeURIComponent(string)}`;
- }, [string]);
+ if (!isMounted)
+ return `https://moodist.app/?share=${encodeURIComponent(string)}`;
+
+ return `${window.location.protocol}//${
+ window.location.host
+ }/?share=${encodeURIComponent(string)}`;
+ }, [string, isMounted]);
+
+ useEffect(() => setIsMounted(true), []);
return (
diff --git a/src/components/modals/shared/index.ts b/src/components/modals/shared/index.ts
new file mode 100644
index 0000000..995eba6
--- /dev/null
+++ b/src/components/modals/shared/index.ts
@@ -0,0 +1 @@
+export { SharedModal } from './shared';
diff --git a/src/components/modals/shared/shared.module.css b/src/components/modals/shared/shared.module.css
new file mode 100644
index 0000000..2cfd0ae
--- /dev/null
+++ b/src/components/modals/shared/shared.module.css
@@ -0,0 +1,68 @@
+.heading {
+ font-family: var(--font-heading);
+ font-size: var(--font-md);
+ font-weight: 700;
+}
+
+.desc {
+ margin-top: 12px;
+ line-height: 1.6;
+ color: var(--color-foreground-subtle);
+}
+
+.sounds {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ width: 100%;
+ padding: 12px;
+ margin-top: 12px;
+ background-color: var(--color-neutral-50);
+ border: 1px solid var(--color-neutral-200);
+ border-radius: 4px;
+
+ & .sound {
+ padding: 8px 16px;
+ font-size: var(--font-sm);
+ font-weight: 500;
+ background-color: var(--color-neutral-100);
+ border: 1px solid var(--color-neutral-200);
+ border-radius: 100px;
+ }
+}
+
+.footer {
+ display: flex;
+ column-gap: 8px;
+ align-items: center;
+ justify-content: flex-end;
+ margin-top: 12px;
+
+ .button {
+ padding: 12px 16px;
+ font-family: var(--font-heading);
+ font-size: var(--font-sm);
+ font-weight: 600;
+ color: var(--color-foreground-subtle);
+ cursor: pointer;
+ background-color: var(--color-neutral-200);
+ border: none;
+ border-radius: 4px;
+ outline: none;
+ transition: 0.2s;
+
+ &:hover {
+ color: var(--color-foreground);
+ background-color: var(--color-neutral-300);
+ }
+
+ &.primary {
+ color: var(--color-neutral-200);
+ background-color: var(--color-neutral-950);
+
+ &:hover {
+ background-color: var(--color-neutral-800);
+ }
+ }
+ }
+}
diff --git a/src/components/modals/shared/shared.tsx b/src/components/modals/shared/shared.tsx
new file mode 100644
index 0000000..8a8a3d6
--- /dev/null
+++ b/src/components/modals/shared/shared.tsx
@@ -0,0 +1,107 @@
+import { useState, useEffect } from 'react';
+
+import { Modal } from '@/components/modal';
+
+import { useSoundStore } from '@/store';
+import { useSnackbar } from '@/contexts/snackbar';
+import { cn } from '@/helpers/styles';
+import { sounds } from '@/data/sounds';
+
+import styles from './shared.module.css';
+
+export function SharedModal() {
+ const override = useSoundStore(state => state.override);
+ const showSnackbar = useSnackbar();
+
+ const [isOpen, setIsOpen] = useState(false);
+ const [sharedSounds, setSharedSounds] = useState<
+ Array<{
+ id: string;
+ label: string;
+ volume: number;
+ }>
+ >([]);
+
+ useEffect(() => {
+ const searchParams = new URLSearchParams(window.location.search);
+ const share = searchParams.get('share');
+
+ if (share) {
+ try {
+ const parsed = JSON.parse(decodeURIComponent(share));
+ const allSounds: Record = {};
+
+ sounds.categories.forEach(category => {
+ category.sounds.forEach(sound => {
+ allSounds[sound.id] = sound.label;
+ });
+ });
+
+ const _sharedSounds: Array<{
+ id: string;
+ label: string;
+ volume: number;
+ }> = [];
+
+ Object.keys(parsed).forEach(sound => {
+ if (allSounds[sound]) {
+ _sharedSounds.push({
+ id: sound,
+ label: allSounds[sound],
+ volume: Number(parsed[sound]),
+ });
+ }
+ });
+
+ if (_sharedSounds.length) {
+ setIsOpen(true);
+ setSharedSounds(_sharedSounds);
+ }
+ } catch (error) {
+ return;
+ } finally {
+ history.pushState({}, '', location.href.split('?')[0]);
+ }
+ }
+ }, []);
+
+ const handleOverride = () => {
+ const newSounds: Record = {};
+
+ sharedSounds.forEach(sound => {
+ newSounds[sound.id] = sound.volume;
+ });
+
+ override(newSounds);
+ setIsOpen(false);
+ showSnackbar('Overrode sounds! You can now play them.');
+ };
+
+ return (
+ setIsOpen(false)}>
+ New sound mix detected!
+
+ Someone has shared the following mix with you. Would you want to
+ override your current selection?
+
+
+ {sharedSounds.map(sound => (
+
+ {sound.label}
+
+ ))}
+
+
+
+
+
+
+ );
+}
diff --git a/src/store/sound/sound.actions.ts b/src/store/sound/sound.actions.ts
index 709695a..a167301 100644
--- a/src/store/sound/sound.actions.ts
+++ b/src/store/sound/sound.actions.ts
@@ -5,6 +5,7 @@ import type { SoundState } from './sound.state';
import { pickMany, random } from '@/helpers/random';
export interface SoundActions {
+ override: (sounds: Record) => void;
pause: () => void;
play: () => void;
restoreHistory: () => void;
@@ -24,6 +25,19 @@ export const createActions: StateCreator<
SoundActions
> = (set, get) => {
return {
+ override(newSounds) {
+ get().unselectAll();
+
+ const sounds = get().sounds;
+
+ Object.keys(newSounds).forEach(sound => {
+ sounds[sound].isSelected = true;
+ sounds[sound].volume = newSounds[sound];
+ });
+
+ set({ sounds: { ...sounds } });
+ },
+
pause() {
set({ isPlaying: false });
},