diff --git a/src/components/tools/about.astro b/src/components/tools/about.astro new file mode 100644 index 0000000..1490962 --- /dev/null +++ b/src/components/tools/about.astro @@ -0,0 +1,34 @@ +--- +import { Container } from '../container'; + +interface Props { + text: string; +} + +const { text } = Astro.props; +--- + +
+ +

About This Tool

+

{text}

+
+
+ + diff --git a/src/components/tools/breathing/breathing.module.css b/src/components/tools/breathing/breathing.module.css new file mode 100644 index 0000000..fdbd99d --- /dev/null +++ b/src/components/tools/breathing/breathing.module.css @@ -0,0 +1 @@ +/* WIP */ diff --git a/src/components/tools/breathing/breathing.tsx b/src/components/tools/breathing/breathing.tsx new file mode 100644 index 0000000..cc54de4 --- /dev/null +++ b/src/components/tools/breathing/breathing.tsx @@ -0,0 +1,10 @@ +import { Container } from '@/components/container'; +import { Exercise } from './exercise'; + +export function BreathingExercises() { + return ( + + + + ); +} diff --git a/src/components/tools/breathing/exercise/exercise.module.css b/src/components/tools/breathing/exercise/exercise.module.css new file mode 100644 index 0000000..05b9baa --- /dev/null +++ b/src/components/tools/breathing/exercise/exercise.module.css @@ -0,0 +1,47 @@ +.exercise { + position: relative; + z-index: 1; + display: flex; + align-items: center; + justify-content: center; + padding: 75px 0; + margin-top: 12px; + background-color: var(--color-neutral-50); + border: 1px solid var(--color-neutral-200); + border-radius: 8px; + + & .phase { + font-family: var(--font-display); + font-size: var(--font-lg); + font-weight: 600; + } + + & .circle { + position: absolute; + top: 50%; + left: 50%; + z-index: -1; + height: 55%; + aspect-ratio: 1 / 1; + background-image: radial-gradient( + var(--color-neutral-50), + var(--color-neutral-100) + ); + border: 1px solid var(--color-neutral-200); + border-radius: 50%; + transform: translate(-50%, -50%); + } +} + +.selectBox { + width: 100%; + min-width: 0; + height: 45px; + padding: 0 12px; + margin-top: 8px; + font-size: var(--font-sm); + color: var(--color-foreground); + background-color: var(--color-neutral-100); + border: 1px solid var(--color-neutral-200); + border-radius: 8px; +} diff --git a/src/components/tools/breathing/exercise/exercise.tsx b/src/components/tools/breathing/exercise/exercise.tsx new file mode 100644 index 0000000..b3087ea --- /dev/null +++ b/src/components/tools/breathing/exercise/exercise.tsx @@ -0,0 +1,122 @@ +import { useState, useEffect } from 'react'; +import { motion } from 'framer-motion'; + +import styles from './exercise.module.css'; + +type Exercise = 'Box Breathing' | 'Resonant Breathing' | '4-7-8 Breathing'; +type Phase = 'inhale' | 'exhale' | 'holdInhale' | 'holdExhale'; + +export function Exercise() { + const [selectedExercise, setSelectedExercise] = + useState('4-7-8 Breathing'); + + const getAnimationPhases = ( + exercise: Exercise, + ): Array<'inhale' | 'holdInhale' | 'exhale' | 'holdExhale'> => { + switch (exercise) { + case 'Box Breathing': + return ['inhale', 'holdInhale', 'exhale', 'holdExhale']; + case 'Resonant Breathing': + return ['inhale', 'exhale']; + case '4-7-8 Breathing': + return ['inhale', 'holdInhale', 'exhale']; + default: + return ['inhale', 'holdInhale', 'exhale', 'holdExhale']; + } + }; + + const getAnimationDurations = (exercise: Exercise) => { + switch (exercise) { + case 'Box Breathing': + return { exhale: 4, holdExhale: 4, holdInhale: 4, inhale: 4 }; + case 'Resonant Breathing': + return { exhale: 5, inhale: 5 }; + case '4-7-8 Breathing': + return { exhale: 8, holdInhale: 7, inhale: 4 }; + default: + return { exhale: 4, holdExhale: 4, holdInhale: 4, inhale: 4 }; + } + }; + + const getLabel = (phase: Phase) => { + switch (phase) { + case 'inhale': + return 'Inhale'; + case 'exhale': + return 'Exhale'; + default: + return 'Hold'; + } + }; + + const [phase, setPhase] = useState('inhale'); + const [durations, setDurations] = useState( + getAnimationDurations(selectedExercise), + ); + + const animationVariants = { + exhale: { + transform: 'translate(-50%, -50%) scale(1)', + transition: { duration: durations.exhale }, + }, + holdExhale: { + transform: 'translate(-50%, -50%) scale(1)', + transition: { duration: durations.holdExhale || 4 }, + }, + holdInhale: { + transform: 'translate(-50%, -50%) scale(1.5)', + transition: { duration: durations.holdInhale || 4 }, + }, + inhale: { + transform: 'translate(-50%, -50%) scale(1.5)', + transition: { duration: durations.inhale }, + }, + }; + + useEffect(() => { + setDurations(getAnimationDurations(selectedExercise)); + }, [selectedExercise]); + + useEffect(() => { + const phases = getAnimationPhases(selectedExercise); + + let phaseIndex = 0; + + setPhase(phases[phaseIndex]); + + const interval = setInterval( + () => { + phaseIndex = (phaseIndex + 1) % phases.length; + + setPhase(phases[phaseIndex]); + }, + (durations[phases[phaseIndex]] || 4) * 1000, + ); + + return () => clearInterval(interval); + }, [selectedExercise, durations]); + + return ( + <> +
+ +

{getLabel(phase)}

+
+ + + + ); +} diff --git a/src/components/tools/breathing/exercise/index.ts b/src/components/tools/breathing/exercise/index.ts new file mode 100644 index 0000000..881062d --- /dev/null +++ b/src/components/tools/breathing/exercise/index.ts @@ -0,0 +1 @@ +export { Exercise } from './exercise'; diff --git a/src/components/tools/breathing/index.ts b/src/components/tools/breathing/index.ts new file mode 100644 index 0000000..dbd185a --- /dev/null +++ b/src/components/tools/breathing/index.ts @@ -0,0 +1 @@ +export { BreathingExercises } from './breathing'; diff --git a/src/components/tools/generics/button/button.module.css b/src/components/tools/generics/button/button.module.css new file mode 100644 index 0000000..a96e112 --- /dev/null +++ b/src/components/tools/generics/button/button.module.css @@ -0,0 +1,34 @@ +.button { + display: flex; + align-items: center; + justify-content: center; + width: 30px; + height: 30px; + font-size: var(--font-sm); + color: var(--color-foreground); + cursor: pointer; + background-color: var(--color-neutral-100); + border: 1px solid var(--color-neutral-200); + border-radius: 4px; + outline: none; + transition: 0.2s; + + &:focus-visible { + outline: 2px solid var(--color-neutral-400); + outline-offset: 2px; + } + + &:hover, + &:focus-visible { + background-color: var(--color-neutral-200); + } + + &.smallIcon { + font-size: var(--font-xsm); + } + + &:disabled { + cursor: not-allowed; + opacity: 0.4; + } +} diff --git a/src/components/tools/generics/button/button.tsx b/src/components/tools/generics/button/button.tsx new file mode 100644 index 0000000..5828ad6 --- /dev/null +++ b/src/components/tools/generics/button/button.tsx @@ -0,0 +1,33 @@ +import { Tooltip } from '@/components/tooltip'; + +import { cn } from '@/helpers/styles'; + +import styles from './button.module.css'; + +interface ButtonProps { + disabled?: boolean; + icon: React.ReactElement; + onClick: () => void; + smallIcon?: boolean; + tooltip: string; +} + +export function Button({ + disabled = false, + icon, + onClick, + smallIcon, + tooltip, +}: ButtonProps) { + return ( + + + + ); +} diff --git a/src/components/tools/generics/button/index.ts b/src/components/tools/generics/button/index.ts new file mode 100644 index 0000000..a039b75 --- /dev/null +++ b/src/components/tools/generics/button/index.ts @@ -0,0 +1 @@ +export { Button } from './button'; diff --git a/src/components/tools/hero.astro b/src/components/tools/hero.astro new file mode 100644 index 0000000..a502ae4 --- /dev/null +++ b/src/components/tools/hero.astro @@ -0,0 +1,114 @@ +--- +import { Container } from '../container'; + +interface Props { + desc: string; + title: string; +} + +const { desc, title } = Astro.props; +--- + +
+ +
+
+ + +

