tangled
alpha
login
or
join now
shi.gg
/
mellow-web
4
fork
atom
The weeb for the next gen discord boat - Wamellow
wamellow.com
bot
discord
4
fork
atom
overview
issues
pulls
pipelines
replace nextui buttons & tooltips with custom
shi.gg
7 months ago
4ab589c9
291b5732
verified
This commit was signed with the committer's
known signature
.
shi.gg
SSH Key Fingerprint:
SHA256:4JreSzHjG+M8c3jYKvZRmuzCqt5pfWey2tlyOj/AGIY=
+570
-586
40 changed files
expand all
collapse all
unified
split
app
(home)
premium
subscribe.component.tsx
status
side.component.tsx
dashboard
[guildId]
custom-commands
create.component.tsx
delete.component.tsx
page.tsx
dailyposts
create.component.tsx
delete.component.tsx
page.tsx
layout.tsx
leaderboards
page.tsx
permissions.component.tsx
reset.component.tsx
widget-button.component.tsx
widget.component.tsx
notifications
delete.component.tsx
page.tsx
starboard
page.tsx
docs
[...pathname]
page.tsx
globals.css
layout.tsx
leaderboard
[guildId]
page.tsx
side.component.tsx
not-found.tsx
profile
billing
page.tsx
connections
page.tsx
layout.tsx
page.tsx
rank
card-style.component.tsx
components
ad.tsx
embed-creator.tsx
header.tsx
loading-circle.tsx
screen-message.tsx
section.tsx
ui
badge.tsx
button.tsx
separator.tsx
tooltip.tsx
lib
api
hook.ts
tailwind.config.js
+1
-1
app/(home)/premium/subscribe.component.tsx
···
97
97
value={donation}
98
98
/>
99
99
</InputBaseControl>
100
100
-
<Tooltip delayDuration={0}>
100
100
+
<Tooltip>
101
101
<InputBaseAdornment>
102
102
<InputBaseAdornmentButton asChild>
103
103
<TooltipTrigger>
+4
-6
app/(home)/status/side.component.tsx
···
1
1
"use client";
2
2
3
3
-
import { Accordion, AccordionItem, Chip } from "@nextui-org/react";
3
3
+
import { Accordion, AccordionItem } from "@nextui-org/react";
4
4
import { useCookies } from "next-client-cookies";
5
5
import { type ReactNode, useEffect, useMemo, useState } from "react";
6
6
7
7
import DumbTextInput from "@/components/inputs/dumb-text-input";
8
8
+
import { Badge } from "@/components/ui/badge";
8
9
import { intl } from "@/utils/numbers";
9
10
10
11
import type { ApiV1StatusGetResponse } from "./api";
···
78
79
return (
79
80
<div className="flex items-center justify-between">
80
81
{name}
81
81
-
<Chip
82
82
-
className="select-none"
83
83
-
radius="sm"
84
84
-
>
82
82
+
<Badge>
85
83
{children}
86
86
-
</Chip>
84
84
+
</Badge>
87
85
</div>
88
86
);
89
87
}
+47
-52
app/dashboard/[guildId]/custom-commands/create.component.tsx
···
1
1
"use client";
2
2
3
3
-
import { Button, Chip } from "@nextui-org/react";
4
3
import { useState } from "react";
5
4
import { HiPencil } from "react-icons/hi";
6
5
7
6
import DumbTextInput from "@/components/inputs/dumb-text-input";
8
7
import Modal from "@/components/modal";
8
8
+
import { Button } from "@/components/ui/button";
9
9
import type { ApiV1GuildsModulesTagsGetResponse } from "@/typings";
10
10
11
11
export enum Style {
···
26
26
const [open, setOpen] = useState(false);
27
27
const [name, setName] = useState("");
28
28
29
29
-
return (
30
30
-
<>
31
31
-
{style === Style.Compact
32
32
-
?
33
33
-
<Chip
34
34
-
as={Button}
35
35
-
className="default"
36
36
-
variant="faded"
37
37
-
onClick={() => setOpen(true)}
38
38
-
startContent={<HiPencil className="relative left-1 ml-1" />}
39
39
-
>
40
40
-
Create
41
41
-
</Chip>
42
42
-
:
43
43
-
<Button
44
44
-
color="secondary"
45
45
-
onClick={() => setOpen(true)}
46
46
-
startContent={<HiPencil />}
47
47
-
>
48
48
-
Create new tag
49
49
-
</Button>
50
50
-
}
51
51
-
52
52
-
<Modal<ApiV1GuildsModulesTagsGetResponse>
53
53
-
title="Create new tag"
54
54
-
isOpen={open}
55
55
-
onClose={() => setOpen(false)}
56
56
-
onSubmit={() => {
57
57
-
return fetch(`${process.env.NEXT_PUBLIC_API}/guilds/${guildId}/modules/tags`, {
58
58
-
method: "POST",
59
59
-
credentials: "include",
60
60
-
headers: {
61
61
-
"Content-Type": "application/json"
62
62
-
},
63
63
-
body: JSON.stringify({ name: name || "new-tag" })
64
64
-
});
65
65
-
}}
66
66
-
onSuccess={(tag) => {
67
67
-
addTag(tag);
68
68
-
setTagId(tag.id);
69
69
-
}}
29
29
+
return (<>
30
30
+
{style === Style.Compact
31
31
+
?
32
32
+
<Button
33
33
+
className="rounded-full h-8"
34
34
+
onClick={() => setOpen(true)}
35
35
+
>
36
36
+
<HiPencil />
37
37
+
Create
38
38
+
</Button>
39
39
+
:
40
40
+
<Button
41
41
+
variant="secondary"
42
42
+
onClick={() => setOpen(true)}
70
43
>
71
71
-
<DumbTextInput
72
72
-
name="Name"
73
73
-
placeholder="new-tag"
74
74
-
value={name}
75
75
-
setValue={setName}
76
76
-
max={32}
77
77
-
/>
78
78
-
</Modal>
79
79
-
</>
80
80
-
);
44
44
+
<HiPencil />
45
45
+
Create a new Tag
46
46
+
</Button>
47
47
+
}
81
48
49
49
+
<Modal<ApiV1GuildsModulesTagsGetResponse>
50
50
+
title="Create new tag"
51
51
+
isOpen={open}
52
52
+
onClose={() => setOpen(false)}
53
53
+
onSubmit={() => {
54
54
+
return fetch(`${process.env.NEXT_PUBLIC_API}/guilds/${guildId}/modules/tags`, {
55
55
+
method: "POST",
56
56
+
credentials: "include",
57
57
+
headers: {
58
58
+
"Content-Type": "application/json"
59
59
+
},
60
60
+
body: JSON.stringify({ name: name || "new-tag" })
61
61
+
});
62
62
+
}}
63
63
+
onSuccess={(tag) => {
64
64
+
addTag(tag);
65
65
+
setTagId(tag.id);
66
66
+
}}
67
67
+
>
68
68
+
<DumbTextInput
69
69
+
name="Name"
70
70
+
placeholder="new-tag"
71
71
+
value={name}
72
72
+
setValue={setName}
73
73
+
max={32}
74
74
+
/>
75
75
+
</Modal>
76
76
+
</>);
82
77
}
+33
-31
app/dashboard/[guildId]/custom-commands/delete.component.tsx
···
1
1
"use client";
2
2
3
3
-
import { Button, Tooltip } from "@nextui-org/react";
4
3
import { useState } from "react";
5
4
import { HiTrash } from "react-icons/hi";
6
5
7
6
import Modal from "@/components/modal";
7
7
+
import { Button } from "@/components/ui/button";
8
8
+
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
8
9
9
10
interface Props {
10
11
guildId: string;
···
18
19
19
20
const [open, setOpen] = useState(false);
20
21
21
21
-
return (
22
22
-
<>
23
23
-
<Tooltip content="Delete Tag" closeDelay={0}>
22
22
+
return (<>
23
23
+
<Tooltip>
24
24
+
<TooltipTrigger asChild>
24
25
<Button
25
25
-
isIconOnly
26
26
-
color="danger"
26
26
+
className="size-9 p-1.5"
27
27
+
variant="destructive"
27
28
onClick={() => setOpen(true)}
28
28
-
isDisabled={!id}
29
29
+
disabled={!id}
29
30
>
30
30
-
<HiTrash className="h-5 w-5" />
31
31
-
<span className="sr-only">Delete selected tag</span>
31
31
+
<HiTrash />
32
32
</Button>
33
33
-
</Tooltip>
34
34
-
35
35
-
<Modal
36
36
-
buttonName="Delete"
37
37
-
variant="destructive"
38
38
-
title={"Delete Tag: " + name}
39
39
-
isOpen={open}
40
40
-
onClose={() => setOpen(false)}
41
41
-
onSubmit={() => {
42
42
-
return fetch(`${process.env.NEXT_PUBLIC_API}/guilds/${guildId}/modules/tags/${id}`, {
43
43
-
method: "DELETE",
44
44
-
credentials: "include"
45
45
-
});
46
46
-
}}
47
47
-
onSuccess={() => {
48
48
-
if (id) removeTag(id);
49
49
-
}}
50
50
-
>
51
51
-
Are you sure you want to delete the {"\""}{name}{"\""} tag? It will be gone forever, probably, who knows.
52
52
-
</Modal>
53
53
-
</>
54
54
-
);
33
33
+
</TooltipTrigger>
34
34
+
<TooltipContent>
35
35
+
<p>Delete Tag</p>
36
36
+
</TooltipContent>
37
37
+
</Tooltip>
55
38
39
39
+
<Modal
40
40
+
buttonName="Delete"
41
41
+
variant="destructive"
42
42
+
title={"Delete Tag: " + name}
43
43
+
isOpen={open}
44
44
+
onClose={() => setOpen(false)}
45
45
+
onSubmit={() => {
46
46
+
return fetch(`${process.env.NEXT_PUBLIC_API}/guilds/${guildId}/modules/tags/${id}`, {
47
47
+
method: "DELETE",
48
48
+
credentials: "include"
49
49
+
});
50
50
+
}}
51
51
+
onSuccess={() => {
52
52
+
if (id) removeTag(id);
53
53
+
}}
54
54
+
>
55
55
+
Are you sure you want to delete the {"\""}{name}{"\""} tag? It will be gone forever, probably, who knows.
56
56
+
</Modal>
57
57
+
</>);
56
58
}
+21
-18
app/dashboard/[guildId]/custom-commands/page.tsx
···
1
1
"use client";
2
2
3
3
-
import { Button, Chip, Tooltip } from "@nextui-org/react";
4
4
-
import Image from "next/image";
5
3
import { useParams, usePathname, useRouter, useSearchParams } from "next/navigation";
6
4
import { useCallback, useEffect } from "react";
7
5
import { HiViewGridAdd } from "react-icons/hi";
···
13
11
import SelectInput from "@/components/inputs/select-menu";
14
12
import TextInput from "@/components/inputs/text-input";
15
13
import { ScreenMessage } from "@/components/screen-message";
14
14
+
import { Button } from "@/components/ui/button";
15
15
+
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
16
16
import { cacheOptions, getData } from "@/lib/api";
17
17
import { Permissions } from "@/lib/discord/enum/permissions";
18
18
-
import SadWumpusPic from "@/public/sad-wumpus.gif";
19
18
import type { ApiV1GuildsModulesTagsGetResponse } from "@/typings";
20
19
21
20
import CreateTag, { Style } from "./create.component";
···
66
65
href={`/dashboard/${guild?.id}`}
67
66
button="Go back to overview"
68
67
icon={<HiViewGridAdd />}
69
69
-
>
70
70
-
<Image src={SadWumpusPic} alt="" height={141} width={124} />
71
71
-
</ScreenMessage>
68
68
+
/>
72
69
);
73
70
}
74
71
···
95
92
};
96
93
97
94
const removeTag = (id: string) => {
98
98
-
queryClient.setQueryData<ApiV1GuildsModulesTagsGetResponse[]>(url, () =>
95
95
+
queryClient.setQueryData<ApiV1GuildsModulesTagsGetResponse[]>(url, () => (
99
96
data?.filter((t) => t.id !== id) || []
100
100
-
);
97
97
+
));
101
98
};
102
99
103
100
return (<>
···
106
103
{data
107
104
.sort((a, b) => a.name.localeCompare(b.name))
108
105
.map((tag) => (
109
109
-
<Chip
106
106
+
<Button
110
107
key={"guildTags-" + tag.id}
111
111
-
as={Button}
112
112
-
className="default border-0"
113
113
-
variant={id === tag.id ? "flat" : "faded"}
114
114
-
color={id === tag.id ? "secondary" : undefined}
115
115
-
startContent={<span className="opacity-50 relative left-2">{tag.applicationCommandId ? "/" : "wm -"}</span>}
108
108
+
className="rounded-full h-8"
116
109
onClick={() => setTagId(tag.id)}
117
110
>
118
118
-
{tag.name + " "}
119
119
-
</Chip>
111
111
+
<span className="opacity-50 text-sm">
112
112
+
{tag.applicationCommandId ? "/" : "wm -"}
113
113
+
</span>
114
114
+
{tag.name}
115
115
+
</Button>
120
116
))
121
117
}
122
118
···
128
124
/>
129
125
130
126
<div className="ml-auto flex items-center gap-4">
131
131
-
<Tooltip content="Created tags / Limit" closeDelay={0}>
132
132
-
<span className="dark:text-neutral-600 text-neutral-400 cursor-default">{data.length}/{30}</span>
127
127
+
<Tooltip>
128
128
+
<TooltipTrigger>
129
129
+
<span className="dark:text-neutral-600 text-neutral-400 cursor-default">
130
130
+
{data.length}/{30}
131
131
+
</span>
132
132
+
</TooltipTrigger>
133
133
+
<TooltipContent>
134
134
+
<p>{data.length} created tags / {30} limit</p>
135
135
+
</TooltipContent>
133
136
</Tooltip>
134
137
135
138
<DeleteTag
+8
-9
app/dashboard/[guildId]/dailyposts/create.component.tsx
···
1
1
"use client";
2
2
3
3
-
import { Button, Chip } from "@nextui-org/react";
4
3
import { useMemo, useState } from "react";
5
4
import { HiPencil } from "react-icons/hi";
6
5
···
8
7
import MultiSelectMenu from "@/components/inputs/multi-select-menu";
9
8
import SelectMenu from "@/components/inputs/select-menu";
10
9
import Modal from "@/components/modal";
10
10
+
import { Button } from "@/components/ui/button";
11
11
import { type ApiV1GuildsModulesDailypostsGetResponse, DailypostType } from "@/typings";
12
12
import { createSelectableItems } from "@/utils/create-selectable-items";
13
13
···
43
43
return (<>
44
44
{style === Style.Compact
45
45
?
46
46
-
<Chip
47
47
-
as={Button}
48
48
-
className="default"
46
46
+
<Button
47
47
+
className="rounded-full h-8"
49
48
onClick={() => setOpen(true)}
50
50
-
startContent={<HiPencil className="relative left-1 ml-1" />}
51
49
>
52
52
-
Add Dailypost
53
53
-
</Chip>
50
50
+
<HiPencil />
51
51
+
Create
52
52
+
</Button>
54
53
:
55
54
<Button
56
56
-
color="secondary"
55
55
+
variant="secondary"
57
56
onClick={() => setOpen(true)}
58
58
-
startContent={<HiPencil />}
59
57
>
58
58
+
<HiPencil />
60
59
Create a new Dailypost
61
60
</Button>
62
61
}
+15
-16
app/dashboard/[guildId]/dailyposts/delete.component.tsx
···
1
1
"use client";
2
2
3
3
-
import { Button, Tooltip } from "@nextui-org/react";
4
3
import { useState } from "react";
5
4
import { HiTrash } from "react-icons/hi";
6
5
7
6
import { guildStore } from "@/common/guilds";
8
7
import Modal from "@/components/modal";
8
8
+
import { Button } from "@/components/ui/button";
9
9
+
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
9
10
10
11
interface Props {
11
12
id: string | null;
···
24
25
const [open, setOpen] = useState(false);
25
26
26
27
return (<>
27
27
-
<Tooltip
28
28
-
content="Delete Dailypost"
29
29
-
closeDelay={0}
30
30
-
>
31
31
-
<Button
32
32
-
isIconOnly
33
33
-
color="danger"
34
34
-
variant="flat"
35
35
-
onClick={() => setOpen(true)}
36
36
-
isDisabled={!id}
37
37
-
>
38
38
-
<span>
28
28
+
<Tooltip>
29
29
+
<TooltipTrigger asChild>
30
30
+
<Button
31
31
+
className="size-9 p-1.5"
32
32
+
variant="destructive"
33
33
+
onClick={() => setOpen(true)}
34
34
+
disabled={!id}
35
35
+
>
39
36
<HiTrash />
40
40
-
</span>
41
41
-
<span className="sr-only">Delete selected dailypost</span>
42
42
-
</Button>
37
37
+
</Button>
38
38
+
</TooltipTrigger>
39
39
+
<TooltipContent>
40
40
+
<p>Delete Dailypost</p>
41
41
+
</TooltipContent>
43
42
</Tooltip>
44
43
45
44
<Modal
+1
-4
app/dashboard/[guildId]/dailyposts/page.tsx
···
13
13
import MultiSelectMenu from "@/components/inputs/multi-select-menu";
14
14
import SelectMenu from "@/components/inputs/select-menu";
15
15
import { ScreenMessage } from "@/components/screen-message";
16
16
-
import SadWumpusPic from "@/public/sad-wumpus.gif";
17
16
import type { ApiV1GuildsModulesDailypostsGetResponse } from "@/typings";
18
17
import { createSelectableItems } from "@/utils/create-selectable-items";
19
18
···
49
48
href={`/dashboard/${guild?.id}`}
50
49
button="Go back to overview"
51
50
icon={<HiViewGridAdd />}
52
52
-
>
53
53
-
<Image src={SadWumpusPic} alt="" height={141} width={124} />
54
54
-
</ScreenMessage>
51
51
+
/>
55
52
);
56
53
}
57
54
+23
-21
app/dashboard/[guildId]/layout.tsx
···
1
1
"use client";
2
2
3
3
-
import { Button, Skeleton } from "@nextui-org/react";
4
3
import Head from "next/head";
5
5
-
import Image from "next/image";
6
4
import Link from "next/link";
7
5
import { redirect, useParams } from "next/navigation";
8
6
import { useCookies } from "next-client-cookies";
···
11
9
import { useQuery } from "react-query";
12
10
13
11
import { guildStore } from "@/common/guilds";
14
14
-
import { ClientButton } from "@/components/client";
15
12
import ImageReduceMotion from "@/components/image-reduce-motion";
16
13
import { ListTab } from "@/components/list";
17
14
import { ScreenMessage, SupportButton } from "@/components/screen-message";
15
15
+
import { Button } from "@/components/ui/button";
16
16
+
import { Skeleton } from "@/components/ui/skeleton";
18
17
import { cacheOptions, getData } from "@/lib/api";
19
19
-
import SadWumpusPic from "@/public/sad-wumpus.gif";
20
18
import type { ApiV1GuildsChannelsGetResponse, ApiV1GuildsEmojisGetResponse, ApiV1GuildsGetResponse, ApiV1GuildsRolesGetResponse } from "@/typings";
21
19
import { intl } from "@/utils/numbers";
22
20
···
104
102
<div className="flex flex-col w-full">
105
103
{guild?.name && (
106
104
<Head>
107
107
-
<title>{`${guild?.name}'s Dashboard`}</title>
105
105
+
<title>{guild.name}{"'"}s Dashboard</title>
108
106
</Head>
109
107
)}
110
108
111
109
<div className="flex flex-col gap-5 mb-3">
112
110
<Button
113
113
-
as={Link}
111
111
+
asChild
114
112
className="w-fit"
115
115
-
href="/profile"
116
116
-
startContent={<HiArrowNarrowLeft />}
117
113
>
118
118
-
Serverlist
114
114
+
<Link href="/profile">
115
115
+
<HiArrowNarrowLeft />
116
116
+
Serverlist
117
117
+
</Link>
119
118
</Button>
120
119
121
120
<div className="text-lg flex gap-5">
122
122
-
<Skeleton isLoaded={!isLoading} className="rounded-full h-14 w-14 ring-offset-[var(--background-rgb)] ring-2 ring-offset-2 ring-violet-400/40 shrink-0">
121
121
+
<Skeleton
122
122
+
isLoading={!guild?.id}
123
123
+
className="rounded-full h-14 w-14 ring-offset-[var(--background-rgb)] ring-2 ring-offset-2 ring-violet-400/40 shrink-0 relative top-1"
124
124
+
>
123
125
<ImageReduceMotion
124
124
-
alt="this server"
126
126
+
alt="this server's icon"
125
127
className="rounded-full"
126
128
url={`https://cdn.discordapp.com/icons/${guild?.id}/${guild?.icon}`}
127
129
size={128}
···
197
199
<ScreenMessage
198
200
title={error.includes("permssions")
199
201
? "You cannot access this page.."
200
200
-
: "Something went wrong on this page.."
202
202
+
: undefined
201
203
}
202
204
description={error}
203
205
buttons={<>
204
204
-
<ClientButton
205
205
-
as={Link}
206
206
-
href="/profile"
207
207
-
startContent={<HiViewGridAdd />}
206
206
+
<Button
207
207
+
asChild
208
208
+
variant="secondary"
208
209
>
209
209
-
Go back to Dashboard
210
210
-
</ClientButton>
210
210
+
<Link href="/profile">
211
211
+
<HiViewGridAdd />
212
212
+
Go back to Dashboard
213
213
+
</Link>
214
214
+
</Button>
211
215
<SupportButton />
212
216
</>}
213
213
-
>
214
214
-
<Image src={SadWumpusPic} alt="" height={141} width={124} />
215
215
-
</ScreenMessage>
217
217
+
/>
216
218
:
217
219
(guild && loaded.length === 3) ? children : <></>
218
220
}
+6
-22
app/dashboard/[guildId]/leaderboards/page.tsx
···
1
1
"use client";
2
2
3
3
import { ChannelType } from "discord-api-types/v10";
4
4
-
import Image from "next/image";
5
4
import { useParams } from "next/navigation";
6
5
import { HiChartBar, HiViewGridAdd } from "react-icons/hi";
7
7
-
import { useQuery } from "react-query";
8
6
9
7
import { type Guild, guildStore } from "@/common/guilds";
10
8
import ImageUrlInput from "@/components/inputs/image-url-input";
11
9
import MultiSelectMenu from "@/components/inputs/multi-select-menu";
12
10
import { ScreenMessage } from "@/components/screen-message";
13
11
import { Section, SubSection } from "@/components/section";
14
14
-
import { cacheOptions, getData } from "@/lib/api";
15
15
-
import SadWumpusPic from "@/public/sad-wumpus.gif";
12
12
+
import { useApi } from "@/lib/api/hook";
16
13
import type { ApiV1GuildsModulesLeaderboardGetResponse } from "@/typings";
17
14
import { createSelectableItems } from "@/utils/create-selectable-items";
18
15
···
27
24
const params = useParams();
28
25
29
26
const url = `/guilds/${params.guildId}/modules/leaderboard` as const;
27
27
+
const { data, isLoading, error } = useApi<ApiV1GuildsModulesLeaderboardGetResponse>(url);
30
28
31
31
-
const { data, isLoading, error } = useQuery(
32
32
-
url,
33
33
-
() => getData<ApiV1GuildsModulesLeaderboardGetResponse>(url),
34
34
-
{
35
35
-
enabled: !!params.guildId,
36
36
-
...cacheOptions,
37
37
-
refetchOnMount: true
38
38
-
}
39
39
-
);
29
29
+
if (isLoading) return <></>;
40
30
41
41
-
if (error || (data && "message" in data)) {
31
31
+
if (!data || error) {
42
32
return (
43
33
<ScreenMessage
44
34
top="0rem"
45
45
-
title="Something went wrong on this page.."
46
46
-
description={
47
47
-
(data && "message" in data ? data.message : `${error}`)
48
48
-
|| "An unknown error occurred."}
35
35
+
description={error}
49
36
href={`/dashboard/${guild?.id}`}
50
37
button="Go back to overview"
51
38
icon={<HiViewGridAdd />}
52
52
-
>
53
53
-
<Image src={SadWumpusPic} alt="" height={141} width={124} />
54
54
-
</ScreenMessage>
39
39
+
/>
55
40
);
56
41
}
57
42
58
58
-
if (isLoading || !data) return <></>;
59
43
60
44
return (<>
61
45
<div className="flex flex-col-reverse md:flex-row gap-6">
+20
-16
app/dashboard/[guildId]/leaderboards/permissions.component.tsx
···
1
1
-
import { Tooltip } from "@nextui-org/react";
2
1
import { PermissionFlagsBits } from "discord-api-types/v10";
3
2
import { useMemo } from "react";
4
3
import { HiExclamation } from "react-icons/hi";
5
4
6
5
import type { Guild } from "@/common/guilds";
7
6
import DiscordChannel from "@/components/discord/channel";
7
7
+
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
8
8
import type { ApiV1GuildsChannelsGetResponse } from "@/typings";
9
9
import { cn } from "@/utils/cn";
10
10
···
46
46
}) {
47
47
return (<>
48
48
<p className="text-sm opacity-75 mb-3">
49
49
-
Wammellow cannot track acivity in {channels.length} channels as it is missing permissions.
49
49
+
Wammellow cannot track activity in {channels.length} channels as it is missing permissions.
50
50
</p>
51
51
52
52
<div className="flex flex-col gap-1">
53
53
-
54
53
{channels.map((c, i) => {
55
54
if (i >= MAX_CHANNELS) return null;
56
55
return (
···
63
62
name={c.name}
64
63
isTruncated
65
64
/>
66
66
-
<Tooltip
67
67
-
content={"Missing View Channel"}
68
68
-
placement="right"
69
69
-
className="bg-[#030206]"
70
70
-
closeDelay={0}
71
71
-
>
72
72
-
<span>
65
65
+
<Tooltip>
66
66
+
<TooltipTrigger asChild>
73
67
<HiExclamation className="text-red-500" />
74
74
-
</span>
68
68
+
</TooltipTrigger>
69
69
+
<TooltipContent>
70
70
+
<p>Missing View Channel</p>
71
71
+
</TooltipContent>
75
72
</Tooltip>
76
73
</div>
77
74
);
78
75
})}
79
76
</div>
80
77
81
81
-
{channels.length > MAX_CHANNELS &&
82
82
-
<p className="text-sm opacity-75 relative bottom-1 mt-2">
83
83
-
+{channels.length - MAX_CHANNELS} more
84
84
-
</p>
85
85
-
}
78
78
+
{channels.length > MAX_CHANNELS && (
79
79
+
<Tooltip>
80
80
+
<TooltipTrigger>
81
81
+
<p className="text-sm opacity-75 relative bottom-1 mt-2">
82
82
+
+{channels.length - MAX_CHANNELS} more
83
83
+
</p>
84
84
+
</TooltipTrigger>
85
85
+
<TooltipContent>
86
86
+
{channels.slice(MAX_CHANNELS).map((channel) => "#" + channel.name).join(", ")}
87
87
+
</TooltipContent>
88
88
+
</Tooltip>
89
89
+
)}
86
90
</>);
87
91
}
+39
-42
app/dashboard/[guildId]/leaderboards/reset.component.tsx
···
1
1
-
import { Button } from "@nextui-org/react";
2
1
import { useState } from "react";
3
2
import { HiTrash, HiUsers } from "react-icons/hi";
4
3
···
6
5
import ImageReduceMotion from "@/components/image-reduce-motion";
7
6
import Modal from "@/components/modal";
8
7
import Notice, { NoticeType } from "@/components/notice";
8
8
+
import { Button } from "@/components/ui/button";
9
9
import { intl } from "@/utils/numbers";
10
10
11
11
interface Props {
···
20
20
21
21
const [modal, setModal] = useState<ModalType>();
22
22
23
23
-
return (
24
24
-
<>
25
25
-
<Button
26
26
-
onClick={() => setModal(ModalType.Delete)}
27
27
-
color="danger"
28
28
-
variant="flat"
29
29
-
startContent={<HiTrash />}
30
30
-
>
31
31
-
Reset Leaderboard
32
32
-
</Button>
23
23
+
return (<>
24
24
+
<Button
25
25
+
onClick={() => setModal(ModalType.Delete)}
26
26
+
variant="destructive"
27
27
+
>
28
28
+
<HiTrash />
29
29
+
Reset Leaderboard
30
30
+
</Button>
31
31
+
32
32
+
<Modal
33
33
+
title="Reset @everyone's stats"
34
34
+
buttonName="Reset"
35
35
+
variant="destructive"
36
36
+
isOpen={modal === ModalType.Delete}
37
37
+
onClose={() => setModal(undefined)}
38
38
+
onSubmit={() => {
39
39
+
return fetch(`${process.env.NEXT_PUBLIC_API}/guilds/${guild.id}/top-members`, {
40
40
+
method: "DELETE",
41
41
+
credentials: "include"
42
42
+
});
43
43
+
}}
44
44
+
>
45
45
+
<Notice
46
46
+
type={NoticeType.Info}
47
47
+
message="Takes a few seconds to apply"
48
48
+
/>
33
49
34
34
-
<Modal
35
35
-
title="Reset @everyone's stats"
36
36
-
buttonName="Reset"
37
37
-
variant="destructive"
38
38
-
isOpen={modal === ModalType.Delete}
39
39
-
onClose={() => setModal(undefined)}
40
40
-
onSubmit={() => {
41
41
-
return fetch(`${process.env.NEXT_PUBLIC_API}/guilds/${guild.id}/top-members`, {
42
42
-
method: "DELETE",
43
43
-
credentials: "include"
44
44
-
});
45
45
-
}}
46
46
-
>
47
47
-
<Notice
48
48
-
type={NoticeType.Info}
49
49
-
message="Takes a few seconds to apply"
50
50
+
<div className="flex items-center gap-3">
51
51
+
<ImageReduceMotion
52
52
+
alt="Guild Icon"
53
53
+
className="rounded-full"
54
54
+
url={`https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}`}
55
55
+
size={56}
50
56
/>
51
57
52
52
-
<div className="flex items-center gap-3">
53
53
-
<ImageReduceMotion
54
54
-
alt="Guild Icon"
55
55
-
className="rounded-full"
56
56
-
url={`https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}`}
57
57
-
size={56}
58
58
-
/>
59
59
-
60
60
-
<div className="flex flex-col gap-1">
61
61
-
<div className="text-xl dark:text-neutral-200 text-neutral-800 font-medium">{guild?.name || "Unknown Server"}</div>
62
62
-
<div className="text-sm font-semibold flex items-center gap-1"> <HiUsers /> {intl.format(guild?.memberCount || 0)}</div>
63
63
-
</div>
58
58
+
<div className="flex flex-col gap-1">
59
59
+
<div className="text-xl dark:text-neutral-200 text-neutral-800 font-medium">{guild?.name || "Unknown Server"}</div>
60
60
+
<div className="text-sm font-semibold flex items-center gap-1"> <HiUsers /> {intl.format(guild?.memberCount || 0)}</div>
64
61
</div>
62
62
+
</div>
65
63
66
66
-
</Modal>
67
67
-
</>
68
68
-
);
64
64
+
</Modal>
65
65
+
</>);
69
66
}
+30
-31
app/dashboard/[guildId]/leaderboards/widget-button.component.tsx
···
1
1
-
import { Button } from "@nextui-org/react";
2
1
import React, { useState } from "react";
3
2
import { HiEmojiHappy, HiLockClosed } from "react-icons/hi";
3
3
+
4
4
+
import { Button } from "@/components/ui/button";
4
5
5
6
enum State {
6
7
Idle = 0,
···
54
55
setError(res.status + ": " + res.statusText);
55
56
};
56
57
57
57
-
return (
58
58
-
<>
59
59
-
<Button
60
60
-
className="w-fit"
61
61
-
onClick={handle}
62
62
-
color={
63
63
-
isEnabled
64
64
-
? "danger"
65
65
-
: "secondary"
66
66
-
}
67
67
-
startContent={
68
68
-
isEnabled
69
69
-
? <HiLockClosed />
70
70
-
: <HiEmojiHappy />
71
71
-
}
72
72
-
variant="flat"
73
73
-
isLoading={state === State.Loading}
74
74
-
isDisabled={state === State.Ratelimited}
75
75
-
>
76
76
-
{isEnabled
77
77
-
? "Disable invite widget"
78
78
-
: "Enable invite widget"
79
79
-
}
80
80
-
</Button>
58
58
+
return (<>
59
59
+
<Button
60
60
+
className="w-fit"
61
61
+
onClick={handle}
62
62
+
variant={
63
63
+
isEnabled
64
64
+
? "destructive"
65
65
+
: "secondary"
66
66
+
}
67
67
+
icon={
68
68
+
isEnabled
69
69
+
? <HiLockClosed />
70
70
+
: <HiEmojiHappy />
81
71
82
82
-
{error &&
83
83
-
<div className="text-red-500 text-sm mt-1">
84
84
-
{error}
85
85
-
</div>
72
72
+
}
73
73
+
loading={state === State.Loading}
74
74
+
disabled={state === State.Ratelimited}
75
75
+
>
76
76
+
{isEnabled
77
77
+
? "Disable invite widget"
78
78
+
: "Enable invite widget"
86
79
}
87
87
-
</>
88
88
-
);
80
80
+
</Button>
81
81
+
82
82
+
{error && (
83
83
+
<div className="text-red-500 text-sm mt-1">
84
84
+
{error}
85
85
+
</div>
86
86
+
)}
87
87
+
</>);
89
88
}
+24
-26
app/dashboard/[guildId]/leaderboards/widget.component.tsx
···
1
1
-
import { Skeleton } from "@nextui-org/react";
2
1
import type { RESTError, RESTGetAPIGuildWidgetJSONResult } from "discord-api-types/v10";
3
2
import { useState } from "react";
4
3
import { HiEmojiHappy, HiLockClosed } from "react-icons/hi";
···
6
5
7
6
import type { Guild } from "@/common/guilds";
8
7
import Notice from "@/components/notice";
8
8
+
import { Skeleton } from "@/components/ui/skeleton";
9
9
import { cacheOptions } from "@/lib/api";
10
10
import { cn } from "@/utils/cn";
11
11
···
47
47
if (isLoading || !data) {
48
48
return (
49
49
<div className="pt-1">
50
50
-
<Skeleton className="h-4 w-96 mb-2.5 rounded-lg" />
50
50
+
<Skeleton className="h-5 w-96 mb-2.5 rounded-lg" />
51
51
<Skeleton className="h-10 w-52 rounded-lg" />
52
52
</div>
53
53
);
54
54
}
55
55
56
56
-
return (
57
57
-
<>
58
58
-
<div className={cn(
59
59
-
"flex items-center gap-1 mb-2 font-medium",
60
60
-
isEnabled
61
61
-
? "text-violet-400"
62
62
-
: "text-red-500"
63
63
-
)}>
64
64
-
{isEnabled
65
65
-
? <HiEmojiHappy />
66
66
-
: <HiLockClosed />
67
67
-
}
68
68
-
{isEnabled
69
69
-
? "Invite widget is enabled — people can join this server"
70
70
-
: "Invite widget is disabled — this server is private"
71
71
-
}
72
72
-
</div>
56
56
+
return (<>
57
57
+
<div className={cn(
58
58
+
"flex items-center gap-1 mb-2 font-medium",
59
59
+
isEnabled
60
60
+
? "text-violet-400"
61
61
+
: "text-red-500"
62
62
+
)}>
63
63
+
{isEnabled
64
64
+
? <HiEmojiHappy />
65
65
+
: <HiLockClosed />
66
66
+
}
67
67
+
{isEnabled
68
68
+
? "Invite widget is enabled — people can join this server"
69
69
+
: "Invite widget is disabled — this server is private"
70
70
+
}
71
71
+
</div>
73
72
74
74
-
<DiscordWidgetButton
75
75
-
guildId={guild.id}
76
76
-
isEnabled={isEnabled}
77
77
-
setEnabled={setEnabled}
78
78
-
/>
79
79
-
</>
80
80
-
);
73
73
+
<DiscordWidgetButton
74
74
+
guildId={guild.id}
75
75
+
isEnabled={isEnabled}
76
76
+
setEnabled={setEnabled}
77
77
+
/>
78
78
+
</>);
81
79
}
+16
-16
app/dashboard/[guildId]/notifications/delete.component.tsx
···
1
1
"use client";
2
2
3
3
-
import { Button, Tooltip } from "@nextui-org/react";
3
3
+
4
4
import { useState } from "react";
5
5
import { HiTrash } from "react-icons/hi";
6
6
7
7
import { guildStore } from "@/common/guilds";
8
8
import Modal from "@/components/modal";
9
9
+
import { Button } from "@/components/ui/button";
10
10
+
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
9
11
10
12
interface Props {
11
13
id: string | null;
···
24
26
const [open, setOpen] = useState(false);
25
27
26
28
return (<>
27
27
-
<Tooltip
28
28
-
content="Delete Notification"
29
29
-
closeDelay={0}
30
30
-
>
31
31
-
<Button
32
32
-
isIconOnly
33
33
-
color="danger"
34
34
-
variant="flat"
35
35
-
onClick={() => setOpen(true)}
36
36
-
isDisabled={!id}
37
37
-
>
38
38
-
<span>
29
29
+
<Tooltip>
30
30
+
<TooltipTrigger asChild>
31
31
+
<Button
32
32
+
className="size-9 p-1.5"
33
33
+
variant="destructive"
34
34
+
onClick={() => setOpen(true)}
35
35
+
disabled={!id}
36
36
+
>
39
37
<HiTrash />
40
40
-
</span>
41
41
-
<span className="sr-only">Delete selected notification</span>
42
42
-
</Button>
38
38
+
</Button>
39
39
+
</TooltipTrigger>
40
40
+
<TooltipContent>
41
41
+
<p>Delete Notification</p>
42
42
+
</TooltipContent>
43
43
</Tooltip>
44
44
45
45
<Modal
+1
-5
app/dashboard/[guildId]/notifications/page.tsx
···
21
21
import { ScreenMessage } from "@/components/screen-message";
22
22
import { Button } from "@/components/ui/button";
23
23
import { cacheOptions } from "@/lib/api";
24
24
-
import SadWumpusPic from "@/public/sad-wumpus.gif";
25
24
import { type ApiV1GuildsModulesNotificationsGetResponse, NotificationFlags, NotificationType } from "@/typings";
26
25
import { BitfieldManager, bitfieldToArray } from "@/utils/bitfields";
27
26
import { createSelectableItems } from "@/utils/create-selectable-items";
···
53
52
return (
54
53
<ScreenMessage
55
54
top="20vh"
56
56
-
title="Something went wrong on this page.."
57
55
description={error}
58
56
href={`/dashboard/${guild?.id}`}
59
57
button="Go back to overview"
60
58
icon={<HiViewGridAdd />}
61
61
-
>
62
62
-
<Image src={SadWumpusPic} alt="" height={141} width={124} />
63
63
-
</ScreenMessage>
59
59
+
/>
64
60
);
65
61
}
66
62
+9
-7
app/dashboard/[guildId]/starboard/page.tsx
···
1
1
"use client";
2
2
3
3
-
import { Button } from "@nextui-org/react";
4
3
import Image from "next/image";
5
4
import Link from "next/link";
6
5
import { useParams } from "next/navigation";
···
16
15
import Switch from "@/components/inputs/switch";
17
16
import TextInput from "@/components/inputs/text-input";
18
17
import Notice from "@/components/notice";
18
18
+
import { Button } from "@/components/ui/button";
19
19
import { useApi } from "@/lib/api/hook";
20
20
import { type ApiV1GuildsModulesStarboardGetResponse, StarboardStyle } from "@/typings";
21
21
import { createSelectableItems } from "@/utils/create-selectable-items";
···
45
45
return (<>
46
46
<div className="flex justify-between relative bottom-2 mb-3">
47
47
<Button
48
48
-
className="ml-auto"
49
49
-
as={Link}
50
50
-
href="/docs/starboard"
51
51
-
target="_blank"
52
52
-
endContent={<HiExternalLink />}
48
48
+
asChild
53
49
size="sm"
54
50
>
55
55
-
Read docs
51
51
+
<Link
52
52
+
href="/docs/starboard"
53
53
+
target="_blank"
54
54
+
>
55
55
+
<HiExternalLink />
56
56
+
Read docs
57
57
+
</Link>
56
58
</Button>
57
59
</div>
58
60
+2
-10
app/docs/[...pathname]/page.tsx
···
1
1
import { Code } from "@nextui-org/react";
2
2
import { readFile } from "fs/promises";
3
3
-
import Image from "next/image";
4
3
5
4
import { Faq } from "@/app/(home)/faq.component";
6
5
import BeautifyMarkdown from "@/components/markdown";
7
6
import Notice, { NoticeType } from "@/components/notice";
8
8
-
import { HomeButton, ScreenMessage, SupportButton } from "@/components/screen-message";
7
7
+
import { ScreenMessage } from "@/components/screen-message";
9
8
import metadata from "@/public/docs/meta.json";
10
10
-
import SadWumpusPic from "@/public/sad-wumpus.gif";
11
9
12
10
interface Props {
13
11
params: Promise<{ pathname: string[]; }>;
···
26
24
top="6rem"
27
25
title="Sadly, this page can not be found.."
28
26
description="Seems like you got a little lost here? Here's wumpus for now!"
29
29
-
buttons={<>
30
30
-
<HomeButton />
31
31
-
<SupportButton />
32
32
-
</>}
33
33
-
>
34
34
-
<Image src={SadWumpusPic} alt="" height={141} width={124} />
35
35
-
</ScreenMessage>
27
27
+
/>
36
28
);
37
29
}
38
30
+1
app/globals.css
···
42
42
--border: 260 3% 16%;
43
43
--input: 260 3% 16%;
44
44
--ring: 258 89% 66%;
45
45
+
--separator: 0 0% 100%;
45
46
46
47
--chart-1: 220 70% 50%;
47
48
--chart-2: 160 60% 45%;
+2
-2
app/layout.tsx
···
1
1
import "./globals.css";
2
2
3
3
-
import { Divider } from "@nextui-org/react";
4
3
import type { Metadata, Viewport } from "next";
5
4
import { Lexend, Noto_Sans_JP, Outfit } from "next/font/google";
6
5
import { cookies } from "next/headers";
···
11
10
12
11
import { Header } from "@/components/header";
13
12
import { LoginButton } from "@/components/login-button";
13
13
+
import { Separator } from "@/components/ui/separator";
14
14
import { cn } from "@/utils/cn";
15
15
import { getBaseUrl } from "@/utils/urls";
16
16
···
182
182
<span className="text-xl dark:text-neutral-100 text-neutral-900 hidden sm:block">Wamellow</span>
183
183
</Link>
184
184
185
185
-
<Divider
185
185
+
<Separator
186
186
className="h-10 rotate-6 mx-3"
187
187
orientation="vertical"
188
188
/>
+4
-17
app/leaderboard/[guildId]/page.tsx
···
1
1
import { cookies } from "next/headers";
2
2
-
import Image from "next/image";
3
2
import { redirect } from "next/navigation";
4
3
5
5
-
import { AddButton, HomeButton, ScreenMessage, SupportButton } from "@/components/screen-message";
4
4
+
import { ScreenMessage } from "@/components/screen-message";
6
5
import { getGuild } from "@/lib/api";
7
7
-
import SadWumpusPic from "@/public/sad-wumpus.gif";
8
6
9
7
import { getPagination, getTopMembers } from "./api";
10
8
import Member from "./member.component";
···
47
45
if (error || !guild || !members || !pagination || "message" in pagination) {
48
46
return (
49
47
<ScreenMessage
50
50
-
top="0rem"
51
51
-
title="Something went wrong on this page.."
48
48
+
top="14rem"
52
49
description={error}
53
53
-
buttons={<>
54
54
-
<HomeButton />
55
55
-
<SupportButton />
56
56
-
</>}
57
57
-
>
58
58
-
<Image src={SadWumpusPic} alt="" height={141 * 1.5} width={124 * 1.5} />
59
59
-
</ScreenMessage>
50
50
+
/>
60
51
);
61
52
}
62
53
63
54
if (!Array.isArray(members) || !members.length) {
64
55
return (
65
56
<ScreenMessage
66
66
-
top="0rem"
57
57
+
top="14rem"
67
58
title="No members to see here.."
68
59
description="No members could be found on this page"
69
69
-
buttons={<>
70
70
-
<HomeButton />
71
71
-
<AddButton />
72
72
-
</>}
73
60
/>
74
61
);
75
62
}
+22
-15
app/leaderboard/[guildId]/side.component.tsx
···
1
1
"use client";
2
2
3
3
-
import { Accordion, AccordionItem, Button, Code } from "@nextui-org/react";
3
3
+
import { Accordion, AccordionItem, Code } from "@nextui-org/react";
4
4
import Link from "next/link";
5
5
import { useRouter } from "next/navigation";
6
6
import { useCookies } from "next-client-cookies";
···
12
12
import Modal from "@/components/modal";
13
13
import Notice, { NoticeType } from "@/components/notice";
14
14
import { Share } from "@/components/share";
15
15
+
import { Button } from "@/components/ui/button";
15
16
import type { ApiError, ApiV1GuildsGetResponse, ApiV1GuildsTopmembersPaginationGetResponse } from "@/typings";
16
17
import { intl } from "@/utils/numbers";
17
18
import { getCanonicalUrl } from "@/utils/urls";
···
41
42
42
43
{guild && "inviteUrl" in guild && guild.inviteUrl &&
43
44
<Button
44
44
-
as={Link}
45
45
-
className="w-full !justify-start"
46
46
-
color="secondary"
47
47
-
href={guild.inviteUrl}
48
48
-
target="_blank"
49
49
-
startContent={<BsDiscord />}
45
45
+
asChild
46
46
+
className="justify-start"
47
47
+
variant="secondary"
50
48
>
51
51
-
Join {guild.name}
49
49
+
<Link
50
50
+
href={guild.inviteUrl}
51
51
+
target="_blank"
52
52
+
>
53
53
+
<BsDiscord />
54
54
+
Join {guild.name}
55
55
+
</Link>
52
56
</Button>
53
57
}
54
58
···
68
72
classNames={{ content: "mb-2" }}
69
73
>
70
74
<Button
71
71
-
className="w-full !justify-start"
75
75
+
className="w-full justify-start"
72
76
onClick={() => setModal(true)}
73
73
-
startContent={<HiTrash />}
74
77
>
78
78
+
<HiTrash />
75
79
Reset member stats
76
80
</Button>
77
81
<Button
78
78
-
as={Link}
79
79
-
className="w-full !justify-start mt-2"
80
80
-
href={getCanonicalUrl("dashboard", guild.id as string)}
81
81
-
startContent={<HiViewGridAdd />}
82
82
+
asChild
83
83
+
className="w-full justify-start mt-2"
82
84
>
83
83
-
Dashboard
85
85
+
<Link
86
86
+
href={getCanonicalUrl("dashboard", guild.id as string)}
87
87
+
>
88
88
+
<HiViewGridAdd />
89
89
+
Dashboard
90
90
+
</Link>
84
91
</Button>
85
92
</AccordionItem>
86
93
:
+2
-11
app/not-found.tsx
···
1
1
-
import Image from "next/image";
2
2
-
3
3
-
import { AddButton, HomeButton, ScreenMessage } from "@/components/screen-message";
4
4
-
import SadWumpusPic from "@/public/sad-wumpus.gif";
1
1
+
import { ScreenMessage } from "@/components/screen-message";
5
2
6
3
export default function NotFound() {
7
4
return (
8
5
<ScreenMessage
9
6
title="Sadly, this page can not be found.."
10
7
description="Seems like you got a little lost here? Here's wumpus for now!"
11
11
-
buttons={<>
12
12
-
<HomeButton />
13
13
-
<AddButton />
14
14
-
</>}
15
15
-
>
16
16
-
<Image src={SadWumpusPic} alt="" height={141 * 1.5} width={124 * 1.5} />
17
17
-
</ScreenMessage>
8
8
+
/>
18
9
);
19
10
}
+2
-2
app/profile/billing/page.tsx
···
1
1
"use client";
2
2
3
3
import { Turnstile, type TurnstileInstance } from "@marsidev/react-turnstile";
4
4
-
import { Link } from "@nextui-org/react";
4
4
+
import Link from "next/link";
5
5
import { useRef, useState } from "react";
6
6
import { GrAmex } from "react-icons/gr";
7
7
import { HiCreditCard, HiLightningBolt } from "react-icons/hi";
···
115
115
className="ml-auto"
116
116
variant="link"
117
117
>
118
118
-
<Link href={data?.portalUrl}>
118
118
+
<Link href={data!.portalUrl}>
119
119
Change
120
120
</Link>
121
121
</Button>
+2
-14
app/profile/connections/page.tsx
···
10
10
import DumbTextInput from "@/components/inputs/dumb-text-input";
11
11
import Modal from "@/components/modal";
12
12
import Notice, { NoticeType } from "@/components/notice";
13
13
-
import { HomeButton, ScreenMessage, SupportButton } from "@/components/screen-message";
13
13
+
import { ScreenMessage } from "@/components/screen-message";
14
14
import { Button } from "@/components/ui/button";
15
15
import { useApi } from "@/lib/api/hook";
16
16
-
import SadWumpusPic from "@/public/sad-wumpus.gif";
17
16
import { type ApiV1UsersMeConnectionsGetResponse, ConnectionType } from "@/typings";
18
17
import { cn } from "@/utils/cn";
19
18
···
27
26
const { isLoading, data, error } = useApi<ApiV1UsersMeConnectionsGetResponse[]>(url);
28
27
29
28
if (error) {
30
30
-
return (
31
31
-
<ScreenMessage
32
32
-
title="Something went wrong on this page.."
33
33
-
description={error}
34
34
-
buttons={<>
35
35
-
<HomeButton />
36
36
-
<SupportButton />
37
37
-
</>}
38
38
-
>
39
39
-
<Image src={SadWumpusPic} alt="" height={141} width={124} />
40
40
-
</ScreenMessage>
41
41
-
);
29
29
+
return <ScreenMessage description={error} />;
42
30
}
43
31
44
32
if (isLoading || !data) return <></>;
+16
-24
app/profile/layout.tsx
···
1
1
"use client";
2
2
3
3
-
import { Chip } from "@nextui-org/react";
4
4
-
import Image from "next/image";
5
3
import Link from "next/link";
6
4
import { redirect } from "next/navigation";
7
5
import { useCookies } from "next-client-cookies";
···
14
12
import ImageReduceMotion from "@/components/image-reduce-motion";
15
13
import { ListTab } from "@/components/list";
16
14
import { MetricCard, Metrics } from "@/components/metric-card";
17
17
-
import { HomeButton, ScreenMessage, SupportButton } from "@/components/screen-message";
15
15
+
import { ScreenMessage } from "@/components/screen-message";
16
16
+
import { Badge } from "@/components/ui/badge";
18
17
import { Skeleton } from "@/components/ui/skeleton";
19
18
import { cacheOptions, getData } from "@/lib/api";
20
20
-
import SadWumpusPic from "@/public/sad-wumpus.gif";
21
19
import type { ApiV1UsersMeGetResponse } from "@/typings";
22
20
23
21
export default function RootLayout({
···
50
48
if (error || (data && "message" in data)) {
51
49
return (
52
50
<ScreenMessage
53
53
-
title="Something went wrong on this page.."
54
51
description={
55
52
(data && "message" in data ? data.message : `${error}`)
56
56
-
|| "An unknown error occurred."}
57
57
-
buttons={<>
58
58
-
<HomeButton />
59
59
-
<SupportButton />
60
60
-
</>}
61
61
-
>
62
62
-
<Image src={SadWumpusPic} alt="" height={141} width={124} />
63
63
-
</ScreenMessage>
53
53
+
|| "An unknown error occurred."
54
54
+
}
55
55
+
/>
64
56
);
65
57
}
66
58
···
87
79
{!user?.id ?
88
80
<div className="flex flex-col mt-2">
89
81
<Skeleton className="rounded-xl w-32 h-5 mb-2" />
90
90
-
<Skeleton className="rounded-md w-24 h-7" />
82
82
+
<Skeleton className="rounded-md w-[90px] h-7" />
91
83
</div>
92
84
:
93
85
<div className="flex flex-col gap-1">
94
86
<div className="text-2xl dark:text-neutral-200 text-neutral-800 font-medium">
95
87
{user.globalName || user.username}
96
88
</div>
97
97
-
<Chip
98
98
-
as={Link}
89
89
+
<Link
90
90
+
className="font-bold"
99
91
href="/vote"
100
92
target="_blank"
101
101
-
color="secondary"
102
102
-
startContent={<HiFire className="ml-1" />}
103
103
-
variant="flat"
104
104
-
radius="sm"
105
93
>
106
106
-
<span className="font-bold uppercase">
107
107
-
{user.extended?.voteCount} votes
108
108
-
</span>
109
109
-
</Chip>
94
94
+
<Badge
95
95
+
className="h-7 font-semibold"
96
96
+
variant="flat"
97
97
+
>
98
98
+
<HiFire/>
99
99
+
{user.extended?.voteCount} VOTE{user.extended?.voteCount === 1 ? "" : "S"}
100
100
+
</Badge>
101
101
+
</Link>
110
102
</div>
111
103
}
112
104
</div>
+51
-43
app/profile/page.tsx
···
1
1
"use client";
2
2
3
3
-
import { Button } from "@nextui-org/react";
4
3
import { motion } from "framer-motion";
5
5
-
import Image from "next/image";
6
4
import Link from "next/link";
7
5
import { useSearchParams } from "next/navigation";
8
6
import { useCookies } from "next-client-cookies";
···
11
9
12
10
import ImageReduceMotion from "@/components/image-reduce-motion";
13
11
import DumbTextInput from "@/components/inputs/dumb-text-input";
14
14
-
import { HomeButton, ScreenMessage, SupportButton } from "@/components/screen-message";
12
12
+
import { ScreenMessage } from "@/components/screen-message";
13
13
+
import { Button } from "@/components/ui/button";
15
14
import { useApi } from "@/lib/api/hook";
16
16
-
import SadWumpusPic from "@/public/sad-wumpus.gif";
17
15
import type { ApiV1UsersMeGuildsGetResponse } from "@/typings";
18
16
import { cn } from "@/utils/cn";
19
17
···
48
46
return (
49
47
<ScreenMessage
50
48
top="10rem"
51
51
-
title="Something went wrong on this page.."
52
49
description={`${error}`}
53
53
-
buttons={<>
54
54
-
<HomeButton />
55
55
-
<SupportButton />
56
56
-
</>}
57
57
-
>
58
58
-
<Image src={SadWumpusPic} alt="" height={141} width={124} />
59
59
-
</ScreenMessage>
50
50
+
/>
60
51
);
61
52
}
62
53
···
64
55
65
56
return (<div className="flex flex-col w-full">
66
57
67
67
-
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
58
58
+
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-2">
68
59
<div className="relative top-2 w-full">
69
60
<DumbTextInput
70
61
value={search}
···
76
67
77
68
<div className="flex gap-2 md:mt-0">
78
69
<Button
79
79
-
as={Link}
70
70
+
asChild
80
71
className="w-1/2 md:w-min"
81
81
-
href="/login?invite=true"
82
82
-
prefetch={false}
83
83
-
startContent={<HiUserAdd />}
84
72
>
85
85
-
Add to Server
73
73
+
<Link
74
74
+
href="/login?invite=true"
75
75
+
prefetch={false}
76
76
+
>
77
77
+
<HiUserAdd />
78
78
+
Add to Server
79
79
+
</Link>
86
80
</Button>
87
81
<Button
88
88
-
as={Link}
89
89
-
className="button-primary w-1/2 md:w-min"
90
90
-
href="/login"
91
91
-
prefetch={false}
92
92
-
startContent={<HiRefresh />}
82
82
+
asChild
83
83
+
className="w-1/2 md:w-min"
84
84
+
variant="secondary"
93
85
>
94
94
-
Reload
86
86
+
<Link
87
87
+
href="/login"
88
88
+
prefetch={false}
89
89
+
>
90
90
+
<HiRefresh />
91
91
+
Reload
92
92
+
</Link>
95
93
</Button>
96
94
</div>
97
95
</div>
···
121
119
<ScreenMessage
122
120
title="There are too many servers.."
123
121
description={`To save some performance, use the search to find a guild. Showing ${MAX_GUILDS} out of ~${guilds.length < 1000 ? length : Math.round(length / 1000) * 1000}.`}
124
124
-
>
125
125
-
<Image src={SadWumpusPic} alt="" height={141} width={124} />
126
126
-
</ScreenMessage>
122
122
+
/>
127
123
}
128
124
129
125
</div>);
···
191
187
function InviteButton({ guildId }: { guildId: string; }) {
192
188
return (
193
189
<Button
194
194
-
as={Link}
195
195
-
className="default dark:bg-neutral-500/40 hover:dark:bg-neutral-500/20 bg-neutral-400/40 hover:bg-neutral-400/20 text-sm h-9"
196
196
-
href={`/login?invite=true&guild_id=${guildId}`}
197
197
-
prefetch={false}
198
198
-
startContent={<HiUserAdd />}
190
190
+
asChild
191
191
+
className="h-8"
199
192
>
200
200
-
Add Wamellow
193
193
+
<Link
194
194
+
href={`/login?invite=true&guild_id=${guildId}`}
195
195
+
prefetch={false}
196
196
+
>
197
197
+
<HiUserAdd />
198
198
+
Add Wamellow
199
199
+
</Link>
201
200
</Button>
202
201
);
203
202
}
204
203
205
204
function ManageButton({ guildId }: { guildId: string; }) {
206
205
const searchParams = useSearchParams();
206
206
+
const to = searchParams.get("to");
207
207
208
208
return (
209
209
<Button
210
210
-
as={Link}
211
211
-
className="default dark:bg-neutral-500/40 hover:dark:bg-neutral-500/20 bg-neutral-400/40 hover:bg-neutral-400/20 text-sm h-9"
212
212
-
href={`/dashboard/${guildId}${searchParams.get("to") ? `/${searchParams.get("to")}` : ""}`}
213
213
-
startContent={<HiViewGridAdd />}
210
210
+
asChild
211
211
+
className="h-8"
214
212
>
215
215
-
Manage
213
213
+
<Link
214
214
+
href={`/dashboard/${guildId}${to ? `/${to}` : ""}`}
215
215
+
prefetch={false}
216
216
+
>
217
217
+
<HiViewGridAdd />
218
218
+
Manage
219
219
+
</Link>
216
220
</Button>
217
221
);
218
222
}
···
220
224
function LeaderboardButton({ guildId }: { guildId: string; }) {
221
225
return (
222
226
<Button
223
223
-
as={Link}
224
224
-
className="default dark:bg-neutral-500/40 hover:dark:bg-neutral-500/20 bg-neutral-400/40 hover:bg-neutral-400/20 text-sm h-9 md:opacity-0 group-hover/card:opacity-100"
225
225
-
href={`/leaderboard/${guildId}`}
226
226
-
startContent={<HiChartBar />}
227
227
+
asChild
228
228
+
className="h-8"
227
229
>
228
228
-
Leaderboard
230
230
+
<Link
231
231
+
href={`/leaderboard/${guildId}`}
232
232
+
prefetch={false}
233
233
+
>
234
234
+
<HiChartBar />
235
235
+
Leaderboard
236
236
+
</Link>
229
237
</Button>
230
238
);
231
239
}
+8
-8
app/profile/rank/card-style.component.tsx
···
1
1
-
import { Button } from "@nextui-org/react";
2
1
import type { ApiError } from "next/dist/server/api-utils";
3
2
import Image from "next/image";
4
3
import { type ChangeEvent, useRef, useState } from "react";
···
7
6
import { type User, userStore } from "@/common/user";
8
7
import Box from "@/components/box";
9
8
import { Shiggy } from "@/components/shiggy";
9
9
+
import { Button } from "@/components/ui/button";
10
10
import type { ApiV1UsersMeRankEmojiDeleteResponse, ApiV1UsersMeRankEmojiPutResponse } from "@/typings";
11
11
import { cn } from "@/utils/cn";
12
12
import { deepMerge } from "@/utils/deepMerge";
···
123
123
<div className="flex flex-col">
124
124
<Button
125
125
className={cn(state === State.Loading && "shake")}
126
126
-
color="secondary"
127
127
-
startContent={state !== State.Loading && <HiUpload />}
126
126
+
variant="secondary"
128
127
onClick={() => ref.current?.click()}
129
129
-
isLoading={state === State.Loading}
128
128
+
icon={<HiUpload />}
129
129
+
loading={state === State.Loading}
130
130
>
131
131
{state === State.Success
132
132
? "Looking good!"
133
133
: "Upload Emoji"
134
134
}
135
135
</Button>
136
136
-
{user?.extended?.rank?.emoji &&
136
136
+
{user?.extended?.rank?.emoji && (
137
137
<button
138
138
onClick={() => remove()}
139
139
className="text-red-400 hover:underline md:text-sm w-fit mt-1"
140
140
>
141
141
Remove
142
142
</button>
143
143
-
}
143
143
+
) }
144
144
</div>
145
145
</div>
146
146
···
157
157
</Box>
158
158
159
159
<div className="flex">
160
160
-
{error &&
160
160
+
{error && (
161
161
<div className="ml-auto text-red-500 text-sm">
162
162
{error}
163
163
</div>
164
164
-
}
164
164
+
)}
165
165
</div>
166
166
</div>
167
167
);
+11
-7
components/ad.tsx
···
1
1
"use client";
2
2
3
3
-
import { Button } from "@nextui-org/react";
4
3
import { Poppins } from "next/font/google";
5
4
import Link from "next/link";
6
5
import type { FunctionComponent } from "react";
7
6
import { HiArrowNarrowRight } from "react-icons/hi";
8
7
9
8
import { cn } from "@/utils/cn";
9
9
+
10
10
+
import { Button } from "./ui/button";
10
11
11
12
const poppins = Poppins({ subsets: ["latin"], weight: "700" });
12
13
···
48
49
</div>
49
50
50
51
<Button
51
51
-
as={Link}
52
52
+
asChild
52
53
className="mt-3 font-medium"
53
53
-
href={url}
54
54
-
target="_blank"
55
55
-
prefetch={false}
56
56
-
endContent={<HiArrowNarrowRight />}
57
54
>
58
58
-
{button}
55
55
+
<Link
56
56
+
href={url}
57
57
+
target="_blank"
58
58
+
prefetch={false}
59
59
+
>
60
60
+
{button}
61
61
+
<HiArrowNarrowRight />
62
62
+
</Link>
59
63
</Button>
60
64
</div>
61
65
);
+6
-9
components/embed-creator.tsx
···
1
1
-
import { Button } from "@nextui-org/react";
2
1
import React, { useState } from "react";
3
2
import { BiMoon, BiSun } from "react-icons/bi";
4
3
import { FaFloppyDisk } from "react-icons/fa6";
···
12
11
import DiscordMessageEmbed from "./discord/message-embed";
13
12
import DumbColorInput from "./inputs/dumb-color-input";
14
13
import DumbTextInput from "./inputs/dumb-text-input";
14
14
+
import { Button } from "./ui/button";
15
15
16
16
enum State {
17
17
Idle = 0,
···
194
194
</div>
195
195
196
196
<Button
197
197
-
className={cn(
198
198
-
"mt-1 w-full",
199
199
-
disabled && "cursor-not-allowed opacity-50"
200
200
-
)}
201
201
-
color="secondary"
202
202
-
isDisabled={disabled}
203
203
-
isLoading={state === State.Loading}
197
197
+
className="mt-1 w-full"
204
198
onClick={() => save()}
205
205
-
startContent={state !== State.Loading && <FaFloppyDisk />}
199
199
+
icon={<FaFloppyDisk />}
200
200
+
disabled={disabled}
201
201
+
loading={state === State.Loading}
202
202
+
variant="secondary"
206
203
>
207
204
Save Changes
208
205
</Button>
+6
-10
components/header.tsx
···
1
1
"use client";
2
2
3
3
-
import { Switch } from "@nextui-org/react";
4
3
import { AnimatePresence, motion, MotionConfig } from "framer-motion";
5
4
import Link from "next/link";
6
5
import { useRouter } from "next/navigation";
···
17
16
import ImageReduceMotion from "./image-reduce-motion";
18
17
import { Button } from "./ui/button";
19
18
import { Skeleton } from "./ui/skeleton";
19
19
+
import { Switch } from "./ui/switch";
20
20
21
21
enum State {
22
22
Idle = 0,
···
221
221
<Switch
222
222
key={"headerButton-" + button.name}
223
223
className="ml-auto"
224
224
-
isSelected={button.value}
225
225
-
onValueChange={button.onChange}
226
226
-
aria-label={button.name}
227
227
-
color="secondary"
228
228
-
size="sm"
224
224
+
checked={button.value}
225
225
+
onChange={button.onChange}
229
226
/>
230
227
</div>
231
228
);
···
240
237
}
241
238
242
239
<MotionConfig
243
243
-
transition={reduceMotions ?
244
244
-
{ duration: 0 }
245
245
-
:
246
246
-
{ type: "spring", bounce: 0.4, duration: menu ? 0.7 : 0.4 }
240
240
+
transition={reduceMotions
241
241
+
? { duration: 0 }
242
242
+
: { type: "spring", bounce: 0.4, duration: menu ? 0.7 : 0.4 }
247
243
}
248
244
>
249
245
<AnimatePresence initial={false}>
+9
components/loading-circle.tsx
···
1
1
+
import { LoaderCircleIcon } from "lucide-react";
2
2
+
3
3
+
import { cn } from "@/utils/cn";
4
4
+
5
5
+
export function LoadingCircle({ className = "" }: { className?: string; }) {
6
6
+
return (
7
7
+
<LoaderCircleIcon className={cn("animate-spin size-8", className)} strokeWidth={2.5} />
8
8
+
);
9
9
+
}
+63
-64
components/screen-message.tsx
···
1
1
-
import type { Button } from "@nextui-org/react";
1
1
+
import Image from "next/image";
2
2
import Link from "next/link";
3
3
import { BsDiscord } from "react-icons/bs";
4
4
import { HiHome } from "react-icons/hi";
5
5
6
6
+
import SadWumpusPic from "@/public/sad-wumpus.gif";
6
7
import { cn } from "@/utils/cn";
7
8
8
8
-
import { ClientButton } from "./client";
9
9
+
import { Button } from "./ui/button";
9
10
10
10
-
type Props = {
11
11
-
title: string;
12
12
-
description: string;
11
11
+
interface Props {
12
12
+
title?: string;
13
13
+
description?: string;
13
14
top?: string;
14
15
15
15
-
/**
16
16
-
* @deprecated
17
17
-
*/
18
16
icon?: React.ReactNode;
19
19
-
/**
20
20
-
* @deprecated
21
21
-
*/
22
17
button?: string;
18
18
+
href?: string;
23
19
24
20
buttons?: React.ReactNode;
25
21
children?: React.ReactNode;
26
26
-
} & React.ComponentProps<typeof Button>;
22
22
+
}
27
23
28
24
export function ScreenMessage({
29
25
title,
30
26
description,
31
31
-
top = "16vh",
27
27
+
top = "30vh",
32
28
33
29
icon,
34
30
button,
31
31
+
href,
35
32
36
36
-
buttons,
37
37
-
children,
38
38
-
...props
33
33
+
buttons = (<>
34
34
+
<HomeButton />
35
35
+
<SupportButton />
36
36
+
</>),
37
37
+
children = <Image src={SadWumpusPic} alt="" height={141 * 1.5} width={124 * 1.5} />
39
38
}: Props) {
40
39
41
40
return (
42
42
-
<div className="w-full h-full flex flex-col items-center justify-center md:my-0 my-40">
43
43
-
44
44
-
<div style={{ marginTop: top }} />
41
41
+
<div
42
42
+
className="w-full h-full flex justify-center gap-8"
43
43
+
style={{ marginTop: top }}
44
44
+
>
45
45
46
46
-
{children &&
46
46
+
{children && (
47
47
<div className={cn("relative bottom-8", buttons ? "ml-8" : "ml-4")}>
48
48
{children}
49
49
</div>
50
50
-
}
50
50
+
)}
51
51
52
52
<div>
53
53
-
54
54
-
<div className="mb-8 flex flex-col items-center text-center">
55
55
-
<span className="text-4xl dark:text-neutral-100 text-neutral-900 font-semibold">{title}</span> <br />
56
56
-
<span className="text-lg dark:text-neutral-400 text-neutral-600 font-semibold max-w-xl">{description}</span>
53
53
+
<div className="mb-8">
54
54
+
<h2 className="text-4xl dark:text-neutral-100 text-neutral-900 font-semibold">{title || "Something strange happened..."}</h2>
55
55
+
<h3 className="text-lg dark:text-neutral-400 text-neutral-600 font-semibold max-w-xl mt-1">{description || "Some error has occurred, but no worries, we're fixing it!"}</h3>
57
56
</div>
58
57
59
59
-
{(button && props.href) &&
58
58
+
{button && href &&
60
59
<div className="w-full flex flex-col items-center">
61
61
-
<ClientButton
62
62
-
as={Link}
63
63
-
{...props}
64
64
-
className={cn("px-20", props.className)}
65
65
-
startContent={icon}
60
60
+
<Button
61
61
+
asChild
62
62
+
variant="secondary"
66
63
>
67
67
-
{button}
68
68
-
</ClientButton>
64
64
+
<Link href={href}>
65
65
+
{icon}
66
66
+
{button}
67
67
+
</Link>
68
68
+
</Button>
69
69
</div>
70
70
}
71
71
72
72
-
{buttons &&
73
73
-
<div className="w-full flex flex-col items-center">
74
74
-
<div className="flex flex-wrap gap-2">
75
75
-
{buttons}
76
76
-
</div>
72
72
+
{buttons && (
73
73
+
<div className="flex flex-wrap gap-2">
74
74
+
{buttons}
77
75
</div>
78
78
-
}
79
79
-
76
76
+
)}
80
77
</div>
81
81
-
82
78
</div>
83
79
);
84
80
}
85
81
86
82
export function HomeButton() {
87
83
return (
88
88
-
<ClientButton
89
89
-
as={Link}
90
90
-
href="/"
91
91
-
startContent={<HiHome />}
84
84
+
<Button
85
85
+
asChild
86
86
+
variant="secondary"
92
87
>
93
93
-
Go back to Home
94
94
-
</ClientButton>
88
88
+
<Link href="/">
89
89
+
<HiHome />
90
90
+
Go back to Home
91
91
+
</Link>
92
92
+
</Button>
95
93
);
96
94
}
97
95
98
96
export function AddButton() {
99
97
return (
100
100
-
<ClientButton
101
101
-
as={Link}
102
102
-
className="button-primary"
103
103
-
href="/login?invite=true"
104
104
-
prefetch={false}
105
105
-
startContent={<BsDiscord />}
106
106
-
>
107
107
-
Add Wamellow to your server
108
108
-
</ClientButton>
98
98
+
<Button asChild>
99
99
+
<Link
100
100
+
href="/login?invite=true"
101
101
+
prefetch={false}
102
102
+
>
103
103
+
<BsDiscord />
104
104
+
Add Wamellow to your server
105
105
+
</Link>
106
106
+
</Button>
109
107
);
110
108
}
111
109
112
110
export function SupportButton() {
113
111
return (
114
114
-
<ClientButton
115
115
-
as={Link}
116
116
-
className="button-primary"
117
117
-
href="/support"
118
118
-
startContent={<BsDiscord />}
119
119
-
>
120
120
-
Join support server
121
121
-
</ClientButton>
112
112
+
<Button asChild>
113
113
+
<Link
114
114
+
href="/support"
115
115
+
prefetch={false}
116
116
+
>
117
117
+
<BsDiscord />
118
118
+
Join support server
119
119
+
</Link>
120
120
+
</Button>
122
121
);
123
122
}
+18
-20
components/section.tsx
···
1
1
-
import { Divider } from "@nextui-org/react";
2
2
-
3
1
import { cn } from "@/utils/cn";
2
2
+
3
3
+
import { Separator } from "./ui/separator";
4
4
5
5
export function Section({
6
6
title,
···
14
14
showDivider?: boolean;
15
15
children?: React.ReactNode;
16
16
} & React.HTMLAttributes<HTMLDivElement>) {
17
17
-
return (
18
18
-
<>
19
19
-
{showDivider && <Divider className="mt-12 mb-4" />}
17
17
+
return (<>
18
18
+
{showDivider && <Separator className="mt-12 mb-4" />}
20
19
21
21
-
<div
22
22
-
className={cn("mb-3", className)}
23
23
-
{...props}
24
24
-
>
25
25
-
<h3 className="text-xl text-neutral-200">{title}</h3>
26
26
-
{children &&
27
27
-
<p className="dark:text-neutral-500 text-neutral-400">
28
28
-
{children}
29
29
-
</p>
30
30
-
}
31
31
-
</div>
32
32
-
</>
33
33
-
);
20
20
+
<div
21
21
+
className={cn("mb-3", className)}
22
22
+
{...props}
23
23
+
>
24
24
+
<h3 className="text-xl text-neutral-200">{title}</h3>
25
25
+
{children && (
26
26
+
<p className="dark:text-neutral-500 text-neutral-400">
27
27
+
{children}
28
28
+
</p>
29
29
+
)}
30
30
+
</div>
31
31
+
</>);
34
32
}
35
33
36
34
export function SubSection({
···
47
45
<div {...props}>
48
46
<h3 className="text-medium font-medium text-neutral-300 mt-5">{title}</h3>
49
47
<div className="dark:text-neutral-500 text-neutral-400 mb-3">
50
50
-
{description &&
48
48
+
{description && (
51
49
<div className="mb-3">
52
50
{description}
53
51
</div>
54
54
-
}
52
52
+
)}
55
53
56
54
{children}
57
55
</div>
+1
-1
components/ui/badge.tsx
···
4
4
import { cn } from "@/utils/cn";
5
5
6
6
const badgeVariants = cva(
7
7
-
"w-fit inline-flex items-center rounded-full border font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 [&>svg]:relative [&>svg]:right-1",
7
7
+
"w-fit inline-flex items-center border font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 [&>svg]:relative [&>svg]:right-1",
8
8
{
9
9
variants: {
10
10
variant: {
+35
-2
components/ui/button.tsx
···
5
5
6
6
import { cn } from "@/utils/cn";
7
7
8
8
+
import { LoadingCircle } from "../loading-circle";
9
9
+
8
10
const buttonVariants = cva(
9
11
"inline-flex justify-center items-center gap-2 whitespace-nowrap rounded-lg text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 [&>svg]:size-5",
10
12
{
···
38
40
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
39
41
VariantProps<typeof buttonVariants> {
40
42
asChild?: boolean;
43
43
+
loading?: boolean;
44
44
+
icon?: React.ReactNode;
41
45
}
42
46
43
47
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
44
44
-
({ className, variant, size, asChild = false, ...props }, ref) => {
48
48
+
({ className, variant, size, asChild = false, children, disabled = false, loading = false, icon, ...props }, ref) => {
45
49
const Comp = asChild ? Slot : "button";
46
50
51
51
+
const content = (
52
52
+
<>
53
53
+
{loading
54
54
+
? <LoadingCircle />
55
55
+
: icon
56
56
+
}
57
57
+
{children}
58
58
+
</>
59
59
+
);
60
60
+
61
61
+
if (asChild) {
62
62
+
return (
63
63
+
<Comp
64
64
+
className={cn(buttonVariants({ variant, size, className }))}
65
65
+
data-disabled={disabled || loading}
66
66
+
ref={ref}
67
67
+
{...props}
68
68
+
>
69
69
+
{icon
70
70
+
? React.cloneElement(children as React.ReactElement, {}, content)
71
71
+
: children
72
72
+
}
73
73
+
</Comp>
74
74
+
);
75
75
+
}
76
76
+
47
77
return (
48
78
<Comp
49
79
className={cn(buttonVariants({ variant, size, className }))}
80
80
+
disabled={disabled || loading}
50
81
ref={ref}
51
82
{...props}
52
52
-
/>
83
83
+
>
84
84
+
{content}
85
85
+
</Comp>
53
86
);
54
87
}
55
88
);
+1
-1
components/ui/separator.tsx
···
18
18
decorative={decorative}
19
19
orientation={orientation}
20
20
className={cn(
21
21
-
"shrink-0 bg-border",
21
21
+
"shrink-0 bg-separator/15",
22
22
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
23
23
className
24
24
)}
+8
-2
components/ui/tooltip.tsx
···
7
7
8
8
const TooltipProvider = TooltipPrimitive.Provider;
9
9
10
10
-
const Tooltip = TooltipPrimitive.Root;
10
10
+
const Tooltip = ({ delayDuration, ...props }: React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Root>) => (
11
11
+
<TooltipPrimitive.Root
12
12
+
delayDuration={delayDuration ?? 0}
13
13
+
{...props}
14
14
+
/>
15
15
+
);
16
16
+
Tooltip.displayName = TooltipPrimitive.Root.displayName;
11
17
12
18
const TooltipTrigger = TooltipPrimitive.Trigger;
13
19
···
19
25
ref={ref}
20
26
sideOffset={sideOffset}
21
27
className={cn(
22
22
-
"z-50 overflow-hidden rounded-md bg-popover/30 px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
28
28
+
"z-50 overflow-hidden rounded-md bg-popover/30 backdrop-blur-md px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 max-w-md data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
23
29
className
24
30
)}
25
31
{...props}
+1
-1
lib/api/hook.ts
···
33
33
}
34
34
35
35
return {
36
36
-
data,
36
36
+
data: data as T,
37
37
isLoading,
38
38
error: error ? `${error}` : undefined,
39
39
edit
+1
tailwind.config.js
···
62
62
border: 'hsl(var(--border))',
63
63
input: 'hsl(var(--input))',
64
64
ring: 'hsl(var(--ring))',
65
65
+
separator: 'hsl(var(--separator))',
65
66
chart: {
66
67
'1': 'hsl(var(--chart-1))',
67
68
'2': 'hsl(var(--chart-2))',