diff --git a/packages/web/components/Dashboard/Header/Header.tsx b/packages/web/components/Dashboard/Header/Header.tsx index ad21b363f..298a2d692 100644 --- a/packages/web/components/Dashboard/Header/Header.tsx +++ b/packages/web/components/Dashboard/Header/Header.tsx @@ -28,10 +28,6 @@ const Header: React.FC = ({ onMenuClick }) => { {showNotificationFeed && setShowNotificationFeed(false)} />} - - + + .typewriter-sounds-switch-container { + display: flex; + justify-content: center; + align-items: center; + gap: 10px; + height: 100%; + margin-left: 10px; + } + + /* Hide "Typewriter Sounds" when Toolbar is floating */ + .editor-toolbar-container.is-fixed .typewriter-sounds.toolbar-row { + display: none; + } + + .editor-toolbar-popover-item { + width: 100%; + justify-content: left; + padding-left: 7px; + } + + .editor-toolbar-popover-item:hover { + font-weight: 600; + } + + .toolbar-row { + display: flex; + gap: 10px; + } + `} + + ) } diff --git a/packages/web/components/Layouts/DashboardLayout.tsx b/packages/web/components/Layouts/DashboardLayout.tsx index 1861d672b..82e6f3661 100644 --- a/packages/web/components/Layouts/DashboardLayout.tsx +++ b/packages/web/components/Layouts/DashboardLayout.tsx @@ -2,7 +2,7 @@ import React, { useState } from 'react' import classNames from 'classnames' import { useRouter } from 'next/router' import Nav, { navConstants } from '@/components/Dashboard/Nav' -import { layoutPadding, headerHeight } from '@/components/Dashboard/dashboardConstants' +import { layoutPadding } from '@/components/Dashboard/dashboardConstants' import Header from '@/components/Dashboard/Header' import NotificationContextProvider from '../NotificationFeed/NotificationContext' @@ -41,7 +41,6 @@ const DashboardLayout: React.FC = ({ children, pad = 'always' }) => { position: relative; height: 100%; width: 100%; - overflow: hidden; } .dashboard-container { @@ -54,15 +53,8 @@ const DashboardLayout: React.FC = ({ children, pad = 'always' }) => { } } - @media (${navConstants.mobileNavOnly}) { - .dashboard { - padding-top: ${headerHeight}; - } - } - .dashboard-container { height: 100%; - overflow-y: auto; transition: margin-left ${navConstants.transitionDuration}ms ease-in-out; } diff --git a/packages/web/components/SwitchToggle/SwitchToggle.tsx b/packages/web/components/SwitchToggle/SwitchToggle.tsx new file mode 100644 index 000000000..f76485212 --- /dev/null +++ b/packages/web/components/SwitchToggle/SwitchToggle.tsx @@ -0,0 +1,60 @@ +import theme from '@/theme' +import React from 'react' + +type SwitchToggleProps = { + isToggled: boolean + onToggle: () => void +} + +const SwitchToggle: React.FC = ({ isToggled, onToggle }) => { + return ( + + ) +} + +export default SwitchToggle diff --git a/packages/web/components/SwitchToggle/index.ts b/packages/web/components/SwitchToggle/index.ts new file mode 100644 index 000000000..528d5305f --- /dev/null +++ b/packages/web/components/SwitchToggle/index.ts @@ -0,0 +1 @@ +export { default } from './SwitchToggle' diff --git a/packages/web/hooks/useAutosavedState.ts b/packages/web/hooks/useAutosavedState.ts index 826f9606b..523b6205c 100644 --- a/packages/web/hooks/useAutosavedState.ts +++ b/packages/web/hooks/useAutosavedState.ts @@ -1,21 +1,28 @@ import React from 'react' +type UseAutosavedStateOpts = { + key: string + debounceTime?: number + initialTimestamp?: number +} + export default function useAutosavedState( initialValue: T, - opts = { - key: 'default', + opts: UseAutosavedStateOpts, +): [T, (value: T) => void, () => void] { + const mergedOpts = { debounceTime: 1000, initialTimestamp: 0, - }, -): [T, (value: T) => void, () => void] { + ...opts, + } const storage: any = (typeof window !== 'undefined' && window.localStorage) || {} - const storageKey = `autosave-v1[${opts.key || 'default'}]` + const storageKey = `autosave-v1[${mergedOpts.key || 'default'}]` const initializer = React.useMemo(() => { if (storageKey in storage) { const { value, timestamp } = JSON.parse(storage[storageKey]) - if (timestamp > opts.initialTimestamp) { + if (timestamp > mergedOpts.initialTimestamp) { console.info('Restoring value from storage', storageKey) return value } @@ -40,7 +47,7 @@ export default function useAutosavedState( }) valueRef.current.savePending = false console.info(`Saved value on key ${storageKey}`) - }, opts.debounceTime) + }, mergedOpts.debounceTime) } }, [value]) diff --git a/packages/web/hooks/useGetWindowSize.ts b/packages/web/hooks/useGetWindowSize.ts new file mode 100644 index 000000000..20db3234c --- /dev/null +++ b/packages/web/hooks/useGetWindowSize.ts @@ -0,0 +1,42 @@ +import { useState, useEffect, useMemo } from 'react' + +/** + * A simple hook to get dimensions of window. + * Handles tracking and returning updated values on resizing. + * + * Example Usage: + * const windowSize = useGetWindowSize() + * + * if (windowSize.width === someBreakpoint) { + * // Show mobile view + * } + */ + +const useWindowSize = () => { + // Make sure we're client-side / not server-side + const isClient = typeof window === 'object' + + const [windowWidth, setWindowWidth] = useState(isClient ? window.innerWidth : null) + const [windowHeight, setWindowHeight] = useState(isClient ? window.innerHeight : null) + + const handleResize = () => { + setWindowWidth(window.innerWidth) + setWindowHeight(window.innerHeight) + } + + useEffect(() => { + if (!isClient) return + window.addEventListener('resize', handleResize) + return () => window.removeEventListener('resize', handleResize) + }, []) + + return useMemo( + () => ({ + width: windowWidth, + height: windowHeight, + }), + [windowWidth, windowHeight], + ) +} + +export default useWindowSize diff --git a/packages/web/hooks/usePlayPolyphonicSound.ts b/packages/web/hooks/usePlayPolyphonicSound.ts new file mode 100644 index 000000000..b5d772726 --- /dev/null +++ b/packages/web/hooks/usePlayPolyphonicSound.ts @@ -0,0 +1,23 @@ +import { useCallback, useMemo, useRef } from 'react' + +const usePlayPolyphonicSound = (sampleUrl: string, voiceCount: number) => { + if (typeof Audio === 'undefined') return () => {} + + const sampleIndex = useRef(0) + + const voices = useMemo(() => { + return new Array(voiceCount).fill(null).map(() => new Audio(sampleUrl)) + }, [sampleUrl, voiceCount]) + + const playSound = useCallback(() => { + const voice = voices[sampleIndex.current++ % voiceCount] + // Always rewind audio to beginning before playing + // This enables rapid replays even if the file was still playing + voice.currentTime = 0 + voice.play() + }, [voices]) + + return playSound +} + +export default usePlayPolyphonicSound diff --git a/packages/web/hooks/useWindowSize.ts b/packages/web/hooks/useWindowSize.ts deleted file mode 100644 index f02e4cab4..000000000 --- a/packages/web/hooks/useWindowSize.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { useState, useEffect } from 'react' - -type WindowSize = { - width: number - height: number -} - -const useWindowSize = (initialWidth = Infinity, initialHeight = Infinity): WindowSize => { - const isClient = typeof window === 'object' - - const getSize = (): WindowSize => { - return { - width: isClient ? window.innerWidth : initialWidth, - height: isClient ? window.innerHeight : initialHeight, - } - } - - const [windowSize, setWindowSize] = useState(getSize) - - useEffect((): (() => void) | void => { - if (!isClient) return - - function handleResize() { - setWindowSize(getSize()) - } - - window.addEventListener('resize', handleResize) - return () => window.removeEventListener('resize', handleResize) - }, []) - - return windowSize -} - -export default useWindowSize diff --git a/packages/web/hooks/userIntersectionObserver.ts b/packages/web/hooks/userIntersectionObserver.ts new file mode 100644 index 000000000..29f4c12cc --- /dev/null +++ b/packages/web/hooks/userIntersectionObserver.ts @@ -0,0 +1,56 @@ +import { useEffect, useState } from 'react' + +/** + * A hook to unable using an Intersection Observer. + * + * Wraps the logic in a try/catch because some browsers don't support IntersectionObserver + * and this would throw an error, rendering the app unusable for those users. + * * Currently only supports observing one node per usage, but could be modified to handle more. + * + * @param ref : Current code expects a div element + * @param rootMargin : String + * : CSS Margin syntax - sets an invisible box around the root + * : Example: '0px 0px 0px 0px' + * @returns Boolean : true if the DOM element crosses the threshold, otherwise false + */ + +type IntersectionObserverOptions = { + root?: HTMLElement | Document | null + rootMargin?: string + threshold?: number +} + +const useIntersectionObserver = ({ + root = null, + rootMargin = '0px', + threshold = 0, +}: IntersectionObserverOptions) => { + const [observedElementRef, setObservedElementRef] = useState(null) + const [isIntersecting, setIsIntersecting] = useState(false) + + useEffect(() => { + try { + const observer = new IntersectionObserver( + // Destrucure first entry since we only observe one node at a time + ([entry]) => { + setIsIntersecting(entry.isIntersecting) + }, + { root, rootMargin, threshold }, + ) + + if (observedElementRef) { + observer.observe(observedElementRef) + } + + return () => { + if (observedElementRef) { + observer.disconnect() + } + } + } catch (e) {} + }, [observedElementRef, root, rootMargin, threshold]) + + return [setObservedElementRef, isIntersecting] as const +} + +export default useIntersectionObserver diff --git a/packages/web/public/static/locales/de/common.json b/packages/web/public/static/locales/de/common.json index e9a8bb421..165c45ad7 100644 --- a/packages/web/public/static/locales/de/common.json +++ b/packages/web/public/static/locales/de/common.json @@ -47,8 +47,8 @@ "bodyOne": "Brauchst du Hilfe? Wir sind für dich da!", "bodyTwo": "Bitte sende eine E-Mail an ", "bodyThree": " mit einer detaillierten Beschreibung deines Problems und wir werden uns so schnell wie möglich bei dir melden!", - "bodyFour": "Have you seen concerning behavior on the platform? Please refer to our Terms of Service and always feel free to report anything you feel may be misaligned with our guidelines:", - "termsOfService": "Terms of Service", + "bodyFour": "Hast du beunruhigendes Verhalten auf der Plattform gesehen? Bitte bezieh dich auf unsere Allgemeinen Geschäftsbedingungen (Terms of Service) und melde alles, von dem du denkst, dass es unseren Richtlinien widerspricht:", + "termsOfService": "Allgemeine Geschäftsbedingungen", "footer": "Danke, dass du uns hilfst, Journaly zu einem besseren Ort für alle zu machen" }, "badge": { diff --git a/packages/web/public/static/locales/de/j-editor.json b/packages/web/public/static/locales/de/j-editor.json new file mode 100644 index 000000000..29039e3ec --- /dev/null +++ b/packages/web/public/static/locales/de/j-editor.json @@ -0,0 +1,3 @@ +{ + "typewriterSounds": "Schreibmaschinengeräusche" +} diff --git a/packages/web/public/static/locales/en/j-editor.json b/packages/web/public/static/locales/en/j-editor.json new file mode 100644 index 000000000..e1abeb9e7 --- /dev/null +++ b/packages/web/public/static/locales/en/j-editor.json @@ -0,0 +1,3 @@ +{ + "typewriterSounds": "Typewriter Sounds" +} diff --git a/packages/web/public/static/sounds/typewriter-key-sound.wav b/packages/web/public/static/sounds/typewriter-key-sound.wav new file mode 100644 index 000000000..bf7494433 Binary files /dev/null and b/packages/web/public/static/sounds/typewriter-key-sound.wav differ diff --git a/packages/web/public/static/sounds/typewriter-return-sound.wav b/packages/web/public/static/sounds/typewriter-return-sound.wav new file mode 100644 index 000000000..177a63707 Binary files /dev/null and b/packages/web/public/static/sounds/typewriter-return-sound.wav differ