Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: email verification #45

Merged
merged 18 commits into from
Jul 9, 2024
3 changes: 2 additions & 1 deletion .devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"github.vscode-github-actions",
"codecov.codecov",
"ritwickdey.liveserver",
"esbenp.prettier-vscode"
"esbenp.prettier-vscode",
"eamodio.gitlens"
]
}
},
Expand Down
5 changes: 3 additions & 2 deletions src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ import {
type UseSessionOptions,
} from "./auth"
import { useCountdown, useEventListener, useExternalScript } from "./general"
import { useNavigate, useSearchParamEntries } from "./router"
import { useNavigate, useParams, useSearchParams } from "./router"

export {
useCountdown,
useEventListener,
useExternalScript,
useNavigate,
useSearchParamEntries,
useParams,
useSearchParams,
useSession,
useSessionMetadata,
type SessionMetadata,
Expand Down
81 changes: 77 additions & 4 deletions src/hooks/router.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
import {
useNavigate as _useNavigate,
useSearchParams,
useParams as _useParams,
useSearchParams as _useSearchParams,
type NavigateOptions,
type To as NavigateTo,
type Params,
} from "react-router-dom"
import { object as objectSchema, type ObjectShape } from "yup"

import { type PageState } from "../components/page"
import { type ReadOnly } from "../utils/router"
import {
tryValidateSync,
type ObjectSchemaFromShape,
type TryValidateSyncOnErrorRT,
type TryValidateSyncOptions,
type TryValidateSyncRT,
} from "../utils/schema"

export function useNavigate(): <
State extends Record<string, any> = Record<string, any>,
Expand All @@ -17,7 +28,7 @@ export function useNavigate(): <
},
) => void {
const navigate = _useNavigate()
const searchParams = useSearchParamEntries()
const searchParams = useSearchParams()

return (to, options) => {
const { next = true, ..._options } = options || {}
Expand All @@ -26,6 +37,68 @@ export function useNavigate(): <
}
}

