From b1efc0ebf18fcb88f81263dd2680f45a47433443 Mon Sep 17 00:00:00 2001 From: sina_sajjadi Date: Sat, 21 Dec 2024 19:15:04 +0330 Subject: [PATCH] feat: add new motion utilities and enhance UI components --- package-lock.json | 71 +++++++ package.json | 4 + public/assets/images/Group 1000005169.svg | 44 ++++ public/assets/images/Group 1000005170.svg | 4 + public/assets/images/Search.svg | 4 + public/assets/images/Setting.svg | 4 +- public/assets/images/WhiteSetting.svg | 6 + public/assets/images/hamburgure.svg | 5 + public/assets/images/islamic-pattern3.svg | 11 + .../context/font-setting-context.tsx | 79 +++++++ src/components/context/ui.context.tsx | 90 ++++++++ src/components/layout/header.tsx | 10 +- src/components/layout/mobile-header.tsx | 44 ++++ src/components/layout/sidebar.tsx | 2 +- src/components/modals/modal-manager.tsx | 30 +++ src/components/modals/modal.tsx | 117 ++++++++++ src/components/modals/search-modal.tsx | 29 +++ src/components/sidebar/list.tsx | 6 +- src/components/sidebar/tabs.tsx | 4 +- src/components/ui/check-box.tsx | 25 +++ src/components/ui/download-app.tsx | 40 ++++ src/components/ui/footer-sticky.tsx | 39 ---- src/components/ui/mobile-search.tsx | 15 ++ src/components/ui/mobile-setting.tsx | 16 ++ src/components/ui/range-input.tsx | 53 +++++ src/components/ui/setting.tsx | 199 ++++++++++++++++++ src/components/utils/colorize-vowels.tsx | 4 +- src/pages/_app.tsx | 19 +- src/pages/_document.tsx | 2 +- src/pages/duas/[slug].tsx | 147 ++++++++----- src/pages/last-reads.tsx | 2 +- src/styles/globals.css | 7 - src/utils/motion/fade-in-left.ts | 18 ++ src/utils/motion/fade-in-out.ts | 18 ++ src/utils/motion/fade-in-right.ts | 18 ++ src/utils/motion/fade-in-top.ts | 22 ++ src/utils/motion/height-collapse.ts | 18 ++ src/utils/motion/zoom-out-in.ts | 18 ++ src/utils/use-click-outside.ts | 27 +++ tailwind.config.ts | 7 + 40 files changed, 1159 insertions(+), 119 deletions(-) create mode 100644 public/assets/images/Group 1000005169.svg create mode 100644 public/assets/images/Group 1000005170.svg create mode 100644 public/assets/images/Search.svg create mode 100644 public/assets/images/WhiteSetting.svg create mode 100644 public/assets/images/hamburgure.svg create mode 100644 public/assets/images/islamic-pattern3.svg create mode 100644 src/components/context/font-setting-context.tsx create mode 100644 src/components/context/ui.context.tsx create mode 100644 src/components/layout/mobile-header.tsx create mode 100644 src/components/modals/modal-manager.tsx create mode 100644 src/components/modals/modal.tsx create mode 100644 src/components/modals/search-modal.tsx create mode 100644 src/components/ui/check-box.tsx create mode 100644 src/components/ui/download-app.tsx delete mode 100644 src/components/ui/footer-sticky.tsx create mode 100644 src/components/ui/mobile-search.tsx create mode 100644 src/components/ui/mobile-setting.tsx create mode 100644 src/components/ui/range-input.tsx create mode 100644 src/components/ui/setting.tsx create mode 100644 src/utils/motion/fade-in-left.ts create mode 100644 src/utils/motion/fade-in-out.ts create mode 100644 src/utils/motion/fade-in-right.ts create mode 100644 src/utils/motion/fade-in-top.ts create mode 100644 src/utils/motion/height-collapse.ts create mode 100644 src/utils/motion/zoom-out-in.ts create mode 100644 src/utils/use-click-outside.ts diff --git a/package-lock.json b/package-lock.json index e6f1b58..3eff8af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,11 @@ "name": "duas", "version": "0.1.0", "dependencies": { + "@reach/portal": "^0.18.0", "axios": "^1.7.9", + "body-scroll-lock": "^4.0.0-beta.0", + "classnames": "^2.5.1", + "framer-motion": "^11.15.0", "moment": "^2.30.1", "moment-hijri": "^3.0.0", "next": "15.1.0", @@ -825,6 +829,27 @@ "node": ">=14" } }, + "node_modules/@reach/portal": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@reach/portal/-/portal-0.18.0.tgz", + "integrity": "sha512-TImozRapd576ofRk30Le2L3lRTFXF1p47B182wnp5eMTdZa74JX138BtNGEPJFOyrMaVmguVF8SSwZ6a0fon1Q==", + "dependencies": { + "@reach/utils": "0.18.0" + }, + "peerDependencies": { + "react": "^16.8.0 || 17.x", + "react-dom": "^16.8.0 || 17.x" + } + }, + "node_modules/@reach/utils": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@reach/utils/-/utils-0.18.0.tgz", + "integrity": "sha512-KdVMdpTgDyK8FzdKO9SCpiibuy/kbv3pwgfXshTI6tEcQT1OOwj7BAksnzGC0rPz0UholwC+AgkqEl3EJX3M1A==", + "peerDependencies": { + "react": "^16.8.0 || 17.x", + "react-dom": "^16.8.0 || 17.x" + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -1475,6 +1500,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/body-scroll-lock": { + "version": "4.0.0-beta.0", + "resolved": "https://registry.npmjs.org/body-scroll-lock/-/body-scroll-lock-4.0.0-beta.0.tgz", + "integrity": "sha512-a7tP5+0Mw3YlUJcGAKUqIBkYYGlYxk2fnCasq/FUph1hadxlTRjF+gAcZksxANnaMnALjxEddmSi/H3OR8ugcQ==" + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1644,6 +1674,11 @@ "node": ">= 6" } }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -2713,6 +2748,32 @@ "node": ">= 6" } }, + "node_modules/framer-motion": { + "version": "11.15.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.15.0.tgz", + "integrity": "sha512-MLk8IvZntxOMg7lDBLw2qgTHHv664bYoYmnFTmE0Gm/FW67aOJk0WM3ctMcG+Xhcv+vh5uyyXwxvxhSeJzSe+w==", + "dependencies": { + "motion-dom": "^11.14.3", + "motion-utils": "^11.14.3", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -3753,6 +3814,16 @@ "moment": "^2.30.1" } }, + "node_modules/motion-dom": { + "version": "11.14.3", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.14.3.tgz", + "integrity": "sha512-lW+D2wBy5vxLJi6aCP0xyxTxlTfiu+b+zcpVbGVFUxotwThqhdpPRSmX8xztAgtZMPMeU0WGVn/k1w4I+TbPqA==" + }, + "node_modules/motion-utils": { + "version": "11.14.3", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.14.3.tgz", + "integrity": "sha512-Xg+8xnqIJTpr0L/cidfTTBFkvRw26ZtGGuIhA94J9PQ2p4mEa06Xx7QVYZH0BP+EpMSaDlu+q0I0mmvwADPsaQ==" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", diff --git a/package.json b/package.json index af7c1e3..bbe2f4b 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,11 @@ "lint": "next lint" }, "dependencies": { + "@reach/portal": "^0.18.0", "axios": "^1.7.9", + "body-scroll-lock": "^4.0.0-beta.0", + "classnames": "^2.5.1", + "framer-motion": "^11.15.0", "moment": "^2.30.1", "moment-hijri": "^3.0.0", "next": "15.1.0", diff --git a/public/assets/images/Group 1000005169.svg b/public/assets/images/Group 1000005169.svg new file mode 100644 index 0000000..adaaf86 --- /dev/null +++ b/public/assets/images/Group 1000005169.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/images/Group 1000005170.svg b/public/assets/images/Group 1000005170.svg new file mode 100644 index 0000000..c313d34 --- /dev/null +++ b/public/assets/images/Group 1000005170.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/assets/images/Search.svg b/public/assets/images/Search.svg new file mode 100644 index 0000000..4120852 --- /dev/null +++ b/public/assets/images/Search.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/assets/images/Setting.svg b/public/assets/images/Setting.svg index ae4e9cd..45cefdb 100644 --- a/public/assets/images/Setting.svg +++ b/public/assets/images/Setting.svg @@ -1,6 +1,6 @@ - + - + diff --git a/public/assets/images/WhiteSetting.svg b/public/assets/images/WhiteSetting.svg new file mode 100644 index 0000000..ae4e9cd --- /dev/null +++ b/public/assets/images/WhiteSetting.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/assets/images/hamburgure.svg b/public/assets/images/hamburgure.svg new file mode 100644 index 0000000..5a288d1 --- /dev/null +++ b/public/assets/images/hamburgure.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/assets/images/islamic-pattern3.svg b/public/assets/images/islamic-pattern3.svg new file mode 100644 index 0000000..259d1f2 --- /dev/null +++ b/public/assets/images/islamic-pattern3.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/components/context/font-setting-context.tsx b/src/components/context/font-setting-context.tsx new file mode 100644 index 0000000..421fab0 --- /dev/null +++ b/src/components/context/font-setting-context.tsx @@ -0,0 +1,79 @@ +// context/FontSettingsContext.tsx +import React, { + createContext, + useContext, + useState, + useEffect, + ReactNode, +} from "react"; + +interface FontSettings { + arabic: boolean; + arabicRange: number; + translation: boolean; + translationRange: number; + transliteration: boolean; + transliterationRange: number; +} + +interface FontSettingsContextProps { + fontSettings: FontSettings; + setFontSettings: React.Dispatch>; +} + +const DEFAULT_FONT_SETTINGS: FontSettings = { + arabic: true, + arabicRange: 100, + translation: true, + translationRange: 100, + transliteration: true, + transliterationRange: 100, +}; + +const FontSettingsContext = createContext( + undefined +); + +export const FontSettingsProvider: React.FC<{ children: ReactNode }> = ({ + children, +}) => { + const [fontSettings, setFontSettings] = useState(() => { + if (typeof window !== "undefined") { + const savedSettings = localStorage.getItem("font-setting"); + if (savedSettings) { + try { + return JSON.parse(savedSettings); + } catch (error) { + console.error( + "Failed to parse font settings from localStorage:", + error + ); + return DEFAULT_FONT_SETTINGS; + } + } + } + return DEFAULT_FONT_SETTINGS; + }); + + useEffect(() => { + if (typeof window !== "undefined") { + localStorage.setItem("font-setting", JSON.stringify(fontSettings)); + } + }, [fontSettings]); + + return ( + + {children} + + ); +}; + +export const useFontSettingsContext = (): FontSettingsContextProps => { + const context = useContext(FontSettingsContext); + if (!context) { + throw new Error( + "useFontSettingsContext must be used within a FontSettingsProvider" + ); + } + return context; +}; diff --git a/src/components/context/ui.context.tsx b/src/components/context/ui.context.tsx new file mode 100644 index 0000000..f223331 --- /dev/null +++ b/src/components/context/ui.context.tsx @@ -0,0 +1,90 @@ +import React, { createContext, useContext, ReactNode } from "react"; + +export interface UIContextProps { + displaySidebar: boolean; + displayDownload: boolean; + displaySetting: boolean; + displayModal: boolean; + modalView: string | null; + toastMessage: string | null; +} + +const initialState = { + displaySidebar: false, + displaySetting: false, + displayDownload: true, + displayModal: false, + modalView: null, + toastMessage: null, +}; + +type Action = + | { type: "OPEN_SIDEBAR" } + | { type: "CLOSE_SIDEBAR" } + | { type: "OPEN_SETTING" } + | { type: "CLOSE_SETTING" } + | { type: "CLOSE_DOWNLOAD" } + | { type: "OPEN_MODAL"; view: string } + | { type: "CLOSE_MODAL" } + | { type: "SET_TOAST_MESSAGE"; message: string }; + +const UIContext = createContext(undefined); + +function uiReducer(state: typeof initialState, action: Action) { + switch (action.type) { + case "OPEN_SIDEBAR": + return { ...state, displaySidebar: true }; + case "CLOSE_SIDEBAR": + return { ...state, displaySidebar: false }; + case "OPEN_SETTING": + return { ...state, displaySetting: true }; + case "CLOSE_SETTING": + return { ...state, displaySetting: false }; + case "CLOSE_DOWNLOAD": + return { ...state, displayDownload: false }; + case "OPEN_MODAL": + return { ...state, displayModal: true, modalView: action.view }; + case "CLOSE_MODAL": + return { ...state, displayModal: false, modalView: null }; + case "SET_TOAST_MESSAGE": + return { ...state, toastMessage: action.message }; + default: + return state; + } +} + +export const UIProvider: React.FC<{ children: ReactNode }> = ({ children }) => { + const [state, dispatch] = React.useReducer(uiReducer, initialState); + + const openSidebar = () => dispatch({ type: "OPEN_SIDEBAR" }); + const closeSidebar = () => dispatch({ type: "CLOSE_SIDEBAR" }); + const openSetting = () => dispatch({ type: "OPEN_SETTING" }); + const closeSetting = () => dispatch({ type: "CLOSE_SETTING" }); + const closeDownload = () => dispatch({ type: "CLOSE_DOWNLOAD" }); + const openModal = (view: string) => {dispatch({ type: "OPEN_MODAL", view })}; + const closeModal = () => dispatch({ type: "CLOSE_MODAL" }); + const setToastMessage = (message: string) => + dispatch({ type: "SET_TOAST_MESSAGE", message }); + + const value = { + ...state, + openSidebar, + closeSidebar, + openModal, + closeModal, + setToastMessage, + closeDownload, + openSetting, + closeSetting, + }; + + return {children}; +}; + +export const useUI = (): UIContextProps => { + const context = useContext(UIContext); + if (context === undefined) { + throw new Error("useUI must be used within a UIProvider"); + } + return context; +}; diff --git a/src/components/layout/header.tsx b/src/components/layout/header.tsx index 3e38ad1..3e5a5bd 100644 --- a/src/components/layout/header.tsx +++ b/src/components/layout/header.tsx @@ -3,9 +3,11 @@ import Image from "next/image"; import Link from "next/link"; import LanguageSwitcher from "../language-switcher"; import SearchDuas from "../ui/search-duas"; +import { useUI } from "../context/ui.context"; const Header = () => { + const {displayDownload} = useUI() return ( -
+