@hobenakicoffee/libraries 3.4.1 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +119 -246
- package/package.json +22 -22
- package/src/App.tsx +192 -19
- package/src/index.css +0 -1
- package/src/utils/get-product-link.ts +1 -1
- package/src/components/turnstile-captcha.tsx +0 -47
- package/src/components/ui/button.tsx +0 -77
- package/src/components/ui/calendar.tsx +0 -235
- package/src/components/ui/spinner.tsx +0 -18
- package/src/constants/common.test.ts +0 -33
- package/src/constants/legal.test.ts +0 -72
- package/src/constants/payment.test.ts +0 -259
- package/src/constants/platforms.test.ts +0 -66
- package/src/constants/services.test.ts +0 -58
- package/src/lib/utils.ts +0 -6
- package/src/moderation/profanity-service.test.ts +0 -106
- package/src/providers/theme-provider.tsx +0 -73
- package/src/utils/check-moderation.test.ts +0 -321
- package/src/utils/format-amount.test.ts +0 -30
- package/src/utils/format-count.test.ts +0 -56
- package/src/utils/format-date.test.ts +0 -19
- package/src/utils/format-number.test.ts +0 -29
- package/src/utils/format-plain-text.test.ts +0 -36
- package/src/utils/get-newsletter-post-link.test.ts +0 -27
- package/src/utils/get-product-link.test.ts +0 -36
- package/src/utils/get-social-handle.test.ts +0 -32
- package/src/utils/get-social-link.test.ts +0 -63
- package/src/utils/get-user-name-initials.test.ts +0 -34
- package/src/utils/get-user-page-link.test.ts +0 -9
- package/src/utils/open-to-new-window.test.ts +0 -34
- package/src/utils/post-to-facebook.test.ts +0 -43
- package/src/utils/post-to-instagram.test.ts +0 -56
- package/src/utils/post-to-linkedin.test.ts +0 -43
- package/src/utils/post-to-x.test.ts +0 -45
- package/src/utils/qr-svg-utils.test.ts +0 -104
- package/src/utils/to-human-readable.test.ts +0 -25
- package/src/utils/validate-phone-number.test.ts +0 -28
package/src/App.tsx
CHANGED
|
@@ -1,28 +1,201 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import { productInfo } from "@/constants/legal";
|
|
3
|
+
|
|
4
|
+
const EXPORTS = [
|
|
5
|
+
{
|
|
6
|
+
path: "/constants",
|
|
7
|
+
name: "constants",
|
|
8
|
+
desc: "Payment, Platform, Visibility consts",
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
path: "/utils",
|
|
12
|
+
name: "utils",
|
|
13
|
+
desc: "Formatters, validators, social helpers",
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
path: "/types",
|
|
17
|
+
name: "types",
|
|
18
|
+
desc: "Supabase + custom TypeScript types",
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
path: "/moderation",
|
|
22
|
+
name: "moderation",
|
|
23
|
+
desc: "Profanity detection EN/BN",
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
path: "/nuqs",
|
|
27
|
+
name: "nuqs",
|
|
28
|
+
desc: "URL state parsers (zod)",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
path: "/scripts",
|
|
32
|
+
name: "scripts",
|
|
33
|
+
desc: "Build utilities",
|
|
34
|
+
},
|
|
35
|
+
{ path: "/hooks", name: "hooks", desc: "React hooks" },
|
|
36
|
+
{ path: "/docs", name: "docs", desc: "Documentation & guides" },
|
|
37
|
+
];
|
|
5
38
|
|
|
6
39
|
const App = () => {
|
|
7
|
-
const [
|
|
40
|
+
const [copied, setCopied] = useState(false);
|
|
41
|
+
|
|
42
|
+
const handleCopy = () => {
|
|
43
|
+
navigator.clipboard.writeText("bun add @hobenakicoffee/libraries");
|
|
44
|
+
setCopied(true);
|
|
45
|
+
setTimeout(() => setCopied(false), 2000);
|
|
46
|
+
};
|
|
8
47
|
|
|
9
48
|
return (
|
|
10
|
-
<div className="
|
|
11
|
-
<
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
<Button>Click me</Button>
|
|
16
|
-
|
|
17
|
-
<Calendar
|
|
18
|
-
captionLayout="dropdown"
|
|
19
|
-
className="rounded-lg border"
|
|
20
|
-
mode="single"
|
|
21
|
-
onSelect={setDate}
|
|
22
|
-
selected={date}
|
|
23
|
-
/>
|
|
24
|
-
{getProductLink("leo", "example-product-slug")}
|
|
49
|
+
<div className="min-dvh relative flex flex-col overflow-hidden bg-[#0a0c10] text-slate-200">
|
|
50
|
+
<div className="pointer-events-none absolute inset-0 overflow-hidden">
|
|
51
|
+
<div className="absolute inset-0 bg-[linear-gradient(to_right,#1e293b_0.5px,transparent_0.5px),linear-gradient(to_bottom,#1e293b_0.5px,transparent_0.5px)] bg-[size:4rem_4rem] [mask-image:radial-gradient(ellipse_60%_50%_at_50%_50%,#000_70%,transparent_100%)]" />
|
|
52
|
+
<div className="absolute top-0 left-1/4 h-[500px] w-[500px] -translate-x-1/2 rounded-full bg-indigo-500/[0.03] blur-3xl" />
|
|
53
|
+
<div className="absolute right-1/4 bottom-0 h-[400px] w-[400px] translate-y-1/2 rounded-full bg-cyan-500/[0.03] blur-3xl" />
|
|
25
54
|
</div>
|
|
55
|
+
|
|
56
|
+
<header className="relative z-10 flex items-center justify-between px-6 py-4 md:px-8">
|
|
57
|
+
<div className="flex items-center gap-3">
|
|
58
|
+
<div className="flex h-10 w-10 items-center justify-center rounded-lg border border-slate-700 bg-slate-900 shadow-[0_0_20px_rgba(99,102,241,0.2)]">
|
|
59
|
+
<span className="font-bold font-mono text-cyan-400 text-lg">H</span>
|
|
60
|
+
</div>
|
|
61
|
+
<div>
|
|
62
|
+
<h1 className="font-mono font-semibold text-slate-100 text-sm tracking-tight">
|
|
63
|
+
@hobenakicoffee/libraries
|
|
64
|
+
</h1>
|
|
65
|
+
<p className="font-mono text-slate-500 text-xs">v3.4.2</p>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
<div className="flex items-center gap-2 rounded-md border border-slate-800 bg-slate-900/50 px-2 py-1 font-mono text-slate-400 text-xs">
|
|
69
|
+
<span className="h-2 w-2 rounded-full bg-green-500" />
|
|
70
|
+
TypeScript
|
|
71
|
+
</div>
|
|
72
|
+
</header>
|
|
73
|
+
|
|
74
|
+
<main className="relative z-10 flex flex-1 flex-col items-center justify-center px-4 py-12 md:px-6">
|
|
75
|
+
<div className="mx-auto max-w-2xl text-center">
|
|
76
|
+
<div className="mb-6 inline-flex items-center gap-2 rounded-md border border-slate-800 bg-slate-900/30 px-3 py-1 font-mono text-slate-400 text-xs">
|
|
77
|
+
<span>framework-agnostic</span>
|
|
78
|
+
<span className="text-slate-600">{"//"}</span>
|
|
79
|
+
<span>tree-shakeable</span>
|
|
80
|
+
<span className="text-slate-600">{"//"}</span>
|
|
81
|
+
<span>zod-powered</span>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<h2 className="mb-4 font-bold font-mono text-3xl tracking-tight md:text-5xl">
|
|
85
|
+
<span className="text-slate-100">Shared </span>
|
|
86
|
+
<span className="bg-linear-to-r from-cyan-400 to-indigo-400 bg-clip-text text-transparent">
|
|
87
|
+
constants,
|
|
88
|
+
</span>
|
|
89
|
+
<br />
|
|
90
|
+
<span className="text-slate-100">utilities & </span>
|
|
91
|
+
<span className="bg-linear-to-r from-cyan-400 to-indigo-400 bg-clip-text text-transparent">
|
|
92
|
+
types
|
|
93
|
+
</span>
|
|
94
|
+
</h2>
|
|
95
|
+
|
|
96
|
+
<p className="mx-auto mb-8 max-w-lg font-mono text-slate-400 text-sm leading-relaxed">
|
|
97
|
+
The core package for{" "}
|
|
98
|
+
<span className="text-cyan-400">"{productInfo.name}"</span>{" "}
|
|
99
|
+
projects. Build faster with pre-built constants, utilities, types,
|
|
100
|
+
and moderation tools.
|
|
101
|
+
</p>
|
|
102
|
+
|
|
103
|
+
<div className="group relative mx-auto inline-flex cursor-pointer flex-col items-center gap-3 md:flex-row">
|
|
104
|
+
<div className="flex items-center gap-2 rounded-lg border border-slate-700 bg-slate-900 p-1 pr-3 font-mono text-sm shadow-[0_0_40px_rgba(99,102,241,0.15)] transition-all group-hover:border-cyan-500/50 group-hover:shadow-[0_0_60px_rgba(99,102,241,0.25)]">
|
|
105
|
+
<button
|
|
106
|
+
className="flex items-center gap-2 rounded-md bg-slate-800 px-3 py-2 text-slate-200 transition-colors hover:bg-slate-700"
|
|
107
|
+
onClick={handleCopy}
|
|
108
|
+
type="button"
|
|
109
|
+
>
|
|
110
|
+
<span className="text-cyan-400">$</span>
|
|
111
|
+
<span>
|
|
112
|
+
{copied ? " Copied!" : " bun add @hobenakicoffee/libraries"}
|
|
113
|
+
</span>
|
|
114
|
+
</button>
|
|
115
|
+
<span className="text-slate-500">{copied ? "✓" : "›"}</span>
|
|
116
|
+
</div>
|
|
117
|
+
<a
|
|
118
|
+
className="inline-flex items-center gap-2 rounded-lg bg-indigo-600 px-4 py-3 font-mono text-sm text-white transition-all hover:bg-indigo-600/90"
|
|
119
|
+
href="/libraries/docs/"
|
|
120
|
+
>
|
|
121
|
+
<span>Documentation</span>
|
|
122
|
+
<span className="text-indigo-500">→</span>
|
|
123
|
+
</a>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<div className="mt-16 w-full max-w-4xl">
|
|
128
|
+
<div className="mb-3 font-mono text-slate-500 text-xs">
|
|
129
|
+
{"// exports"}
|
|
130
|
+
</div>
|
|
131
|
+
<div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
|
|
132
|
+
{EXPORTS.map((exp, i) => (
|
|
133
|
+
<a
|
|
134
|
+
className="group relative overflow-hidden rounded-lg border border-slate-800 bg-slate-900/50 p-3 font-mono transition-all hover:border-slate-600"
|
|
135
|
+
href={
|
|
136
|
+
exp.path === "/docs"
|
|
137
|
+
? `/libraries${exp.path}`
|
|
138
|
+
: `/libraries${exp.path}/overview`
|
|
139
|
+
}
|
|
140
|
+
key={exp.path}
|
|
141
|
+
style={{ animationDelay: `${i * 50}ms` }}
|
|
142
|
+
>
|
|
143
|
+
<div className="absolute inset-0 bg-linear-to-r from-cyan-500/0 to-indigo-500/0 opacity-0 transition-opacity group-hover:from-cyan-500/[0.03] group-hover:to-indigo-500/[0.03] group-hover:opacity-100" />
|
|
144
|
+
<div className="relative">
|
|
145
|
+
<span className="text-cyan-400 text-xs">
|
|
146
|
+
@hobenakicoffee/libraries
|
|
147
|
+
</span>
|
|
148
|
+
<span className="text-slate-500 text-xs">{exp.path}</span>
|
|
149
|
+
</div>
|
|
150
|
+
<div className="relative mt-1 text-slate-500 text-xs">
|
|
151
|
+
{exp.desc}
|
|
152
|
+
</div>
|
|
153
|
+
</a>
|
|
154
|
+
))}
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<div className="mt-12 w-full max-w-2xl overflow-x-auto rounded-lg border border-slate-800 bg-slate-950 p-4 font-mono text-xs">
|
|
159
|
+
<div className="tems-center mb-2 gap-2 text-slate-500">
|
|
160
|
+
<span className="h-3 w-3 rounded-full bg-emerald-500/20" />
|
|
161
|
+
main.ts
|
|
162
|
+
</div>
|
|
163
|
+
<pre className="text-slate-300">
|
|
164
|
+
<code>
|
|
165
|
+
<span className="text-purple-400">import</span> {"{"}{" "}
|
|
166
|
+
<span className="text-cyan-400">PaymentStatuses</span>, {""}
|
|
167
|
+
<span className="text-cyan-400">SupporterPlatforms</span>
|
|
168
|
+
{"}"} <span className="text-purple-400">from</span>{" "}
|
|
169
|
+
<span className="text-amber-300">
|
|
170
|
+
"@hobenakicoffee/libraries"
|
|
171
|
+
</span>
|
|
172
|
+
{"\n"}
|
|
173
|
+
<span className="text-purple-400">import</span> {"{"}{" "}
|
|
174
|
+
<span className="text-cyan-400">formatAmount</span>, {""}
|
|
175
|
+
<span className="text-cyan-400">getUserPageLink</span>
|
|
176
|
+
{"}"} <span className="text-purple-400">from</span>{" "}
|
|
177
|
+
<span className="text-amber-300">
|
|
178
|
+
"@hobenakicoffee/libraries/utils"
|
|
179
|
+
</span>
|
|
180
|
+
{"\n"}
|
|
181
|
+
<span className="text-purple-400">import</span> type {"{"}{" "}
|
|
182
|
+
<span className="text-cyan-400">Database</span>
|
|
183
|
+
{"}"} <span className="text-purple-400">from</span>{" "}
|
|
184
|
+
<span className="text-amber-300">
|
|
185
|
+
"@hobenakicoffee/libraries/types"
|
|
186
|
+
</span>
|
|
187
|
+
</code>
|
|
188
|
+
</pre>
|
|
189
|
+
</div>
|
|
190
|
+
</main>
|
|
191
|
+
|
|
192
|
+
<footer className="relative z-10 px-6 py-4 text-center">
|
|
193
|
+
<p className="font-mono text-slate-600 text-xs">
|
|
194
|
+
<span>© {new Date().getFullYear()}</span>
|
|
195
|
+
<span className="text-slate-700">{" // "}</span>
|
|
196
|
+
<span className="text-slate-500">{productInfo.name}</span>
|
|
197
|
+
</p>
|
|
198
|
+
</footer>
|
|
26
199
|
</div>
|
|
27
200
|
);
|
|
28
201
|
};
|
package/src/index.css
CHANGED
|
@@ -6,5 +6,5 @@ export function getProductLink(
|
|
|
6
6
|
baseUrl = "https://hobenakicoffee.com"
|
|
7
7
|
) {
|
|
8
8
|
const sanitizedSlug = slug.trim().replace(/\s+/g, "-");
|
|
9
|
-
return `${getUserPageLink(username, baseUrl)}/
|
|
9
|
+
return `${getUserPageLink(username, baseUrl)}/shop/products/${sanitizedSlug}`;
|
|
10
10
|
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { Turnstile, type TurnstileInstance } from "@marsidev/react-turnstile";
|
|
2
|
-
import { useRef } from "react";
|
|
3
|
-
import { useTheme } from "@/providers/theme-provider";
|
|
4
|
-
|
|
5
|
-
interface TurnstileCaptchaProps {
|
|
6
|
-
onError?: (error: string) => void;
|
|
7
|
-
onTokenChange: (token: string) => void;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function TurnstileCaptcha({
|
|
11
|
-
onTokenChange,
|
|
12
|
-
onError,
|
|
13
|
-
}: TurnstileCaptchaProps) {
|
|
14
|
-
const { theme } = useTheme();
|
|
15
|
-
const turnstileRef = useRef<TurnstileInstance | null>(null);
|
|
16
|
-
|
|
17
|
-
function handleSuccess(token: string) {
|
|
18
|
-
onTokenChange(token);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function handleExpire() {
|
|
22
|
-
onTokenChange("");
|
|
23
|
-
onError?.("CAPTCHA has expired. Please verify again.");
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function handleError() {
|
|
27
|
-
onTokenChange("");
|
|
28
|
-
onError?.("CAPTCHA verification failed. Please try again.");
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return (
|
|
32
|
-
<div className="rounded-xl border border-input bg-input">
|
|
33
|
-
<Turnstile
|
|
34
|
-
className="overflow-hidden rounded-[calc(var(--radius)+5px)]"
|
|
35
|
-
onError={handleError}
|
|
36
|
-
onExpire={handleExpire}
|
|
37
|
-
onSuccess={handleSuccess}
|
|
38
|
-
options={{
|
|
39
|
-
theme: theme === "system" ? "auto" : theme,
|
|
40
|
-
size: "flexible",
|
|
41
|
-
}}
|
|
42
|
-
ref={turnstileRef}
|
|
43
|
-
siteKey={import.meta.env.VITE_TURNSTILE_SITE_KEY || ""}
|
|
44
|
-
/>
|
|
45
|
-
</div>
|
|
46
|
-
);
|
|
47
|
-
}
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import { cva, type VariantProps } from "class-variance-authority";
|
|
2
|
-
import { Slot } from "radix-ui";
|
|
3
|
-
import type * as React from "react";
|
|
4
|
-
|
|
5
|
-
import { cn } from "@/lib/utils";
|
|
6
|
-
import { Spinner } from "./spinner";
|
|
7
|
-
|
|
8
|
-
const buttonVariants = cva(
|
|
9
|
-
"group/button inline-flex shrink-0 select-none items-center justify-center whitespace-nowrap rounded-xl border border-transparent bg-clip-padding font-medium text-sm outline-none transition-all focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background active:scale-[0.98] disabled:pointer-events-none disabled:opacity-70 aria-invalid:border-destructive aria-invalid:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
|
10
|
-
{
|
|
11
|
-
variants: {
|
|
12
|
-
variant: {
|
|
13
|
-
default: "bg-primary text-primary-foreground hover:bg-primary/80",
|
|
14
|
-
outline:
|
|
15
|
-
"border-border bg-input/30 hover:border-primary/50 hover:bg-input/20 hover:bg-primary/10 hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground",
|
|
16
|
-
secondary:
|
|
17
|
-
"bg-secondary text-secondary-foreground hover:bg-primary/5 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
|
18
|
-
ghost:
|
|
19
|
-
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
|
|
20
|
-
destructive:
|
|
21
|
-
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 dark:hover:bg-destructive/30",
|
|
22
|
-
link: "underline-offset-4 hover:underline",
|
|
23
|
-
inverted:
|
|
24
|
-
"border-border bg-background text-foreground hover:border-primary/50 hover:bg-primary/5",
|
|
25
|
-
dottedUnderline:
|
|
26
|
-
"rounded-none border-0 border-muted-foreground/50 border-b border-dotted bg-transparent text-muted-foreground hover:bg-transparent hover:text-foreground",
|
|
27
|
-
},
|
|
28
|
-
size: {
|
|
29
|
-
default:
|
|
30
|
-
"h-12 gap-1.5 px-4 has-data-[icon=inline-end]:pr-2.5 has-data-[icon=inline-start]:pl-2.5",
|
|
31
|
-
xs: "h-6 gap-1 px-2.5 text-xs has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2 [&_svg:not([class*='size-'])]:size-3",
|
|
32
|
-
sm: "h-8 gap-1 px-3 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
|
33
|
-
lg: "h-16 gap-1.5 px-5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
|
|
34
|
-
icon: "size-9",
|
|
35
|
-
"icon-xs": "size-6 [&_svg:not([class*='size-'])]:size-3",
|
|
36
|
-
"icon-sm": "size-8",
|
|
37
|
-
"icon-lg": "size-12",
|
|
38
|
-
},
|
|
39
|
-
},
|
|
40
|
-
defaultVariants: {
|
|
41
|
-
variant: "default",
|
|
42
|
-
size: "default",
|
|
43
|
-
},
|
|
44
|
-
}
|
|
45
|
-
);
|
|
46
|
-
|
|
47
|
-
function Button({
|
|
48
|
-
className,
|
|
49
|
-
variant = "default",
|
|
50
|
-
size = "default",
|
|
51
|
-
asChild = false,
|
|
52
|
-
loading,
|
|
53
|
-
disabled,
|
|
54
|
-
children,
|
|
55
|
-
...props
|
|
56
|
-
}: React.ComponentProps<"button"> &
|
|
57
|
-
VariantProps<typeof buttonVariants> & {
|
|
58
|
-
asChild?: boolean;
|
|
59
|
-
loading?: boolean;
|
|
60
|
-
}) {
|
|
61
|
-
const Comp = asChild ? Slot.Root : "button";
|
|
62
|
-
|
|
63
|
-
return (
|
|
64
|
-
<Comp
|
|
65
|
-
className={cn(buttonVariants({ variant, size, className }))}
|
|
66
|
-
data-size={size}
|
|
67
|
-
data-slot="button"
|
|
68
|
-
data-variant={variant}
|
|
69
|
-
disabled={loading || disabled}
|
|
70
|
-
{...props}
|
|
71
|
-
>
|
|
72
|
-
{loading ? <Spinner /> : children}
|
|
73
|
-
</Comp>
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export { Button, buttonVariants };
|
|
@@ -1,235 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ArrowDownIcon,
|
|
3
|
-
ArrowLeftIcon,
|
|
4
|
-
ArrowRightIcon,
|
|
5
|
-
} from "@hugeicons/core-free-icons";
|
|
6
|
-
import { HugeiconsIcon } from "@hugeicons/react";
|
|
7
|
-
import { type ComponentProps, useEffect, useRef } from "react";
|
|
8
|
-
import {
|
|
9
|
-
type DayButton,
|
|
10
|
-
DayPicker,
|
|
11
|
-
getDefaultClassNames,
|
|
12
|
-
} from "react-day-picker";
|
|
13
|
-
import { Button, buttonVariants } from "@/components/ui/button";
|
|
14
|
-
import { cn } from "@/lib/utils";
|
|
15
|
-
|
|
16
|
-
function Calendar({
|
|
17
|
-
className,
|
|
18
|
-
classNames,
|
|
19
|
-
showOutsideDays = true,
|
|
20
|
-
captionLayout = "label",
|
|
21
|
-
buttonVariant = "ghost",
|
|
22
|
-
formatters,
|
|
23
|
-
components,
|
|
24
|
-
...props
|
|
25
|
-
}: ComponentProps<typeof DayPicker> & {
|
|
26
|
-
buttonVariant?: ComponentProps<typeof Button>["variant"];
|
|
27
|
-
}) {
|
|
28
|
-
const defaultClassNames = getDefaultClassNames();
|
|
29
|
-
|
|
30
|
-
return (
|
|
31
|
-
<DayPicker
|
|
32
|
-
captionLayout={captionLayout}
|
|
33
|
-
className={cn(
|
|
34
|
-
"group/calendar bg-background in-data-[slot=card-content]:bg-transparent in-data-[slot=popover-content]:bg-transparent p-3 [--cell-radius:var(--radius-md)] [--cell-size:--spacing(8)]",
|
|
35
|
-
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
|
|
36
|
-
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
|
|
37
|
-
className
|
|
38
|
-
)}
|
|
39
|
-
classNames={{
|
|
40
|
-
root: cn("w-fit", defaultClassNames.root),
|
|
41
|
-
months: cn(
|
|
42
|
-
"relative flex flex-col gap-4 md:flex-row",
|
|
43
|
-
defaultClassNames.months
|
|
44
|
-
),
|
|
45
|
-
month: cn("flex w-full flex-col gap-4", defaultClassNames.month),
|
|
46
|
-
nav: cn(
|
|
47
|
-
"absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1",
|
|
48
|
-
defaultClassNames.nav
|
|
49
|
-
),
|
|
50
|
-
button_previous: cn(
|
|
51
|
-
buttonVariants({ variant: buttonVariant }),
|
|
52
|
-
"size-(--cell-size) select-none p-0 aria-disabled:opacity-50",
|
|
53
|
-
defaultClassNames.button_previous
|
|
54
|
-
),
|
|
55
|
-
button_next: cn(
|
|
56
|
-
buttonVariants({ variant: buttonVariant }),
|
|
57
|
-
"size-(--cell-size) select-none p-0 aria-disabled:opacity-50",
|
|
58
|
-
defaultClassNames.button_next
|
|
59
|
-
),
|
|
60
|
-
month_caption: cn(
|
|
61
|
-
"flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)",
|
|
62
|
-
defaultClassNames.month_caption
|
|
63
|
-
),
|
|
64
|
-
dropdowns: cn(
|
|
65
|
-
"flex h-(--cell-size) w-full items-center justify-center gap-1.5 font-medium text-sm",
|
|
66
|
-
defaultClassNames.dropdowns
|
|
67
|
-
),
|
|
68
|
-
dropdown_root: cn(
|
|
69
|
-
"cn-calendar-dropdown-root relative rounded-(--cell-radius)",
|
|
70
|
-
defaultClassNames.dropdown_root
|
|
71
|
-
),
|
|
72
|
-
dropdown: cn(
|
|
73
|
-
"absolute inset-0 bg-popover opacity-0",
|
|
74
|
-
defaultClassNames.dropdown
|
|
75
|
-
),
|
|
76
|
-
caption_label: cn(
|
|
77
|
-
"select-none font-medium",
|
|
78
|
-
captionLayout === "label"
|
|
79
|
-
? "text-sm"
|
|
80
|
-
: "cn-calendar-caption-label flex items-center gap-1 rounded-(--cell-radius) text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground",
|
|
81
|
-
defaultClassNames.caption_label
|
|
82
|
-
),
|
|
83
|
-
table: "w-full border-collapse",
|
|
84
|
-
weekdays: cn("flex", defaultClassNames.weekdays),
|
|
85
|
-
weekday: cn(
|
|
86
|
-
"flex-1 select-none rounded-(--cell-radius) font-normal text-[0.8rem] text-muted-foreground",
|
|
87
|
-
defaultClassNames.weekday
|
|
88
|
-
),
|
|
89
|
-
week: cn("mt-2 flex w-full", defaultClassNames.week),
|
|
90
|
-
week_number_header: cn(
|
|
91
|
-
"w-(--cell-size) select-none",
|
|
92
|
-
defaultClassNames.week_number_header
|
|
93
|
-
),
|
|
94
|
-
week_number: cn(
|
|
95
|
-
"select-none text-[0.8rem] text-muted-foreground",
|
|
96
|
-
defaultClassNames.week_number
|
|
97
|
-
),
|
|
98
|
-
day: cn(
|
|
99
|
-
"group/day relative aspect-square h-full w-full select-none rounded-(--cell-radius) p-0 text-center [&:last-child[data-selected=true]_button]:rounded-r-(--cell-radius)",
|
|
100
|
-
props.showWeekNumber
|
|
101
|
-
? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-(--cell-radius)"
|
|
102
|
-
: "[&:first-child[data-selected=true]_button]:rounded-l-(--cell-radius)",
|
|
103
|
-
defaultClassNames.day
|
|
104
|
-
),
|
|
105
|
-
range_start: cn(
|
|
106
|
-
"relative isolate -z-0 rounded-l-(--cell-radius) bg-muted after:absolute after:inset-y-0 after:right-0 after:w-4 after:bg-muted",
|
|
107
|
-
defaultClassNames.range_start
|
|
108
|
-
),
|
|
109
|
-
range_middle: cn("rounded-none", defaultClassNames.range_middle),
|
|
110
|
-
range_end: cn(
|
|
111
|
-
"relative isolate -z-0 rounded-r-(--cell-radius) bg-muted after:absolute after:inset-y-0 after:left-0 after:w-4 after:bg-muted-200",
|
|
112
|
-
defaultClassNames.range_end
|
|
113
|
-
),
|
|
114
|
-
today: cn(
|
|
115
|
-
"rounded-(--cell-radius) bg-muted text-foreground data-[selected=true]:rounded-none",
|
|
116
|
-
defaultClassNames.today
|
|
117
|
-
),
|
|
118
|
-
outside: cn(
|
|
119
|
-
"text-muted-foreground aria-selected:text-muted-foreground",
|
|
120
|
-
defaultClassNames.outside
|
|
121
|
-
),
|
|
122
|
-
disabled: cn(
|
|
123
|
-
"text-muted-foreground opacity-50",
|
|
124
|
-
defaultClassNames.disabled
|
|
125
|
-
),
|
|
126
|
-
hidden: cn("invisible", defaultClassNames.hidden),
|
|
127
|
-
...classNames,
|
|
128
|
-
}}
|
|
129
|
-
components={{
|
|
130
|
-
Root: ({ className, rootRef, ...props }) => {
|
|
131
|
-
return (
|
|
132
|
-
<div
|
|
133
|
-
className={cn(className)}
|
|
134
|
-
data-slot="calendar"
|
|
135
|
-
ref={rootRef}
|
|
136
|
-
{...props}
|
|
137
|
-
/>
|
|
138
|
-
);
|
|
139
|
-
},
|
|
140
|
-
Chevron: ({ className, orientation, ...props }) => {
|
|
141
|
-
if (orientation === "left") {
|
|
142
|
-
return (
|
|
143
|
-
<HugeiconsIcon
|
|
144
|
-
className={cn("size-4", className)}
|
|
145
|
-
icon={ArrowLeftIcon}
|
|
146
|
-
strokeWidth={2}
|
|
147
|
-
{...props}
|
|
148
|
-
/>
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if (orientation === "right") {
|
|
153
|
-
return (
|
|
154
|
-
<HugeiconsIcon
|
|
155
|
-
className={cn("size-4", className)}
|
|
156
|
-
icon={ArrowRightIcon}
|
|
157
|
-
strokeWidth={2}
|
|
158
|
-
{...props}
|
|
159
|
-
/>
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
return (
|
|
164
|
-
<HugeiconsIcon
|
|
165
|
-
className={cn("size-4", className)}
|
|
166
|
-
icon={ArrowDownIcon}
|
|
167
|
-
strokeWidth={2}
|
|
168
|
-
{...props}
|
|
169
|
-
/>
|
|
170
|
-
);
|
|
171
|
-
},
|
|
172
|
-
DayButton: CalendarDayButton,
|
|
173
|
-
WeekNumber: ({ children, ...props }) => {
|
|
174
|
-
return (
|
|
175
|
-
<td {...props}>
|
|
176
|
-
<div className="flex size-(--cell-size) items-center justify-center text-center">
|
|
177
|
-
{children}
|
|
178
|
-
</div>
|
|
179
|
-
</td>
|
|
180
|
-
);
|
|
181
|
-
},
|
|
182
|
-
...components,
|
|
183
|
-
}}
|
|
184
|
-
formatters={{
|
|
185
|
-
formatMonthDropdown: (date) =>
|
|
186
|
-
date.toLocaleString("default", { month: "short" }),
|
|
187
|
-
...formatters,
|
|
188
|
-
}}
|
|
189
|
-
showOutsideDays={showOutsideDays}
|
|
190
|
-
{...props}
|
|
191
|
-
/>
|
|
192
|
-
);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function CalendarDayButton({
|
|
196
|
-
className,
|
|
197
|
-
day,
|
|
198
|
-
modifiers,
|
|
199
|
-
...props
|
|
200
|
-
}: ComponentProps<typeof DayButton>) {
|
|
201
|
-
const defaultClassNames = getDefaultClassNames();
|
|
202
|
-
|
|
203
|
-
const ref = useRef<HTMLButtonElement>(null);
|
|
204
|
-
useEffect(() => {
|
|
205
|
-
if (modifiers.focused) {
|
|
206
|
-
ref.current?.focus();
|
|
207
|
-
}
|
|
208
|
-
}, [modifiers.focused]);
|
|
209
|
-
|
|
210
|
-
return (
|
|
211
|
-
<Button
|
|
212
|
-
className={cn(
|
|
213
|
-
"relative isolate z-10 flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 border-0 font-normal leading-none data-[range-end=true]:rounded-(--cell-radius) data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-(--cell-radius) data-[range-end=true]:rounded-r-(--cell-radius) data-[range-start=true]:rounded-l-(--cell-radius) data-[range-end=true]:bg-primary data-[range-middle=true]:bg-muted data-[range-start=true]:bg-primary data-[selected-single=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-middle=true]:text-foreground data-[range-start=true]:text-primary-foreground data-[selected-single=true]:text-primary-foreground group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-[3px] group-data-[focused=true]/day:ring-ring/50 dark:hover:text-foreground [&>span]:text-xs [&>span]:opacity-70",
|
|
214
|
-
defaultClassNames.day,
|
|
215
|
-
className
|
|
216
|
-
)}
|
|
217
|
-
data-day={day.date.toLocaleDateString()}
|
|
218
|
-
data-range-end={modifiers.range_end}
|
|
219
|
-
data-range-middle={modifiers.range_middle}
|
|
220
|
-
data-range-start={modifiers.range_start}
|
|
221
|
-
data-selected-single={
|
|
222
|
-
modifiers.selected &&
|
|
223
|
-
!modifiers.range_start &&
|
|
224
|
-
!modifiers.range_end &&
|
|
225
|
-
!modifiers.range_middle
|
|
226
|
-
}
|
|
227
|
-
ref={ref}
|
|
228
|
-
size="icon"
|
|
229
|
-
variant="ghost"
|
|
230
|
-
{...props}
|
|
231
|
-
/>
|
|
232
|
-
);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
export { Calendar, CalendarDayButton };
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { Loading03Icon } from "@hugeicons/core-free-icons";
|
|
2
|
-
import { HugeiconsIcon } from "@hugeicons/react";
|
|
3
|
-
import { cn } from "@/lib/utils";
|
|
4
|
-
|
|
5
|
-
function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
|
|
6
|
-
return (
|
|
7
|
-
<HugeiconsIcon
|
|
8
|
-
aria-label="Loading"
|
|
9
|
-
className={cn("size-4 animate-spin", className)}
|
|
10
|
-
icon={Loading03Icon}
|
|
11
|
-
role="status"
|
|
12
|
-
{...props}
|
|
13
|
-
strokeWidth={2}
|
|
14
|
-
/>
|
|
15
|
-
);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export { Spinner };
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import type { Visibility as VisibilityType } from "./common";
|
|
3
|
-
import { Visibility } from "./common";
|
|
4
|
-
|
|
5
|
-
describe("Visibility", () => {
|
|
6
|
-
test("should contain all expected keys", () => {
|
|
7
|
-
const expectedKeys = ["PUBLIC", "PRIVATE"];
|
|
8
|
-
expect(Object.keys(Visibility)).toEqual(expectedKeys);
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
test("should have correct values for each visibility", () => {
|
|
12
|
-
expect(Visibility.PUBLIC).toBe("public");
|
|
13
|
-
expect(Visibility.PRIVATE).toBe("private");
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
test("should be usable as Visibility type", () => {
|
|
17
|
-
const pub: VisibilityType = "public";
|
|
18
|
-
const priv: VisibilityType = "private";
|
|
19
|
-
expect(pub).toBe("public");
|
|
20
|
-
expect(priv).toBe("private");
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
test("should have 2 visibilities", () => {
|
|
24
|
-
expect(Object.keys(Visibility).length).toBe(2);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
test("all values should be lowercase strings", () => {
|
|
28
|
-
Object.values(Visibility).forEach((v) => {
|
|
29
|
-
expect(v).toBe(v.toLowerCase() as VisibilityType);
|
|
30
|
-
expect(typeof v).toBe("string");
|
|
31
|
-
});
|
|
32
|
-
});
|
|
33
|
-
});
|