{title}

+

{desc}

+ +

Part of Moodist

+
+
+
+ + diff --git a/src/components/tools/notepad/button/button.module.css b/src/components/tools/notepad/button/button.module.css new file mode 100644 index 0000000..4a4e756 --- /dev/null +++ b/src/components/tools/notepad/button/button.module.css @@ -0,0 +1,45 @@ +.button { + display: flex; + align-items: center; + justify-content: center; + width: 30px; + height: 30px; + font-size: var(--font-sm); + color: var(--color-foreground); + cursor: pointer; + background-color: var(--color-neutral-100); + border: 1px solid var(--color-neutral-200); + border-radius: 4px; + outline: none; + transition: 0.2s; + transition-property: border-color, color, background-color; + + &:focus-visible { + outline: 2px solid var(--color-neutral-400); + outline-offset: 2px; + } + + &.critical { + color: #f43f5e; + border-color: #f43f5e; + + &:hover { + background-color: rgb(244 63 94 / 10%); + } + } + + &.recommended { + font-size: var(--font-xsm); + color: #22c55e; + border-color: #22c55e; + + &:hover { + background-color: rgb(34 197 94 / 10%); + } + } + + &:hover, + &:focus-visible { + background-color: var(--color-neutral-200); + } +} diff --git a/src/components/tools/notepad/button/button.tsx b/src/components/tools/notepad/button/button.tsx new file mode 100644 index 0000000..d3da910 --- /dev/null +++ b/src/components/tools/notepad/button/button.tsx @@ -0,0 +1,36 @@ +import { Tooltip } from '@/components/tooltip'; + +import { cn } from '@/helpers/styles'; + +import styles from './button.module.css'; + +interface ButtonProps { + critical?: boolean; + icon: React.ReactElement; + onClick: () => void; + recommended?: boolean; + tooltip: string; +} + +export function Button({ + critical, + icon, + onClick, + recommended, + tooltip, +}: ButtonProps) { + return ( + + + + ); +} diff --git a/src/components/tools/notepad/button/index.ts b/src/components/tools/notepad/button/index.ts new file mode 100644 index 0000000..a039b75 --- /dev/null +++ b/src/components/tools/notepad/button/index.ts @@ -0,0 +1 @@ +export { Button } from './button'; diff --git a/src/components/tools/notepad/index.ts b/src/components/tools/notepad/index.ts new file mode 100644 index 0000000..8a3fad5 --- /dev/null +++ b/src/components/tools/notepad/index.ts @@ -0,0 +1 @@ +export { Notepad } from './notepad'; diff --git a/src/components/tools/notepad/notepad.module.css b/src/components/tools/notepad/notepad.module.css new file mode 100644 index 0000000..2dadc40 --- /dev/null +++ b/src/components/tools/notepad/notepad.module.css @@ -0,0 +1,44 @@ +.header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; + + & .label { + font-size: var(--font-sm); + font-weight: 500; + color: var(--color-foreground-subtle); + } + + & .buttons { + display: flex; + column-gap: 4px; + align-items: center; + } +} + +.textarea { + width: 100%; + height: 350px; + padding: 12px; + line-height: 1.6; + color: var(--color-foreground-subtle); + resize: none; + background-color: var(--color-neutral-50); + border: 1px solid var(--color-neutral-200); + border-radius: 4px; + outline: none; + scroll-padding-bottom: 12px; + + &:focus-visible { + outline: 2px solid var(--color-neutral-400); + outline-offset: 2px; + } +} + +.counter { + margin-top: 8px; + font-size: var(--font-xsm); + color: var(--color-foreground-subtle); + text-align: center; +} diff --git a/src/components/tools/notepad/notepad.tsx b/src/components/tools/notepad/notepad.tsx new file mode 100644 index 0000000..921556f --- /dev/null +++ b/src/components/tools/notepad/notepad.tsx @@ -0,0 +1,90 @@ +import { useRef, useEffect } from 'react'; +import { BiTrash } from 'react-icons/bi/index'; +import { LuCopy, LuDownload } from 'react-icons/lu/index'; +import { FaCheck } from 'react-icons/fa6/index'; +import { FaUndo } from 'react-icons/fa/index'; + +import { Modal } from '@/components/modal'; +import { Button } from './button'; + +import { useNoteStore } from '@/stores/note'; +import { useCopy } from '@/hooks/use-copy'; +import { download } from '@/helpers/download'; + +import styles from './notepad.module.css'; + +interface NotepadProps { + onClose: () => void; + show: boolean; +} + +export function Notepad({ onClose, show }: NotepadProps) { + const textareaRef = useRef(null); + + const note = useNoteStore(state => state.note); + const history = useNoteStore(state => state.history); + const write = useNoteStore(state => state.write); + const words = useNoteStore(state => state.words()); + const characters = useNoteStore(state => state.characters()); + const clear = useNoteStore(state => state.clear); + const restore = useNoteStore(state => state.restore); + + const { copy, copying } = useCopy(); + + useEffect(() => { + if (show && textareaRef.current) { + setTimeout(() => { + textareaRef.current?.focus(); + }, 10); + } + }, [show]); + + const handleKeyDown = (e: React.KeyboardEvent) => { + e.stopPropagation(); + + if (e.key === 'Escape') onClose(); + }; + + return ( + +
+

Your Note

+
+
+
+ +