export function useSearchParamEntries() {
return Object.fromEntries(useSearchParams()[0].entries())
// -----------------------------------------------------------------------------
// Use Search Params
// -----------------------------------------------------------------------------

export function useSearchParams(): { [k: string]: string }

export function useSearchParams<
OnErrorRT extends TryValidateSyncOnErrorRT<ObjectSchemaFromShape<Shape>>,
Shape extends ObjectShape = {},
>(
shape: Shape,
validateOptions?: TryValidateSyncOptions<
ObjectSchemaFromShape<Shape>,
OnErrorRT
>,
): TryValidateSyncRT<ObjectSchemaFromShape<Shape>, OnErrorRT>

export function useSearchParams<
OnErrorRT extends TryValidateSyncOnErrorRT<ObjectSchemaFromShape<Shape>>,
Shape extends ObjectShape = {},
>(
shape?: Shape,
validateOptions?: TryValidateSyncOptions<
ObjectSchemaFromShape<Shape>,
OnErrorRT
>,
) {
const searchParams = Object.fromEntries(_useSearchParams()[0].entries())
if (!shape) return searchParams

return tryValidateSync(searchParams, objectSchema(shape), validateOptions)
}

// -----------------------------------------------------------------------------
// Use Params
// -----------------------------------------------------------------------------

export function useParams(): ReadOnly<Params<string>>

export function useParams<
OnErrorRT extends TryValidateSyncOnErrorRT<ObjectSchemaFromShape<Shape>>,
Shape extends ObjectShape = {},
>(
shape: Shape,
validateOptions?: TryValidateSyncOptions<
ObjectSchemaFromShape<Shape>,
OnErrorRT
>,
): TryValidateSyncRT<ObjectSchemaFromShape<Shape>, OnErrorRT>

export function useParams<
OnErrorRT extends TryValidateSyncOnErrorRT<ObjectSchemaFromShape<Shape>>,
Shape extends ObjectShape = {},
>(
shape?: Shape,
validateOptions?: TryValidateSyncOptions<
ObjectSchemaFromShape<Shape>,
OnErrorRT
>,
) {
const params = _useParams()
if (!shape) return params

return tryValidateSync(params, objectSchema(shape), validateOptions)
}
120 changes: 120 additions & 0 deletions src/utils/router.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { path as _ } from "./router"

test("no nested paths", () => {
const paths = _("")

expect(paths._).toBe("/")
expect(paths.__).toBe("")
})

test("nested paths", () => {
const paths = _("", {
a: _("/a", {
b: _("/b"),
}),
})

expect(paths._).toBe("/")
expect(paths.__).toBe("")
expect(paths.a._).toBe("/a")
expect(paths.a.__).toBe("/a")
expect(paths.a.b._).toBe("/a/b")
expect(paths.a.b.__).toBe("/b")
})

test("one param", () => {
const paths = _("", {
person: _("/person", {
name: _("/:name", {
sam: _({ name: "samantha" }),
}),
}),
})

expect(paths._).toBe("/")
expect(paths.__).toBe("")
expect(paths.person._).toBe("/person")
expect(paths.person.__).toBe("/person")
expect(paths.person.name._).toBe("/person/:name")
expect(paths.person.name.__).toBe("/:name")
expect(paths.person.name.sam._).toBe("/person/samantha")
expect(paths.person.name.sam.__).toMatchObject({ name: "samantha" })
})

test("multiple params", () => {
const paths = _("", {
hero: _("/hero", {
firstAndLastName: _("/:firstName/:lastName", {
spiderMan: _(
{ firstName: "peter", lastName: "parker" },
{
mainVillain: _("/green-goblin"),
},
),
}),
}),
})

expect(paths._).toBe("/")
expect(paths.__).toBe("")
expect(paths.hero._).toBe("/hero")
expect(paths.hero.__).toBe("/hero")
expect(paths.hero.firstAndLastName._).toBe("/hero/:firstName/:lastName")
expect(paths.hero.firstAndLastName.__).toBe("/:firstName/:lastName")
expect(paths.hero.firstAndLastName.spiderMan._).toBe("/hero/peter/parker")
expect(paths.hero.firstAndLastName.spiderMan.__).toMatchObject({
firstName: "peter",
lastName: "parker",
})
expect(paths.hero.firstAndLastName.spiderMan.mainVillain._).toBe(
"/hero/peter/parker/green-goblin",
)
expect(paths.hero.firstAndLastName.spiderMan.mainVillain.__).toBe(
"/green-goblin",
)
})

test("nested params", () => {
const paths = _("", {
hero: _("/hero", {
firstName: _("/:firstName", {
batMan: _(
{ firstName: "bruce" },
{
lastName: _("/:lastName", {
batMan: _(
{ lastName: "wayne" },
{
mainVillain: _("/joker"),
},
),
}),
},
),
}),
}),
})

expect(paths._).toBe("/")
expect(paths.__).toBe("")
expect(paths.hero._).toBe("/hero")
expect(paths.hero.__).toBe("/hero")
expect(paths.hero.firstName._).toBe("/hero/:firstName")
expect(paths.hero.firstName.__).toBe("/:firstName")
expect(paths.hero.firstName.batMan._).toBe("/hero/bruce")
expect(paths.hero.firstName.batMan.__).toMatchObject({ firstName: "bruce" })
expect(paths.hero.firstName.batMan.lastName._).toBe("/hero/bruce/:lastName")
expect(paths.hero.firstName.batMan.lastName.__).toBe("/:lastName")
expect(paths.hero.firstName.batMan.lastName.batMan._).toBe(
"/hero/bruce/wayne",
)
expect(paths.hero.firstName.batMan.lastName.batMan.__).toMatchObject({
lastName: "wayne",
})
expect(paths.hero.firstName.batMan.lastName.batMan.mainVillain._).toBe(
"/hero/bruce/wayne/joker",
)
expect(paths.hero.firstName.batMan.lastName.batMan.mainVillain.__).toBe(
"/joker",
)
})
32 changes: 24 additions & 8 deletions src/utils/router.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,40 @@
import { generatePath } from "react-router-dom"

export type ReadOnly<T> = {
readonly [P in keyof T]: T[P]
}

export type Parameters = Record<string, string>

export interface Path {
_: string
__: string
[subpath: string]: string | Path
__: string | Parameters
[subpath: string]: string | Path | Parameters
}

export function path<Subpaths extends Record<string, Path>>(
_: string,
_: string | Parameters,
subpaths: Subpaths = {} as Subpaths,
): Path & Subpaths {
function updatePath(path: Path, root: boolean): void {
if (typeof path.__ === "object" && typeof _ === "string") {
_ = generatePath(_, path.__)
}

Object.entries(path).forEach(([key, subpath]) => {
if (typeof subpath === "string") {
if (!root || key !== "_") path[key] = _ + subpath
} else {
updatePath(subpath, false)
if (key !== "__") {
if (typeof subpath === "string") {
if (typeof _ === "string" && (!root || key !== "_")) {
path[key] = _ + subpath
}
} else {
updatePath(subpath as Path, false)
}
}
})
}

const path = { ...subpaths, _, __: _ }
const path = { ...subpaths, _: typeof _ === "string" ? _ : "", __: _ }
if (_ === "") {
path._ = "/"
} else {
Expand Down
Loading