From 337ed20ed541b11d1f3b4ba479cc7046bac99067 Mon Sep 17 00:00:00 2001 From: Mordechai Dror Date: Wed, 19 Jun 2024 17:49:15 +0300 Subject: [PATCH] prettify hooks --- src/App.tsx | 22 +++--- src/dashboard/pages/dashboard.page.tsx | 6 +- src/date/types/month-name.ts | 16 ++++ src/date/types/month.ts | 19 +---- src/db/globals/db.ts | 2 +- src/router/hooks/use-nav-links.tsx | 40 ++++++++++ src/router/providers/nav-links.provider.tsx | 32 -------- .../components/add-subscription-button.tsx | 6 +- .../components/subscription-list-item.tsx | 12 ++- .../components/subscription-list.tsx | 6 +- .../components/subscription-upsert.tsx | 20 +++-- .../subscriptions-by-month-chart.tsx | 15 ++-- .../use-subscription-upsert.tsx} | 78 ++++++++++--------- .../use-subscriptions.tsx} | 21 +++-- .../models/subscription-tag.model.ts | 5 +- src/subscriptions/models/subscription.mock.ts | 2 +- .../models/subscription.model.ts | 32 ++++++++ .../models/subscription.table.ts | 2 +- .../pages/subscriptions.page.tsx | 6 +- .../types/subscription-cycle-period.ts | 7 ++ .../subscription-icon.tsx} | 38 --------- ...-period-to-calculate-monthly-price.spec.ts | 2 +- ...cycle-period-to-calculate-monthly-price.ts | 6 +- src/tags/components/tag-select.tsx | 6 +- src/tags/models/tag.model.ts | 2 +- src/ui/hooks/use-breakpoint.ts | 15 +--- src/ui/hooks/use-default-layout.tsx | 56 +++++++++++++ src/ui/layouts/default.layout.tsx | 65 ++-------------- src/ui/types/breakpoint.ts | 10 +++ 29 files changed, 290 insertions(+), 259 deletions(-) create mode 100644 src/date/types/month-name.ts create mode 100644 src/router/hooks/use-nav-links.tsx delete mode 100644 src/router/providers/nav-links.provider.tsx rename src/subscriptions/{providers/subscription-upsert-state.provider.tsx => hooks/use-subscription-upsert.tsx} (81%) rename src/subscriptions/{providers/subscriptions.provider.tsx => hooks/use-subscriptions.tsx} (88%) create mode 100644 src/subscriptions/models/subscription.model.ts create mode 100644 src/subscriptions/types/subscription-cycle-period.ts rename src/subscriptions/{models/subscription.model.tsx => types/subscription-icon.tsx} (58%) create mode 100644 src/ui/hooks/use-default-layout.tsx create mode 100644 src/ui/types/breakpoint.ts diff --git a/src/App.tsx b/src/App.tsx index de7f905..d92bc3a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,10 +1,10 @@ import { - NavLinksContextProvider, + NavLinksProvider, type NavLink, -} from '@/router/providers/nav-links.provider.tsx'; -import { SubscriptionUpsertStateProvider } from '@/subscriptions/providers/subscription-upsert-state.provider'; -import { SubscriptionsProvider } from '@/subscriptions/providers/subscriptions.provider.tsx'; -import { DefaultLayoutContextProvider } from '@/ui/layouts/default.layout'; +} from '@/router/hooks/use-nav-links.tsx'; +import { SubscriptionUpsertProvider } from '@/subscriptions/hooks/use-subscription-upsert.tsx'; +import { SubscriptionsProvider } from '@/subscriptions/hooks/use-subscriptions.tsx'; +import { DefaultLayoutProvider } from '@/ui/hooks/use-default-layout.tsx'; import { faChartSimple, faCreditCard } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { memo } from 'react'; @@ -12,15 +12,15 @@ import { Outlet } from 'react-router-dom'; export const App = memo(() => { return ( - - - + + + - - - + + + ); }); diff --git a/src/dashboard/pages/dashboard.page.tsx b/src/dashboard/pages/dashboard.page.tsx index 655b9e4..0556e66 100644 --- a/src/dashboard/pages/dashboard.page.tsx +++ b/src/dashboard/pages/dashboard.page.tsx @@ -1,17 +1,17 @@ import { AddSubscriptionButton } from '@/subscriptions/components/add-subscription-button.tsx'; import { SubscriptionUpsert } from '@/subscriptions/components/subscription-upsert.tsx'; import { SubscriptionsByMonthChart } from '@/subscriptions/components/subscriptions-by-month-chart.tsx'; -import { SubscriptionUpsertStateContext } from '@/subscriptions/providers/subscription-upsert-state.provider.tsx'; +import { useSubscriptionUpsert } from '@/subscriptions/hooks/use-subscription-upsert'; import { TagSelect } from '@/tags/components/tag-select.tsx'; import { DefaultLayout, DefaultLayoutHeader, } from '@/ui/layouts/default.layout.tsx'; import { cn } from '@/ui/utils/cn.ts'; -import { memo, useContext } from 'react'; +import { memo } from 'react'; export const DashboardPage = memo(() => { - const upsert = useContext(SubscriptionUpsertStateContext); + const upsert = useSubscriptionUpsert(); return ( ; +} + +export interface NavLink { + label: string; + path: Route | string; + icon: ReactNode; +} + +export const NavLinksProvider = memo( + ({ children, navLinks }: PropsWithChildren) => { + return ( + + {children} + + ); + }, +); + +export interface NavLinksProviderProps { + navLinks: Array; +} + +const NavLinksContext = createContext({ + navLinks: [], +}); diff --git a/src/router/providers/nav-links.provider.tsx b/src/router/providers/nav-links.provider.tsx deleted file mode 100644 index 51a2582..0000000 --- a/src/router/providers/nav-links.provider.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { - createContext, - memo, - type PropsWithChildren, - type ReactNode, -} from 'react'; - -export const NavLinksContextProvider = memo( - ({ children, navLinks }: PropsWithChildren) => { - return ( - - {children} - - ); - }, -); - -export interface NavLinksContextProviderProps { - navLinks: Array; -} - -export const NavLinksContext = createContext<{ - navLinks: Array; -}>({ - navLinks: [], -}); - -export interface NavLink { - label: string; - path: string; - icon: ReactNode; -} diff --git a/src/subscriptions/components/add-subscription-button.tsx b/src/subscriptions/components/add-subscription-button.tsx index 54c7ac5..bcc3f79 100644 --- a/src/subscriptions/components/add-subscription-button.tsx +++ b/src/subscriptions/components/add-subscription-button.tsx @@ -2,11 +2,11 @@ import { useBreakpoint } from '@/ui/hooks/use-breakpoint.ts'; import { faPlus } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { ActionIcon, Button } from '@mantine/core'; -import { memo, useCallback, useContext } from 'react'; -import { SubscriptionUpsertStateContext } from '../providers/subscription-upsert-state.provider.tsx'; +import { memo, useCallback } from 'react'; +import { useSubscriptionUpsert } from '../hooks/use-subscription-upsert.tsx'; export const AddSubscriptionButton = memo(() => { - const upsert = useContext(SubscriptionUpsertStateContext); + const upsert = useSubscriptionUpsert(); const isMd = useBreakpoint('md'); const openSubscriptionInsert = useCallback( diff --git a/src/subscriptions/components/subscription-list-item.tsx b/src/subscriptions/components/subscription-list-item.tsx index 59bb81a..b5d6cfc 100644 --- a/src/subscriptions/components/subscription-list-item.tsx +++ b/src/subscriptions/components/subscription-list-item.tsx @@ -1,15 +1,13 @@ import { cn } from '@/ui/utils/cn.ts'; import { Avatar, Card, Text, Title } from '@mantine/core'; -import { memo, useCallback, useContext } from 'react'; -import { - subscriptionIconToSvg, - type SubscriptionModel, -} from '../models/subscription.model.tsx'; -import { SubscriptionUpsertStateContext } from '../providers/subscription-upsert-state.provider.tsx'; +import { memo, useCallback } from 'react'; +import { useSubscriptionUpsert } from '../hooks/use-subscription-upsert.tsx'; +import { type SubscriptionModel } from '../models/subscription.model.ts'; +import { subscriptionIconToSvg } from '../types/subscription-icon.tsx'; export const SubscriptionListItem = memo( ({ subscription }: SubscriptionListItemProps) => { - const upsert = useContext(SubscriptionUpsertStateContext); + const upsert = useSubscriptionUpsert(); const openSubscriptionUpdate = useCallback( () => upsert.dispatch({ type: 'open', subscription }), diff --git a/src/subscriptions/components/subscription-list.tsx b/src/subscriptions/components/subscription-list.tsx index 1109dfa..498557e 100644 --- a/src/subscriptions/components/subscription-list.tsx +++ b/src/subscriptions/components/subscription-list.tsx @@ -1,10 +1,10 @@ import { cn } from '@/ui/utils/cn.ts'; -import { memo, useContext } from 'react'; -import { SubscriptionsContext } from '../providers/subscriptions.provider.tsx'; +import { memo } from 'react'; +import { useSubscriptions } from '../hooks/use-subscriptions.tsx'; import { SubscriptionListItem } from './subscription-list-item.tsx'; export const SubscriptionList = memo(() => { - const { subscriptions } = useContext(SubscriptionsContext); + const { subscriptions } = useSubscriptions(); return (
diff --git a/src/subscriptions/components/subscription-upsert.tsx b/src/subscriptions/components/subscription-upsert.tsx index 3c6c51c..6f1e69e 100644 --- a/src/subscriptions/components/subscription-upsert.tsx +++ b/src/subscriptions/components/subscription-upsert.tsx @@ -14,33 +14,37 @@ import { } from '@mantine/core'; import { DatePickerInput } from '@mantine/dates'; import { useLiveQuery } from 'dexie-react-hooks'; -import { memo, useCallback, useContext, useMemo } from 'react'; +import { memo, useCallback, useMemo } from 'react'; import { Controller, useForm, type DefaultValues, type SubmitHandler, } from 'react-hook-form'; +import { useSubscriptionUpsert } from '../hooks/use-subscription-upsert.tsx'; import { insertSubscriptionSchema, - subscriptionCyclePeriodToLabel, - subscriptionCyclePeriods, - subscriptionIconToLabel, - subscriptionIcons, updateSubscriptionSchema, type InsertSubscriptionModel, type UpdateSubscriptionModel, type UpsertSubscriptionModel, -} from '../models/subscription.model.tsx'; +} from '../models/subscription.model.ts'; import { deleteSubscription, insertSubscription, updateSubscription, } from '../models/subscription.table.ts'; -import { SubscriptionUpsertStateContext } from '../providers/subscription-upsert-state.provider.tsx'; +import { + subscriptionCyclePeriodToLabel, + subscriptionCyclePeriods, +} from '../types/subscription-cycle-period.ts'; +import { + subscriptionIconToLabel, + subscriptionIcons, +} from '../types/subscription-icon.tsx'; export const SubscriptionUpsert = memo(() => { - const { state, dispatch } = useContext(SubscriptionUpsertStateContext); + const { state, dispatch } = useSubscriptionUpsert(); const { register, handleSubmit, control } = useForm({ resolver: zodResolver( diff --git a/src/subscriptions/components/subscriptions-by-month-chart.tsx b/src/subscriptions/components/subscriptions-by-month-chart.tsx index 71f43ff..074d77d 100644 --- a/src/subscriptions/components/subscriptions-by-month-chart.tsx +++ b/src/subscriptions/components/subscriptions-by-month-chart.tsx @@ -1,18 +1,15 @@ -import { - monthToMonthName, - months, - type MonthName, -} from '@/date/types/month.ts'; +import type { MonthName } from '@/date/types/month-name.ts'; +import { monthToMonthName, months } from '@/date/types/month.ts'; import { cn } from '@/ui/utils/cn.ts'; import { Card } from '@mantine/core'; -import { memo, useContext, useMemo } from 'react'; +import { memo, useMemo } from 'react'; import { Bar, BarChart, ResponsiveContainer, XAxis } from 'recharts'; -import type { SubscriptionModel } from '../models/subscription.model.tsx'; -import { SubscriptionsContext } from '../providers/subscriptions.provider.tsx'; +import { useSubscriptions } from '../hooks/use-subscriptions.tsx'; +import type { SubscriptionModel } from '../models/subscription.model.ts'; import { cyclePeriodToCalculateMonthlyPrice } from '../utils/cycle-period-to-calculate-monthly-price.ts'; export const SubscriptionsByMonthChart = memo(() => { - const { subscriptions } = useContext(SubscriptionsContext); + const { subscriptions } = useSubscriptions(); const aggregatedSubscriptions = useMemo( () => Object.entries(aggregateSubscriptionsByMonth(subscriptions ?? [])), diff --git a/src/subscriptions/providers/subscription-upsert-state.provider.tsx b/src/subscriptions/hooks/use-subscription-upsert.tsx similarity index 81% rename from src/subscriptions/providers/subscription-upsert-state.provider.tsx rename to src/subscriptions/hooks/use-subscription-upsert.tsx index 06374c0..419a44b 100644 --- a/src/subscriptions/providers/subscription-upsert-state.provider.tsx +++ b/src/subscriptions/hooks/use-subscription-upsert.tsx @@ -1,4 +1,4 @@ -import { DefaultLayoutContext } from '@/ui/layouts/default.layout.tsx'; +import { useDefaultLayout } from '@/ui/hooks/use-default-layout.tsx'; import { usePrevious } from '@mantine/hooks'; import { createContext, @@ -10,11 +10,46 @@ import { type PropsWithChildren, type Reducer, } from 'react'; -import type { SubscriptionModel } from '../models/subscription.model.tsx'; +import type { SubscriptionModel } from '../models/subscription.model.ts'; -export const SubscriptionUpsertStateProvider = memo( +export function useSubscriptionUpsert() { + return useContext(SubscriptionUpsertContext); +} + +export interface UseSubscriptionUpsert { + state: SubscriptionUpsertState; + dispatch: Dispatch; +} + +export type SubscriptionUpsertState = + | { + subscription: SubscriptionModel; + mode: 'update'; + } + | { + mode: 'insert'; + } + | { + subscription: null; + mode: null; + }; + +export interface SubscriptionUpsertOpenAction { + type: 'open'; + subscription?: SubscriptionModel | null; +} + +export interface SubscriptionUpsertCloseAction { + type: 'close'; +} + +export type SubscriptionUpsertAction = + | SubscriptionUpsertOpenAction + | SubscriptionUpsertCloseAction; + +export const SubscriptionUpsertProvider = memo( ({ children }: PropsWithChildren) => { - const layout = useContext(DefaultLayoutContext); + const layout = useDefaultLayout(); const prevLayout = usePrevious(layout); const [state, dispatch] = useReducer< @@ -56,17 +91,14 @@ export const SubscriptionUpsertStateProvider = memo( }, [layout, prevLayout, prevState, state]); return ( - + {children} - + ); }, ); -export const SubscriptionUpsertStateContext = createContext<{ - state: SubscriptionUpsertState; - dispatch: Dispatch; -}>({ +const SubscriptionUpsertContext = createContext({ state: { subscription: null, mode: null, @@ -74,32 +106,6 @@ export const SubscriptionUpsertStateContext = createContext<{ dispatch: () => {}, }); -export type SubscriptionUpsertState = - | { - subscription: SubscriptionModel; - mode: 'update'; - } - | { - mode: 'insert'; - } - | { - subscription: null; - mode: null; - }; - -export interface SubscriptionUpsertOpenAction { - type: 'open'; - subscription?: SubscriptionModel | null; -} - -export interface SubscriptionUpsertCloseAction { - type: 'close'; -} - -export type SubscriptionUpsertAction = - | SubscriptionUpsertOpenAction - | SubscriptionUpsertCloseAction; - const stateDefaults: SubscriptionUpsertState = { mode: null, subscription: null, diff --git a/src/subscriptions/providers/subscriptions.provider.tsx b/src/subscriptions/hooks/use-subscriptions.tsx similarity index 88% rename from src/subscriptions/providers/subscriptions.provider.tsx rename to src/subscriptions/hooks/use-subscriptions.tsx index e2b41d0..652aa93 100644 --- a/src/subscriptions/providers/subscriptions.provider.tsx +++ b/src/subscriptions/hooks/use-subscriptions.tsx @@ -5,13 +5,25 @@ import { createContext, memo, useCallback, + useContext, useMemo, useState, type PropsWithChildren, } from 'react'; -import type { SubscriptionModel } from '../models/subscription.model.tsx'; +import type { SubscriptionModel } from '../models/subscription.model.ts'; import { findSubscriptions } from '../models/subscription.table.ts'; +export function useSubscriptions(): UseSubscriptions { + return useContext(SubscriptionsContext); +} + +export interface UseSubscriptions { + subscriptions: Array; + tags: Array; + selectedTag: TagModel | null; + selectTag(tagId: string | null): void; +} + export const SubscriptionsProvider = memo(({ children }: PropsWithChildren) => { const unfilteredSubscriptions = useLiveQuery(() => findSubscriptions()); const tags = useLiveQuery(() => findTags()); @@ -53,12 +65,7 @@ export const SubscriptionsProvider = memo(({ children }: PropsWithChildren) => { ); }); -export const SubscriptionsContext = createContext<{ - subscriptions: Array; - tags: Array; - selectedTag: TagModel | null; - selectTag(tagId: string | null): void; -}>({ +const SubscriptionsContext = createContext({ subscriptions: [], tags: [], selectedTag: null, diff --git a/src/subscriptions/models/subscription-tag.model.ts b/src/subscriptions/models/subscription-tag.model.ts index a680a48..267808b 100644 --- a/src/subscriptions/models/subscription-tag.model.ts +++ b/src/subscriptions/models/subscription-tag.model.ts @@ -6,4 +6,7 @@ export const subscriptionTagSchema = z.object({ }); export type SubscriptionTagModel = z.infer; -export type UpsertSubscriptionTagModel = z.infer; +export const upsertSubscriptionTagSchema = subscriptionTagSchema.omit({}); +export type UpsertSubscriptionTagModel = z.infer< + typeof upsertSubscriptionTagSchema +>; diff --git a/src/subscriptions/models/subscription.mock.ts b/src/subscriptions/models/subscription.mock.ts index 7222e07..ed63fe6 100644 --- a/src/subscriptions/models/subscription.mock.ts +++ b/src/subscriptions/models/subscription.mock.ts @@ -1,5 +1,5 @@ import dayjs from 'dayjs'; -import type { SubscriptionModel } from './subscription.model.tsx'; +import type { SubscriptionModel } from './subscription.model.ts'; export const monthlySubscription = { id: 1, diff --git a/src/subscriptions/models/subscription.model.ts b/src/subscriptions/models/subscription.model.ts new file mode 100644 index 0000000..98d0f2d --- /dev/null +++ b/src/subscriptions/models/subscription.model.ts @@ -0,0 +1,32 @@ +import { tagSchema } from '@/tags/models/tag.model.ts'; +import { z } from 'zod'; +import { subscriptionCyclePeriods } from '../types/subscription-cycle-period.ts'; +import { subscriptionIcons } from '../types/subscription-icon.tsx'; + +export const subscriptionSchema = z.object({ + id: z.number(), + name: z.string(), + description: z.string().nullable().optional(), + icon: z.enum(subscriptionIcons), + price: z.number(), + startedAt: z.date(), + endedAt: z.date().nullable().optional(), + cycle: z.object({ + each: z.number(), + period: z.enum(subscriptionCyclePeriods), + }), + tags: z.array(tagSchema), +}); +export type SubscriptionModel = z.infer; + +export const insertSubscriptionSchema = subscriptionSchema.omit({ + id: true, +}); +export type InsertSubscriptionModel = z.infer; + +export const updateSubscriptionSchema = subscriptionSchema.omit({}); +export type UpdateSubscriptionModel = z.infer; + +export type UpsertSubscriptionModel = + | InsertSubscriptionModel + | UpdateSubscriptionModel; diff --git a/src/subscriptions/models/subscription.table.ts b/src/subscriptions/models/subscription.table.ts index ed21910..ab84834 100644 --- a/src/subscriptions/models/subscription.table.ts +++ b/src/subscriptions/models/subscription.table.ts @@ -7,7 +7,7 @@ import { type InsertSubscriptionModel, type SubscriptionModel, type UpdateSubscriptionModel, -} from './subscription.model.tsx'; +} from './subscription.model.ts'; export function findSubscriptions(): Promise> { return db.transaction( diff --git a/src/subscriptions/pages/subscriptions.page.tsx b/src/subscriptions/pages/subscriptions.page.tsx index 31fb031..ae3cc5a 100644 --- a/src/subscriptions/pages/subscriptions.page.tsx +++ b/src/subscriptions/pages/subscriptions.page.tsx @@ -3,14 +3,14 @@ import { DefaultLayout, DefaultLayoutHeader, } from '@/ui/layouts/default.layout.tsx'; -import { memo, useContext } from 'react'; +import { memo } from 'react'; import { AddSubscriptionButton } from '../components/add-subscription-button.tsx'; import { SubscriptionList } from '../components/subscription-list.tsx'; import { SubscriptionUpsert } from '../components/subscription-upsert.tsx'; -import { SubscriptionUpsertStateContext } from '../providers/subscription-upsert-state.provider.tsx'; +import { useSubscriptionUpsert } from '../hooks/use-subscription-upsert.tsx'; export const SubscriptionsPage = memo(() => { - const upsert = useContext(SubscriptionUpsertStateContext); + const upsert = useSubscriptionUpsert(); return ( ; diff --git a/src/subscriptions/models/subscription.model.tsx b/src/subscriptions/types/subscription-icon.tsx similarity index 58% rename from src/subscriptions/models/subscription.model.tsx rename to src/subscriptions/types/subscription-icon.tsx index 0bae573..4d55920 100644 --- a/src/subscriptions/models/subscription.model.tsx +++ b/src/subscriptions/types/subscription-icon.tsx @@ -1,4 +1,3 @@ -import { tagSchema } from '@/tags/models/tag.model.ts'; import { cn } from '@/ui/utils/cn.ts'; import { faHouse } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; @@ -10,7 +9,6 @@ import JetBrains from 'simple-icons/icons/jetbrains.svg?react'; import Netflix from 'simple-icons/icons/netflix.svg?react'; import Telegram from 'simple-icons/icons/telegram.svg?react'; import YouTube from 'simple-icons/icons/youtube.svg?react'; -import { z } from 'zod'; export const subscriptionIcons = [ 'telegram', @@ -51,39 +49,3 @@ export const subscriptionIconToLabel = { headspace: 'HeadSpace', godaddy: 'GoDaddy', } as const satisfies Record; - -export const subscriptionCyclePeriods = ['monthly', 'yearly'] as const; -export type SubscriptionCyclePeriod = (typeof subscriptionCyclePeriods)[number]; - -export const subscriptionCyclePeriodToLabel = { - monthly: 'Month', - yearly: 'Year', -} as const satisfies Record; - -export const subscriptionSchema = z.object({ - id: z.number(), - name: z.string(), - description: z.string().nullable().optional(), - icon: z.enum(subscriptionIcons), - price: z.number(), - startedAt: z.date(), - endedAt: z.date().nullable().optional(), - cycle: z.object({ - each: z.number(), - period: z.enum(subscriptionCyclePeriods), - }), - tags: z.array(tagSchema), -}); -export type SubscriptionModel = z.infer; - -export const insertSubscriptionSchema = subscriptionSchema.omit({ - id: true, -}); -export type InsertSubscriptionModel = z.infer; - -export const updateSubscriptionSchema = subscriptionSchema.omit({}); -export type UpdateSubscriptionModel = z.infer; - -export type UpsertSubscriptionModel = - | InsertSubscriptionModel - | UpdateSubscriptionModel; diff --git a/src/subscriptions/utils/cycle-period-to-calculate-monthly-price.spec.ts b/src/subscriptions/utils/cycle-period-to-calculate-monthly-price.spec.ts index 753c296..5498876 100644 --- a/src/subscriptions/utils/cycle-period-to-calculate-monthly-price.spec.ts +++ b/src/subscriptions/utils/cycle-period-to-calculate-monthly-price.spec.ts @@ -4,7 +4,7 @@ import { monthlySubscription, yearlySubscription, } from '../models/subscription.mock.ts'; -import type { SubscriptionModel } from '../models/subscription.model.tsx'; +import type { SubscriptionModel } from '../models/subscription.model.ts'; import { cyclePeriodToCalculateMonthlyPrice } from './cycle-period-to-calculate-monthly-price.ts'; describe('cyclePeriodToCalculateMonthlyPrice', () => { diff --git a/src/subscriptions/utils/cycle-period-to-calculate-monthly-price.ts b/src/subscriptions/utils/cycle-period-to-calculate-monthly-price.ts index 20728d5..5af68e3 100644 --- a/src/subscriptions/utils/cycle-period-to-calculate-monthly-price.ts +++ b/src/subscriptions/utils/cycle-period-to-calculate-monthly-price.ts @@ -1,8 +1,6 @@ import type { Month } from '@/date/types/month.ts'; -import type { - SubscriptionCyclePeriod, - SubscriptionModel, -} from '../models/subscription.model.tsx'; +import type { SubscriptionModel } from '../models/subscription.model.ts'; +import type { SubscriptionCyclePeriod } from '../types/subscription-cycle-period.ts'; export interface CalculateSubscriptionMonthlyPrice { (subscription: SubscriptionModel, month: Month, year?: number): number; diff --git a/src/tags/components/tag-select.tsx b/src/tags/components/tag-select.tsx index 68643c4..9b7a871 100644 --- a/src/tags/components/tag-select.tsx +++ b/src/tags/components/tag-select.tsx @@ -1,4 +1,4 @@ -import { SubscriptionsContext } from '@/subscriptions/providers/subscriptions.provider.tsx'; +import { useSubscriptions } from '@/subscriptions/hooks/use-subscriptions.tsx'; import { cn } from '@/ui/utils/cn.ts'; import { faCircle, faSliders } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; @@ -11,11 +11,11 @@ import { useCombobox, } from '@mantine/core'; import { useDisclosure, usePrevious } from '@mantine/hooks'; -import { memo, useContext, useEffect } from 'react'; +import { memo, useEffect } from 'react'; import { ManageTagsModal } from './manage-tags-modal.tsx'; export const TagSelect = memo(() => { - const { tags, selectTag, selectedTag } = useContext(SubscriptionsContext); + const { tags, selectTag, selectedTag } = useSubscriptions(); const prevSelectedTag = usePrevious(selectedTag); const combobox = useCombobox({ diff --git a/src/tags/models/tag.model.ts b/src/tags/models/tag.model.ts index 48a0d87..006102d 100644 --- a/src/tags/models/tag.model.ts +++ b/src/tags/models/tag.model.ts @@ -10,7 +10,7 @@ export type TagModel = z.infer; export const insertTagSchema = tagSchema.omit({ id: true }); export type InsertTagModel = z.infer; -export const updateTagSchema = tagSchema; +export const updateTagSchema = tagSchema.omit({}); export type UpdateTagModel = z.infer; export type UpsertTagModel = InsertTagModel | UpdateTagModel; diff --git a/src/ui/hooks/use-breakpoint.ts b/src/ui/hooks/use-breakpoint.ts index 8ed1fee..b4a7793 100644 --- a/src/ui/hooks/use-breakpoint.ts +++ b/src/ui/hooks/use-breakpoint.ts @@ -1,16 +1,9 @@ import { useMediaQuery } from '@mantine/hooks'; +import { + breakpointToMediaQuery, + type Breakpoint, +} from '../types/breakpoint.ts'; export function useBreakpoint(breakpoint: Breakpoint): boolean | undefined { return useMediaQuery(breakpointToMediaQuery[breakpoint]); } - -const breakpointToMediaQuery = { - sm: '(min-width: 640px)', - md: '(min-width: 768px)', - lg: '(min-width: 1024px)', - xl: '(min-width: 1280px)', - '2xl': '(min-width: 1536px)', -} as const satisfies Record; - -export const breakpoints = ['sm', 'md', 'lg', 'xl', '2xl'] as const; -export type Breakpoint = (typeof breakpoints)[number]; diff --git a/src/ui/hooks/use-default-layout.tsx b/src/ui/hooks/use-default-layout.tsx new file mode 100644 index 0000000..3e7ba68 --- /dev/null +++ b/src/ui/hooks/use-default-layout.tsx @@ -0,0 +1,56 @@ +import { useDisclosure, usePrevious } from '@mantine/hooks'; +import { + createContext, + memo, + useContext, + useEffect, + type PropsWithChildren, +} from 'react'; +import { useLocation } from 'react-router-dom'; +import type { Disclosure } from '../types/disclosure.ts'; + +export function useDefaultLayout(): UseDefaultLayout { + return useContext(DefaultLayoutContext); +} + +export interface UseDefaultLayout { + isDrawerOpened: boolean; + drawer: Disclosure; + isNavOpened: boolean; + nav: Disclosure; +} + +export const DefaultLayoutProvider = memo(({ children }: PropsWithChildren) => { + const [isDrawerOpened, drawer] = useDisclosure(false); + const [isNavOpened, nav] = useDisclosure(false); + const { pathname } = useLocation(); + const prevPathname = usePrevious(pathname); + + useEffect(() => { + if (pathname !== prevPathname) { + nav.close(); + } + }, [nav, pathname, prevPathname]); + + return ( + + {children} + + ); +}); + +const DefaultLayoutContext = createContext({ + isDrawerOpened: false, + drawer: { + open: () => {}, + close: () => {}, + toggle: () => {}, + }, + isNavOpened: false, + nav: { + open: () => {}, + close: () => {}, + toggle: () => {}, + }, +}); diff --git a/src/ui/layouts/default.layout.tsx b/src/ui/layouts/default.layout.tsx index 632f8a4..b729b48 100644 --- a/src/ui/layouts/default.layout.tsx +++ b/src/ui/layouts/default.layout.tsx @@ -1,18 +1,10 @@ -import { NavLinksContext } from '@/router/providers/nav-links.provider.tsx'; -import { useBreakpoint } from '@/ui/hooks/use-breakpoint.ts'; -import type { Disclosure } from '@/ui/types/disclosure.ts'; +import { useNavLinks } from '@/router/hooks/use-nav-links.tsx'; import { cn } from '@/ui/utils/cn.ts'; import { Affix, AppShell, Burger, Drawer, NavLink } from '@mantine/core'; -import { useDisclosure, usePrevious } from '@mantine/hooks'; -import { - createContext, - memo, - useContext, - useEffect, - type PropsWithChildren, - type ReactElement, -} from 'react'; +import { memo, type PropsWithChildren, type ReactElement } from 'react'; import { Link, useLocation } from 'react-router-dom'; +import { useBreakpoint } from '../hooks/use-breakpoint.ts'; +import { useDefaultLayout } from '../hooks/use-default-layout.tsx'; export const DefaultLayout = memo( ({ @@ -21,9 +13,8 @@ export const DefaultLayout = memo( drawerContent, drawerTitle, }: PropsWithChildren) => { - const { navLinks } = useContext(NavLinksContext); - const { isDrawerOpened, isNavOpened, drawer, nav } = - useContext(DefaultLayoutContext); + const { navLinks } = useNavLinks(); + const { isDrawerOpened, isNavOpened, drawer, nav } = useDefaultLayout(); const { pathname } = useLocation(); const isMd = useBreakpoint('md'); @@ -91,7 +82,7 @@ export interface DefaultLayoutProps { export const DefaultLayoutHeader = memo( ({ actions, children }: PropsWithChildren) => { const isMd = useBreakpoint('md'); - const { isDrawerOpened, isNavOpened } = useContext(DefaultLayoutContext); + const { isDrawerOpened, isNavOpened } = useDefaultLayout(); return ( <> @@ -115,45 +106,3 @@ export const DefaultLayoutHeader = memo( export interface DefaultLayoutHeaderProps { actions?: ReactElement; } - -export const DefaultLayoutContext = createContext<{ - isDrawerOpened: boolean; - drawer: Disclosure; - isNavOpened: boolean; - nav: Disclosure; -}>({ - isDrawerOpened: false, - drawer: { - open: () => {}, - close: () => {}, - toggle: () => {}, - }, - isNavOpened: false, - nav: { - open: () => {}, - close: () => {}, - toggle: () => {}, - }, -}); - -export const DefaultLayoutContextProvider = memo( - ({ children }: PropsWithChildren) => { - const [isDrawerOpened, drawer] = useDisclosure(false); - const [isNavOpened, nav] = useDisclosure(false); - const { pathname } = useLocation(); - const prevPathname = usePrevious(pathname); - - useEffect(() => { - if (pathname !== prevPathname) { - nav.close(); - } - }, [nav, pathname, prevPathname]); - - return ( - - {children} - - ); - }, -); diff --git a/src/ui/types/breakpoint.ts b/src/ui/types/breakpoint.ts new file mode 100644 index 0000000..c8571d8 --- /dev/null +++ b/src/ui/types/breakpoint.ts @@ -0,0 +1,10 @@ +export const breakpoints = ['sm', 'md', 'lg', 'xl', '2xl'] as const; +export type Breakpoint = (typeof breakpoints)[number]; + +export const breakpointToMediaQuery = { + sm: '(min-width: 640px)', + md: '(min-width: 768px)', + lg: '(min-width: 1024px)', + xl: '(min-width: 1280px)', + '2xl': '(min-width: 1536px)', +} as const satisfies Record;