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
migrate /dashboard -> /profile & update queries
shi.gg
2 years ago
dd4a83ef
ed66c81e
+420
-481
12 changed files
expand all
collapse all
unified
split
app
dashboard
[guildId]
custom-commands
page.tsx
layout.tsx
leaderboards
page.tsx
nsfw-image-scanning
page.tsx
starboard
page.tsx
page.tsx
profile
analytics
page.tsx
layout.tsx
page.tsx
spotify
page.tsx
components
list.tsx
lib
api.ts
+31
-23
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";
4
5
import { useParams, usePathname, useRouter, useSearchParams } from "next/navigation";
5
6
import { useCallback, useEffect } from "react";
6
7
import { HiViewGridAdd } from "react-icons/hi";
···
14
15
import { ScreenMessage } from "@/components/screen-message";
15
16
import { cacheOptions, getData } from "@/lib/api";
16
17
import { Permissions } from "@/lib/discord";
18
18
+
import SadWumpusPic from "@/public/sad-wumpus.gif";
17
19
import { ApiV1GuildsModulesTagsGetResponse } from "@/typings";
18
20
19
21
import CreateTag, { Style } from "./create.component";
···
28
30
const queryClient = useQueryClient();
29
31
30
32
const url = `/guilds/${params.guildId}/modules/tags` as const;
31
31
-
const key = ["guilds", params.guildId, "modules", "custom-commands"] as const;
32
33
33
34
const { data, isLoading, error } = useQuery(
34
34
-
key,
35
35
+
url,
35
36
() => getData<ApiV1GuildsModulesTagsGetResponse[]>(url),
36
37
{
37
38
enabled: !!params.guildId,
···
40
41
);
41
42
42
43
const tagId = search.get("id");
43
43
-
const tag = data?.find((t) => t.tagId === tagId);
44
44
+
const tag = (Array.isArray(data) ? data : []).find((t) => t.tagId === tagId);
44
45
45
46
const createQueryString = useCallback((name: string, value: string) => {
46
47
const params = new URLSearchParams(search);
···
49
50
return params.toString();
50
51
}, [search]);
51
52
53
53
+
useEffect(() => {
54
54
+
if (!Array.isArray(data)) return;
55
55
+
if (data && !tag && data[0]) setTagId(data[0].tagId);
56
56
+
}, [data]);
57
57
+
58
58
+
if (error || (data && "message" in data)) {
59
59
+
return (
60
60
+
<ScreenMessage
61
61
+
top="0rem"
62
62
+
title="Something went wrong on this page.."
63
63
+
description={
64
64
+
(data && "message" in data ? data.message : `${error}`)
65
65
+
|| "An unknown error occurred."}
66
66
+
href={`/dashboard/${guild?.id}`}
67
67
+
button="Go back to overview"
68
68
+
icon={<HiViewGridAdd />}
69
69
+
>
70
70
+
<Image src={SadWumpusPic} alt="" height={141} width={124} />
71
71
+
</ScreenMessage>
72
72
+
);
73
73
+
}
74
74
+
75
75
+
if (isLoading || !data) return <></>;
76
76
+
52
77
const setTagId = (id: string) => {
53
78
router.push(pathname + "?" + createQueryString("id", id));
54
79
};
···
56
81
const editTag = <T extends keyof ApiV1GuildsModulesTagsGetResponse>(k: keyof ApiV1GuildsModulesTagsGetResponse, value: ApiV1GuildsModulesTagsGetResponse[T]) => {
57
82
if (!tag) return;
58
83
59
59
-
queryClient.setQueryData<ApiV1GuildsModulesTagsGetResponse[]>(key, () => [
84
84
+
queryClient.setQueryData<ApiV1GuildsModulesTagsGetResponse[]>(url, () => [
60
85
...(data?.filter((t) => t.tagId !== tag.tagId) || []),
61
86
{ ...tag, [k]: value }
62
87
]);
63
88
};
64
89
65
90
const addTag = (tag: ApiV1GuildsModulesTagsGetResponse) => {
66
66
-
queryClient.setQueryData<ApiV1GuildsModulesTagsGetResponse[]>(key, () => [
91
91
+
queryClient.setQueryData<ApiV1GuildsModulesTagsGetResponse[]>(url, () => [
67
92
...(data || []),
68
93
tag
69
94
]);
70
95
};
71
96
72
97
const removeTag = (id: string) => {
73
73
-
queryClient.setQueryData<ApiV1GuildsModulesTagsGetResponse[]>(key, () =>
98
98
+
queryClient.setQueryData<ApiV1GuildsModulesTagsGetResponse[]>(url, () =>
74
99
data?.filter((t) => t.tagId !== id) || []
75
100
);
76
101
};
77
77
-
78
78
-
useEffect(() => {
79
79
-
if (data && !tag && data[0]) setTagId(data[0].tagId);
80
80
-
}, [data?.length]);
81
81
-
82
82
-
if (!data || isLoading) return <></>;
83
83
-
if (error) {
84
84
-
return (
85
85
-
<ScreenMessage
86
86
-
title="Something went wrong.."
87
87
-
description={error.toString() || "We couldn't load the data for this page."}
88
88
-
href={`/dashboard/${guild?.id}`}
89
89
-
button="Go back to overview"
90
90
-
icon={<HiViewGridAdd />}
91
91
-
/>
92
92
-
);
93
93
-
}
94
102
95
103
return (
96
104
<>
+2
-2
app/dashboard/[guildId]/layout.tsx
···
167
167
<Button
168
168
as={Link}
169
169
className="w-fit"
170
170
-
href="/dashboard"
170
170
+
href="/profile"
171
171
startContent={<HiArrowNarrowLeft />}
172
172
>
173
173
Serverlist
···
259
259
buttons={<>
260
260
<ServerButton
261
261
as={Link}
262
262
-
href="/dashboard"
262
262
+
href="/profile"
263
263
startContent={<HiViewGridAdd />}
264
264
>
265
265
Go back to Dashboard
+21
-13
app/dashboard/[guildId]/leaderboards/page.tsx
···
1
1
"use client";
2
2
+
import Image from "next/image";
2
3
import { useParams } from "next/navigation";
3
3
-
import { useState } from "react";
4
4
+
import { useCookies } from "next-client-cookies";
4
5
import { HiChartBar, HiViewGridAdd } from "react-icons/hi";
5
6
import { useQuery } from "react-query";
6
7
7
8
import { Guild, guildStore } from "@/common/guilds";
8
8
-
import { webStore } from "@/common/webstore";
9
9
import Betweener from "@/components/Betweener";
10
10
import ImageUrlInput from "@/components/inputs/ImageUrlInput";
11
11
import MultiSelectMenu from "@/components/inputs/MultiSelectMenu";
12
12
import TextInput from "@/components/inputs/TextInput";
13
13
import { ScreenMessage } from "@/components/screen-message";
14
14
-
import { getData } from "@/lib/api";
14
14
+
import { cacheOptions, getData } from "@/lib/api";
15
15
+
import SadWumpusPic from "@/public/sad-wumpus.gif";
15
16
import { ApiV1GuildsModulesLeaderboardGetResponse } from "@/typings";
16
17
17
18
import OverviewLinkComponent from "../../../../components/OverviewLinkComponent";
18
19
import UpdatingLeaderboardCard from "./updating.component";
19
20
20
21
export default function Home() {
22
22
+
const cookies = useCookies();
23
23
+
21
24
const guild = guildStore((g) => g);
22
25
const web = webStore((w) => w);
23
26
const params = useParams();
24
27
25
28
const url = `/guilds/${params.guildId}/modules/leaderboard` as const;
26
29
27
27
-
const [data, setData] = useState<ApiV1GuildsModulesLeaderboardGetResponse | null>(null);
28
28
-
29
29
-
const { isLoading, error } = useQuery(
30
30
-
["guilds", params.guildId, "modules", "leaderboard"],
30
30
+
const { data, isLoading, error } = useQuery(
31
31
+
url,
31
32
() => getData<ApiV1GuildsModulesLeaderboardGetResponse>(url),
32
33
{
33
34
enabled: !!params.guildId,
34
34
-
onSuccess: (d) => setData(d)
35
35
+
...cacheOptions,
36
36
+
refetchOnMount: true
35
37
}
36
38
);
37
39
38
38
-
if (!data || isLoading) return <></>;
39
39
-
if (error) {
40
40
+
if (error || (data && "message" in data)) {
40
41
return (
41
42
<ScreenMessage
42
42
-
title="Something went wrong.."
43
43
-
description={error.toString() || "We couldn't load the data for this page."}
43
43
+
top="0rem"
44
44
+
title="Something went wrong on this page.."
45
45
+
description={
46
46
+
(data && "message" in data ? data.message : `${error}`)
47
47
+
|| "An unknown error occurred."}
44
48
href={`/dashboard/${guild?.id}`}
45
49
button="Go back to overview"
46
50
icon={<HiViewGridAdd />}
47
47
-
/>
51
51
+
>
52
52
+
<Image src={SadWumpusPic} alt="" height={141} width={124} />
53
53
+
</ScreenMessage>
48
54
);
49
55
}
56
56
+
57
57
+
if (isLoading || !data) return <></>;
50
58
51
59
return (
52
60
<div>
+20
-10
app/dashboard/[guildId]/nsfw-image-scanning/page.tsx
···
1
1
"use client";
2
2
3
3
import { Code } from "@nextui-org/react";
4
4
+
import Image from "next/image";
4
5
import { useParams } from "next/navigation";
5
6
import { useState } from "react";
6
7
import { HiViewGridAdd } from "react-icons/hi";
···
12
13
import Switch from "@/components/inputs/Switch";
13
14
import Notice, { NoticeType } from "@/components/notice";
14
15
import { ScreenMessage } from "@/components/screen-message";
15
15
-
import { getData } from "@/lib/api";
16
16
-
import { ApiV1GuildsModulesNsfwModerationGetResponse } from "@/typings";
16
16
+
import { cacheOptions, getData } from "@/lib/api";
17
17
+
import SadWumpusPic from "@/public/sad-wumpus.gif";
18
18
+
import { ApiV1GuildsModulesNsfwModerationGetResponse, RouteErrorResponse } from "@/typings";
17
19
18
20
export default function Home() {
19
21
const guild = guildStore((g) => g);
···
21
23
22
24
const url = `/guilds/${params.guildId}/modules/nsfw-image-scanning` as const;
23
25
24
24
-
const [data, setData] = useState<ApiV1GuildsModulesNsfwModerationGetResponse | null>(null);
26
26
+
const [data, setData] = useState<ApiV1GuildsModulesNsfwModerationGetResponse | RouteErrorResponse>();
25
27
26
28
const { isLoading, error } = useQuery(
27
27
-
["guilds", params.guildId, "modules", "nsfw-image-scanning"],
29
29
+
url,
28
30
() => getData<ApiV1GuildsModulesNsfwModerationGetResponse>(url),
29
31
{
30
32
enabled: !!params.guildId,
31
31
-
onSuccess: (d) => setData(d)
33
33
+
onSuccess: (d) => setData(d),
34
34
+
...cacheOptions,
35
35
+
refetchOnMount: true
32
36
}
33
37
);
34
38
···
38
42
setData(updatedLocalData);
39
43
};
40
44
41
41
-
if (!data || isLoading) return <></>;
42
42
-
if (error) {
45
45
+
if (error || (data && "message" in data)) {
43
46
return (
44
47
<ScreenMessage
45
45
-
title="Something went wrong.."
46
46
-
description={error.toString() || "We couldn't load the data for this page."}
48
48
+
top="0rem"
49
49
+
title="Something went wrong on this page.."
50
50
+
description={
51
51
+
(data && "message" in data ? data.message : `${error}`)
52
52
+
|| "An unknown error occurred."}
47
53
href={`/dashboard/${guild?.id}`}
48
54
button="Go back to overview"
49
55
icon={<HiViewGridAdd />}
50
50
-
/>
56
56
+
>
57
57
+
<Image src={SadWumpusPic} alt="" height={141} width={124} />
58
58
+
</ScreenMessage>
51
59
);
52
60
}
61
61
+
62
62
+
if (isLoading || !data) return <></>;
53
63
54
64
return (
55
65
<>
+15
-8
app/dashboard/[guildId]/starboard/page.tsx
···
15
15
import Switch from "@/components/inputs/Switch";
16
16
import { ScreenMessage } from "@/components/screen-message";
17
17
import { getData } from "@/lib/api";
18
18
-
import { ApiV1GuildsModulesStarboardGetResponse } from "@/typings";
18
18
+
import SadWumpusPic from "@/public/sad-wumpus.gif";
19
19
+
import { ApiV1GuildsModulesStarboardGetResponse, RouteErrorResponse } from "@/typings";
19
20
20
21
export default function Home() {
21
22
const guild = guildStore((g) => g);
···
23
24
24
25
const url = `/guilds/${params.guildId}/modules/starboard` as const;
25
26
26
26
-
const [data, setData] = useState<ApiV1GuildsModulesStarboardGetResponse | null>(null);
27
27
+
const [data, setData] = useState<ApiV1GuildsModulesStarboardGetResponse | RouteErrorResponse>();
27
28
28
29
const { isLoading, error } = useQuery(
29
29
-
["guilds", params.guildId, "modules", "starboard"],
30
30
+
url,
30
31
() => getData<ApiV1GuildsModulesStarboardGetResponse>(url),
31
32
{
32
33
enabled: !!params.guildId,
···
88
89
}
89
90
};
90
91
91
91
-
if (!data || isLoading) return <></>;
92
92
-
if (error) {
92
92
+
if (error || (data && "message" in data)) {
93
93
return (
94
94
<ScreenMessage
95
95
-
title="Something went wrong.."
96
96
-
description={error.toString() || "We couldn't load the data for this page."}
95
95
+
top="0rem"
96
96
+
title="Something went wrong on this page.."
97
97
+
description={
98
98
+
(data && "message" in data ? data.message : `${error}`)
99
99
+
|| "An unknown error occurred."}
97
100
href={`/dashboard/${guild?.id}`}
98
101
button="Go back to overview"
99
102
icon={<HiViewGridAdd />}
100
100
-
/>
103
103
+
>
104
104
+
<Image src={SadWumpusPic} alt="" height={141} width={124} />
105
105
+
</ScreenMessage>
101
106
);
102
107
}
108
108
+
109
109
+
if (isLoading || !data) return <></>;
103
110
104
111
return (
105
112
<>
+18
-283
app/dashboard/page.tsx
···
1
1
-
"use client";
2
2
-
3
3
-
import { Button } from "@nextui-org/react";
4
4
-
import { motion } from "framer-motion";
5
5
-
import Image from "next/image";
6
6
-
import Link from "next/link";
7
7
-
import { useSearchParams } from "next/navigation";
8
8
-
import { useCookies } from "next-client-cookies";
9
9
-
import { useEffect, useState } from "react";
10
10
-
import { HiRefresh, HiUserAdd, HiViewGrid, HiViewList } from "react-icons/hi";
11
11
-
12
12
-
import { userStore } from "@/common/user";
13
13
-
import { webStore } from "@/common/webstore";
14
14
-
import ImageReduceMotion from "@/components/image-reduce-motion";
15
15
-
import DumbTextInput from "@/components/inputs/Dumb_TextInput";
16
16
-
import Notice, { NoticeType } from "@/components/notice";
17
17
-
import { ScreenMessage } from "@/components/screen-message";
18
18
-
import SadWumpusPic from "@/public/sad-wumpus.gif";
19
19
-
import { RouteErrorResponse, UserGuild } from "@/typings";
20
20
-
import cn from "@/utils/cn";
21
21
-
22
22
-
const MAX_GUILDS = 24;
23
23
-
24
24
-
export default function Home() {
25
25
-
const cookies = useCookies();
26
26
-
if (cookies.get("hasSession") !== "true") window.location.href = "/login";
27
27
-
28
28
-
const web = webStore((w) => w);
29
29
-
const user = userStore((s) => s);
30
30
-
31
31
-
const [error, setError] = useState<string>();
32
32
-
const [guilds, setGuilds] = useState<UserGuild[] | undefined>();
33
33
-
const [search, setSearch] = useState<string>("");
34
34
-
const [display, setDisplay] = useState<"LIST" | "GRID">("GRID");
35
35
-
36
36
-
function filter(guild: UserGuild) {
37
37
-
if (!search) return true;
38
38
-
39
39
-
if (guild.name.toLowerCase().includes(search.toLowerCase())) return true;
40
40
-
if (search.toLowerCase().includes(guild.name.toLowerCase())) return true;
1
1
+
import { redirect } from "next/navigation";
41
2
42
42
-
if (guild.id.includes(search)) return true;
43
43
-
if (search.includes(guild.id)) return true;
3
3
+
export default function Home({
4
4
+
searchParams
5
5
+
}: {
6
6
+
searchParams: Record<string, string>;
7
7
+
}) {
8
8
+
redirect(`/profile?${objectToSearchParams(searchParams)}`);
9
9
+
}
44
10
45
45
-
return false;
46
46
-
}
47
47
-
48
48
-
const { length } = (guilds || []).filter(filter);
49
49
-
50
50
-
useEffect(() => {
11
11
+
function objectToSearchParams(obj: Record<string, string>): string {
12
12
+
if (!Object.keys(obj).length) return "";
51
13
52
52
-
setDisplay((localStorage.getItem("dashboardServerSelectStyle") || "GRID") as "LIST" | "GRID");
14
14
+
const params = new URLSearchParams();
53
15
54
54
-
fetch(`${process.env.NEXT_PUBLIC_API}/guilds/@me`, {
55
55
-
credentials: "include"
56
56
-
})
57
57
-
.then(async (res) => {
58
58
-
const response = await res.json() as UserGuild[];
59
59
-
if (!response) return;
16
16
+
Object.keys(obj).forEach((key) => {
17
17
+
const value = obj[key];
18
18
+
if (value !== null && value !== undefined) {
19
19
+
params.append(key, value.toString());
20
20
+
}
21
21
+
});
60
22
61
61
-
switch (res.status) {
62
62
-
case 200: {
63
63
-
setGuilds(response);
64
64
-
break;
65
65
-
}
66
66
-
default: {
67
67
-
setError((response as unknown as RouteErrorResponse).message || "Error while fetching guilds");
68
68
-
break;
69
69
-
}
70
70
-
}
71
71
-
72
72
-
})
73
73
-
.catch(() => {
74
74
-
setError("Error while fetching guilds");
75
75
-
});
76
76
-
77
77
-
}, []);
78
78
-
79
79
-
useEffect(() => {
80
80
-
localStorage.setItem("dashboardServerSelectStyle", display);
81
81
-
}, [display]);
82
82
-
83
83
-
return (
84
84
-
<div className="flex flex-col w-full">
85
85
-
<title>Dashboard</title>
86
86
-
87
87
-
{error &&
88
88
-
<Notice
89
89
-
type={NoticeType.Error}
90
90
-
message={error}
91
91
-
/>
92
92
-
}
93
93
-
94
94
-
<div className="md:flex md:items-center">
95
95
-
<div>
96
96
-
<div className="text-2xl dark:text-neutral-100 text-neutral-900 font-semibold mb-2">👋 Heyia, {user?.globalName || `@${user?.username}`}</div>
97
97
-
<div className="text-lg font-medium">Select a server you want to manage.</div>
98
98
-
</div>
99
99
-
100
100
-
<div className="md:hidden mt-3">
101
101
-
<DumbTextInput
102
102
-
value={search}
103
103
-
setValue={setSearch}
104
104
-
placeholder="Search"
105
105
-
thin
106
106
-
/>
107
107
-
</div>
108
108
-
109
109
-
<div className="md:ml-auto flex gap-3 mt-4 md:mt-0">
110
110
-
<div className="hidden md:block">
111
111
-
<DumbTextInput
112
112
-
value={search}
113
113
-
setValue={setSearch}
114
114
-
placeholder="Search"
115
115
-
thin
116
116
-
/>
117
117
-
</div>
118
118
-
<Button
119
119
-
as={Link}
120
120
-
className="w-1/2 md:w-min"
121
121
-
href="/login?invite=true"
122
122
-
prefetch={false}
123
123
-
startContent={<HiUserAdd />}
124
124
-
>
125
125
-
Add to Server
126
126
-
</Button>
127
127
-
<Button
128
128
-
as={Link}
129
129
-
className="button-primary w-1/2 md:w-min"
130
130
-
href="/login"
131
131
-
prefetch={false}
132
132
-
startContent={<HiRefresh />}
133
133
-
>
134
134
-
Reload
135
135
-
</Button>
136
136
-
</div>
137
137
-
</div>
138
138
-
139
139
-
<div className="flex gap-3">
140
140
-
<hr className="mx-0 p-1 my-4 dark:border-wamellow-light border-wamellow-100-light w-full" />
141
141
-
142
142
-
<div className="dark:bg-wamellow bg-wamellow-100 md:flex gap-1 dark:text-neutral-400 text-neutral-600 rounded-md overflow-hidden w-[72px] mb-5 hidden">
143
143
-
<button
144
144
-
onClick={() => setDisplay("GRID")}
145
145
-
className={cn("h-7 w-8 flex items-center justify-center p-[4px] rounded-md", display === "GRID" && "dark:bg-wamellow bg-wamellow-100")}
146
146
-
>
147
147
-
<HiViewGrid />
148
148
-
</button>
149
149
-
<button
150
150
-
onClick={() => setDisplay("LIST")}
151
151
-
className={cn("h-7 w-8 flex items-center justify-center p-[4px] rounded-md", display === "LIST" && "dark:bg-wamellow bg-wamellow-100")}
152
152
-
>
153
153
-
<HiViewList />
154
154
-
</button>
155
155
-
</div>
156
156
-
</div>
157
157
-
158
158
-
{Array.isArray(guilds) ?
159
159
-
<motion.ul
160
160
-
variants={{
161
161
-
hidden: { opacity: 1, scale: 0 },
162
162
-
visible: {
163
163
-
opacity: 1,
164
164
-
scale: 1,
165
165
-
transition: {
166
166
-
delayChildren: guilds.length > 20 ? 0.2 : 0.3,
167
167
-
staggerChildren: guilds.length > 20 ? 0.1 : 0.2
168
168
-
}
169
169
-
}
170
170
-
}}
171
171
-
initial={web.reduceMotions ? "visible" : "hidden"}
172
172
-
animate="visible"
173
173
-
className={cn("grid grid-cols-1 gap-4 w-full", display === "GRID" && "lg:grid-cols-3 md:grid-cols-2")}
174
174
-
>
175
175
-
176
176
-
{guilds
177
177
-
.filter(filter)
178
178
-
.slice(0, MAX_GUILDS)
179
179
-
.map((guild) => (
180
180
-
<motion.li
181
181
-
key={"guildGrid" + guild.id}
182
182
-
variants={{
183
183
-
hidden: { y: 20, opacity: 0 },
184
184
-
visible: {
185
185
-
y: 0,
186
186
-
opacity: 1,
187
187
-
transition: {
188
188
-
type: "spring",
189
189
-
bounce: guilds.length > 20 ? 0.2 : 0.4,
190
190
-
duration: guilds.length > 20 ? 0.35 : 0.7
191
191
-
}
192
192
-
}
193
193
-
}}
194
194
-
className="dark:bg-wamellow bg-wamellow-100 p-4 flex items-center rounded-lg drop-shadow-md overflow-hidden relative h-24 duration-100 outline-violet-500 hover:outline group/card"
195
195
-
>
196
196
-
<ImageReduceMotion
197
197
-
alt=""
198
198
-
className="absolute top-[-48px] left-0 w-full z-0 blur-xl opacity-30 pointer-events-none"
199
199
-
size={24}
200
200
-
url={`https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}`}
201
201
-
forceStatic={true}
202
202
-
/>
203
203
-
204
204
-
<ImageReduceMotion
205
205
-
alt={`Server icon of @${guild.name}`}
206
206
-
className="rounded-lg h-14 w-14 z-1 relative drop-shadow-md"
207
207
-
size={56}
208
208
-
url={`https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}`}
209
209
-
/>
210
210
-
211
211
-
<div className="ml-3 text-sm relative bottom-1">
212
212
-
<span className="text-lg dark:text-neutral-200 font-medium text-neutral-800 mb-1 sm:max-w-64 lg:max-w-56 truncate">
213
213
-
{guild.name}
214
214
-
</span>
215
215
-
<div className="flex gap-2">
216
216
-
<ManageButton guildId={guild.id} />
217
217
-
<LeaderboardButton guildId={guild.id} />
218
218
-
</div>
219
219
-
</div>
220
220
-
221
221
-
</motion.li>
222
222
-
))}
223
223
-
224
224
-
<motion.a
225
225
-
href="/login?invite=true"
226
226
-
target="_blank"
227
227
-
key={"guildGrid" + guilds.length}
228
228
-
variants={{
229
229
-
hidden: { y: 20, opacity: 0 },
230
230
-
visible: {
231
231
-
y: 0,
232
232
-
opacity: 1,
233
233
-
transition: {
234
234
-
type: "spring",
235
235
-
bounce: 0.4,
236
236
-
duration: 0.7
237
237
-
}
238
238
-
}
239
239
-
}}
240
240
-
className="border-2 dark:border-wamellow border-wamellow-100 p-4 flex justify-center items-center rounded-lg drop-shadow-md overflow-hidden relative h-24 duration-100 outline-violet-500 hover:outline"
241
241
-
>
242
242
-
Click to add a new server
243
243
-
</motion.a>
244
244
-
245
245
-
</motion.ul>
246
246
-
:
247
247
-
<div className={cn("border-2 dark:border-wamellow border-wamellow-100 p-4 flex justify-center items-center rounded-lg drop-shadow-md overflow-hidden relative h-24", display === "GRID" && "md:w-1/2 lg:w-1/3")}>
248
248
-
Loading your servers...
249
249
-
</div>
250
250
-
}
251
251
-
252
252
-
{length > MAX_GUILDS &&
253
253
-
<ScreenMessage
254
254
-
title="There are too many servers.."
255
255
-
description={`To save some performance, use the search to find a guild. Showing ${MAX_GUILDS} out of ~${length < 100 ? length : Math.round(length / 100) * 100}.`}
256
256
-
>
257
257
-
<Image src={SadWumpusPic} alt="" height={141} width={124} />
258
258
-
</ScreenMessage>
259
259
-
}
260
260
-
261
261
-
</div>
262
262
-
);
263
263
-
}
264
264
-
265
265
-
function ManageButton({ guildId }: { guildId: string }) {
266
266
-
const searchParams = useSearchParams();
267
267
-
268
268
-
return (
269
269
-
<Button
270
270
-
as={Link}
271
271
-
href={`/dashboard/${guildId}${searchParams.get("to") ? `/${searchParams.get("to")}` : ""}`}
272
272
-
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"
273
273
-
>
274
274
-
Manage
275
275
-
</Button>
276
276
-
);
277
277
-
}
278
278
-
279
279
-
function LeaderboardButton({ guildId }: { guildId: string }) {
280
280
-
return (
281
281
-
<Button
282
282
-
as={Link}
283
283
-
href={`/leaderboard/${guildId}`}
284
284
-
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"
285
285
-
>
286
286
-
Leaderboard
287
287
-
</Button>
288
288
-
);
23
23
+
return params.toString();
289
24
}
+32
-41
app/profile/analytics/page.tsx
···
1
1
"use client";
2
2
3
3
-
import { useEffect, useState } from "react";
4
4
-
import { HiIdentification } from "react-icons/hi";
3
3
+
import Image from "next/image";
4
4
+
import { useQuery } from "react-query";
5
5
import { Area, AreaChart, Bar, BarChart, CartesianGrid, Cell, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
6
6
7
7
import Box from "@/components/box";
8
8
import { StatsBar } from "@/components/counter";
9
9
-
import { ScreenMessage } from "@/components/screen-message";
10
10
-
import { NekosticResponse, RouteErrorResponse } from "@/typings";
9
9
+
import { HomeButton, ScreenMessage, SupportButton } from "@/components/screen-message";
10
10
+
import { cacheOptions, getData } from "@/lib/api";
11
11
+
import SadWumpusPic from "@/public/sad-wumpus.gif";
12
12
+
import { NekosticResponse } from "@/typings";
11
13
import { convertMonthToName } from "@/utils/time";
12
14
13
15
interface CalcUses {
···
22
24
}
23
25
24
26
export default function Home() {
25
25
-
const [error, setError] = useState<string>();
26
26
-
const [data, setData] = useState<NekosticResponse[]>();
27
27
+
const url = "" as const;
27
28
28
28
-
useEffect(() => {
29
29
+
const { isLoading, data, error } = useQuery(
30
30
+
["nekostic", "statistics"],
31
31
+
() => getData<NekosticResponse[]>(url, process.env.NEXT_PUBLIC_NEKOSTIC as string),
32
32
+
cacheOptions
33
33
+
);
29
34
30
30
-
fetch(process.env.NEXT_PUBLIC_NEKOSTIC as string)
31
31
-
.then(async (res) => {
32
32
-
const response = await res.json() as NekosticResponse[];
33
33
-
if (!response) return;
34
34
-
35
35
-
switch (res.status) {
36
36
-
case 200: {
37
37
-
setData(response.sort((a, b) => new Date(a.snapshot).getTime() - new Date(b.snapshot).getTime()));
38
38
-
break;
39
39
-
}
40
40
-
default: {
41
41
-
setData([]);
42
42
-
setError((response as unknown as RouteErrorResponse).message);
43
43
-
break;
44
44
-
}
45
45
-
}
46
46
-
47
47
-
})
48
48
-
.catch(() => {
49
49
-
setError("Error while fetching analytics data");
50
50
-
});
51
51
-
52
52
-
}, []);
53
53
-
54
54
-
if (error) {
55
55
-
return <>
35
35
+
if (error || (data && "message" in data)) {
36
36
+
return (
56
37
<ScreenMessage
57
57
-
title="Something went wrong.."
58
58
-
description={error}
59
59
-
href="/profile"
60
60
-
button="Go back to overview"
61
61
-
icon={<HiIdentification />}
62
62
-
/>
63
63
-
</>;
38
38
+
top="0rem"
39
39
+
title="Something went wrong on this page.."
40
40
+
description={
41
41
+
(data && "message" in data ? data.message : `${error}`)
42
42
+
|| "An unknown error occurred."}
43
43
+
buttons={<>
44
44
+
<HomeButton />
45
45
+
<SupportButton />
46
46
+
</>}
47
47
+
>
48
48
+
<Image src={SadWumpusPic} alt="" height={141} width={124} />
49
49
+
</ScreenMessage>
50
50
+
);
64
51
}
65
52
66
66
-
if (!data?.length) return <></>;
53
53
+
if (isLoading || !data) return <></>;
54
54
+
55
55
+
data.sort((a, b) =>
56
56
+
new Date(a.snapshot).getTime() - new Date(b.snapshot).getTime()
57
57
+
);
67
58
68
59
const now = new Date();
69
60
const yesterday = new Date();
+13
-7
app/profile/layout.tsx
···
13
13
import ImageReduceMotion from "@/components/image-reduce-motion";
14
14
import { ListTab } from "@/components/list";
15
15
import { HomeButton, ScreenMessage, SupportButton } from "@/components/screen-message";
16
16
-
import { getData } from "@/lib/api";
16
16
+
import { cacheOptions, getData } from "@/lib/api";
17
17
import SadWumpusPic from "@/public/sad-wumpus.gif";
18
18
import { ApiV1MeGetResponse } from "@/typings";
19
19
import decimalToRgb from "@/utils/decimalToRgb";
···
26
26
const cookies = useCookies();
27
27
if (cookies.get("hasSession") !== "true") window.location.href = "/login";
28
28
29
29
-
const user = userStore((g) => g);
29
29
+
const user = userStore((u) => u);
30
30
const accent = decimalToRgb(user?.accentColor as number);
31
31
32
32
const url = "/users/@me" as const;
33
33
34
34
-
const { status } = useQuery(
35
35
-
["users", "@me"],
34
34
+
const { data, error } = useQuery(
35
35
+
url,
36
36
() => getData<ApiV1MeGetResponse>(url),
37
37
{
38
38
enabled: !!user?.id,
39
39
-
onSuccess: (d) => userStore.setState({ ...user, extended: d })
39
39
+
onSuccess: (d) => userStore.setState({
40
40
+
...user,
41
41
+
extended: "statusCode" in d ? {} : d
42
42
+
}),
43
43
+
...cacheOptions
40
44
}
41
45
);
42
46
43
43
-
if (status === "error") {
47
47
+
if (error || (data && "message" in data)) {
44
48
return (
45
49
<ScreenMessage
46
50
top="0rem"
47
51
title="Something went wrong on this page.."
48
48
-
description="An unknown error occurred."
52
52
+
description={
53
53
+
(data && "message" in data ? data.message : `${error}`)
54
54
+
|| "An unknown error occurred."}
49
55
buttons={<>
50
56
<HomeButton />
51
57
<SupportButton />
+223
-30
app/profile/page.tsx
···
1
1
"use client";
2
2
3
3
import { Button } from "@nextui-org/react";
4
4
+
import { motion } from "framer-motion";
5
5
+
import Image from "next/image";
4
6
import Link from "next/link";
5
5
-
import { BsDiscord } from "react-icons/bs";
6
6
-
import { HiLightningBolt, HiViewGridAdd } from "react-icons/hi";
7
7
+
import { useSearchParams } from "next/navigation";
8
8
+
import { useCookies } from "next-client-cookies";
9
9
+
import { useState } from "react";
10
10
+
import { HiRefresh, HiUserAdd } from "react-icons/hi";
11
11
+
import { useQuery } from "react-query";
7
12
8
13
import { userStore } from "@/common/user";
9
9
-
import OverviewLinkComponent from "@/components/OverviewLinkComponent";
14
14
+
import ImageReduceMotion from "@/components/image-reduce-motion";
15
15
+
import DumbTextInput from "@/components/inputs/Dumb_TextInput";
16
16
+
import Notice, { NoticeType } from "@/components/notice";
17
17
+
import { HomeButton, ScreenMessage, SupportButton } from "@/components/screen-message";
18
18
+
import { cacheOptions, getData } from "@/lib/api";
19
19
+
import SadWumpusPic from "@/public/sad-wumpus.gif";
20
20
+
import { UserGuild } from "@/typings";
21
21
+
import cn from "@/utils/cn";
22
22
+
23
23
+
const MAX_GUILDS = 24;
10
24
11
25
export default function Home() {
12
12
-
const user = userStore((s) => s);
26
26
+
const cookies = useCookies();
27
27
+
const user = userStore((u) => u);
28
28
+
29
29
+
const display = "GRID";
30
30
+
const [search, setSearch] = useState<string>("");
31
31
+
32
32
+
function filter(guild: UserGuild) {
33
33
+
if (!search) return true;
34
34
+
35
35
+
if (guild.name.toLowerCase().includes(search.toLowerCase())) return true;
36
36
+
if (search.toLowerCase().includes(guild.name.toLowerCase())) return true;
37
37
+
38
38
+
if (guild.id.includes(search)) return true;
39
39
+
if (search.includes(guild.id)) return true;
40
40
+
41
41
+
return false;
42
42
+
}
43
43
+
44
44
+
const url = "/guilds/@me" as const;
45
45
+
46
46
+
const { isLoading, data, error } = useQuery(
47
47
+
url,
48
48
+
() => getData<UserGuild[]>(url),
49
49
+
{
50
50
+
enabled: !!user?.id,
51
51
+
...cacheOptions
52
52
+
}
53
53
+
);
54
54
+
55
55
+
if (error || (data && "message" in data)) {
56
56
+
return (
57
57
+
<ScreenMessage
58
58
+
top="0rem"
59
59
+
title="Something went wrong on this page.."
60
60
+
description={
61
61
+
(data && "message" in data ? data.message : `${error}`)
62
62
+
|| "An unknown error occurred."}
63
63
+
buttons={<>
64
64
+
<HomeButton />
65
65
+
<SupportButton />
66
66
+
</>}
67
67
+
>
68
68
+
<Image src={SadWumpusPic} alt="" height={141} width={124} />
69
69
+
</ScreenMessage>
70
70
+
);
71
71
+
}
72
72
+
73
73
+
if (isLoading || !data) return <></>;
74
74
+
75
75
+
const { length } = (data || []).filter(filter);
76
76
+
77
77
+
const springAnimation = {
78
78
+
hidden: { y: 20, opacity: 0 },
79
79
+
visible: {
80
80
+
y: 0,
81
81
+
opacity: 1,
82
82
+
transition: {
83
83
+
type: "spring",
84
84
+
bounce: data.length > 20 ? 0.2 : 0.4,
85
85
+
duration: data.length > 20 ? 0.35 : 0.7
86
86
+
}
87
87
+
}
88
88
+
} as const;
13
89
14
90
return (
15
15
-
<div>
91
91
+
<div className="flex flex-col w-full">
16
92
17
17
-
<div className="w-full md:flex gap-3">
18
18
-
<OverviewLinkComponent
19
19
-
className="md:w-2/3"
20
20
-
title="Add Wamellow to your server"
21
21
-
message="If you haven't already, now is the ideal moment to introduce Wamellow to your server."
22
22
-
url="/login?invite=true"
23
23
-
icon={<HiLightningBolt />}
93
93
+
{error ?
94
94
+
<Notice
95
95
+
type={NoticeType.Error}
96
96
+
message={`${error}`}
24
97
/>
25
25
-
<OverviewLinkComponent
26
26
-
className="md:w-1/3"
27
27
-
title="Dashboard"
28
28
-
message="Effortlessly handle all your guilds."
29
29
-
url="/dashboard"
30
30
-
icon={<HiViewGridAdd />}
31
31
-
/>
98
98
+
:
99
99
+
<></>
100
100
+
}
101
101
+
102
102
+
<div className="flex flex-col md:flex-row md:items-center gap-2">
103
103
+
104
104
+
<div className="relative top-2 md:max-w-sm w-full">
105
105
+
<DumbTextInput
106
106
+
value={search}
107
107
+
setValue={setSearch}
108
108
+
placeholder="Search"
109
109
+
thin
110
110
+
/>
111
111
+
</div>
112
112
+
113
113
+
<div className="md:ml-auto flex gap-3 md:mt-0">
114
114
+
<Button
115
115
+
as={Link}
116
116
+
className="w-1/2 md:w-min"
117
117
+
href="/login?invite=true"
118
118
+
prefetch={false}
119
119
+
startContent={<HiUserAdd />}
120
120
+
>
121
121
+
Add to Server
122
122
+
</Button>
123
123
+
<Button
124
124
+
as={Link}
125
125
+
className="button-primary w-1/2 md:w-min"
126
126
+
href="/login"
127
127
+
prefetch={false}
128
128
+
startContent={<HiRefresh />}
129
129
+
>
130
130
+
Reload
131
131
+
</Button>
132
132
+
</div>
133
133
+
32
134
</div>
33
135
34
34
-
<hr className="mb-3 dark:border-wamellow-light border-wamellow-100-light" />
136
136
+
{!isLoading &&
137
137
+
<motion.ul
138
138
+
variants={{
139
139
+
hidden: { opacity: 1, scale: 0 },
140
140
+
visible: {
141
141
+
opacity: 1,
142
142
+
scale: 1,
143
143
+
transition: {
144
144
+
delayChildren: data.length > 20 ? 0.2 : 0.3,
145
145
+
staggerChildren: data.length > 20 ? 0.1 : 0.2
146
146
+
}
147
147
+
}
148
148
+
}}
149
149
+
initial={cookies.get("reduceMotions") === "true" ? "visible" : "hidden"}
150
150
+
animate="visible"
151
151
+
className={cn(
152
152
+
"grid grid-cols-1 gap-4 w-full mt-4",
153
153
+
display === "GRID" && "lg:grid-cols-3 md:grid-cols-2"
154
154
+
)}
155
155
+
>
35
156
36
36
-
<div>Hey {user?.username}, thanks for testing out the early version of this bot :)</div>
37
37
-
<div>There will be more exciting stuff coming soon™</div>
157
157
+
{data
158
158
+
.filter(filter)
159
159
+
.slice(0, MAX_GUILDS)
160
160
+
.map((guild) => (
161
161
+
<motion.li
162
162
+
key={"guildGrid" + guild.id}
163
163
+
className="dark:bg-wamellow bg-wamellow-100 p-4 flex items-center rounded-lg drop-shadow-md overflow-hidden relative h-24 duration-100 outline-violet-500 hover:outline group/card"
164
164
+
variants={springAnimation}
165
165
+
>
166
166
+
<ImageReduceMotion
167
167
+
alt=""
168
168
+
className="absolute top-[-48px] left-0 w-full z-0 blur-xl opacity-30 pointer-events-none"
169
169
+
size={24}
170
170
+
url={`https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}`}
171
171
+
forceStatic={true}
172
172
+
/>
38
173
39
39
-
<div className="flex mt-2">
40
40
-
<Button
41
41
-
as={Link}
42
42
-
href="/support"
43
43
-
startContent={<BsDiscord />}
174
174
+
<ImageReduceMotion
175
175
+
alt={`Server icon of @${guild.name}`}
176
176
+
className="rounded-lg h-14 w-14 z-1 relative drop-shadow-md"
177
177
+
size={56}
178
178
+
url={`https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}`}
179
179
+
/>
180
180
+
181
181
+
<div className="ml-3 text-sm relative bottom-1">
182
182
+
<span className="text-lg dark:text-neutral-200 font-medium text-neutral-800 mb-1 sm:max-w-64 lg:max-w-56 truncate">
183
183
+
{guild.name}
184
184
+
</span>
185
185
+
<div className="flex gap-2">
186
186
+
<ManageButton guildId={guild.id} />
187
187
+
<LeaderboardButton guildId={guild.id} />
188
188
+
</div>
189
189
+
</div>
190
190
+
191
191
+
</motion.li>
192
192
+
))}
193
193
+
194
194
+
<motion.a
195
195
+
href="/login?invite=true"
196
196
+
target="_blank"
197
197
+
key={"guildGrid" + data.length}
198
198
+
className="border-2 dark:border-wamellow border-wamellow-100 p-4 flex justify-center items-center rounded-lg drop-shadow-md overflow-hidden relative h-24 duration-100 outline-violet-500 hover:outline"
199
199
+
variants={springAnimation}
200
200
+
>
201
201
+
Click to add a new server
202
202
+
</motion.a>
203
203
+
204
204
+
</motion.ul>
205
205
+
}
206
206
+
207
207
+
{length > MAX_GUILDS &&
208
208
+
<ScreenMessage
209
209
+
title="There are too many servers.."
210
210
+
description={`To save some performance, use the search to find a guild. Showing ${MAX_GUILDS} out of ~${length < 100 ? length : Math.round(length / 100) * 100}.`}
44
211
>
45
45
-
Join our server for updates
46
46
-
</Button>
47
47
-
</div>
212
212
+
<Image src={SadWumpusPic} alt="" height={141} width={124} />
213
213
+
</ScreenMessage>
214
214
+
}
48
215
49
216
</div>
217
217
+
);
218
218
+
}
219
219
+
220
220
+
function ManageButton({ guildId }: { guildId: string }) {
221
221
+
const searchParams = useSearchParams();
222
222
+
223
223
+
return (
224
224
+
<Button
225
225
+
as={Link}
226
226
+
href={`/dashboard/${guildId}${searchParams.get("to") ? `/${searchParams.get("to")}` : ""}`}
227
227
+
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"
228
228
+
>
229
229
+
Manage
230
230
+
</Button>
231
231
+
);
232
232
+
}
233
233
+
234
234
+
function LeaderboardButton({ guildId }: { guildId: string }) {
235
235
+
return (
236
236
+
<Button
237
237
+
as={Link}
238
238
+
href={`/leaderboard/${guildId}`}
239
239
+
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"
240
240
+
>
241
241
+
Leaderboard
242
242
+
</Button>
50
243
);
51
244
}
+35
-56
app/profile/spotify/page.tsx
···
1
1
"use client";
2
2
import Image from "next/image";
3
3
import Link from "next/link";
4
4
-
import { useEffect, useState } from "react";
5
4
import { BsSpotify } from "react-icons/bs";
6
6
-
import { HiIdentification } from "react-icons/hi";
5
5
+
import { useQuery } from "react-query";
7
6
8
7
import { userStore } from "@/common/user";
9
8
import Box from "@/components/box";
10
9
import Highlight from "@/components/discord/markdown";
11
10
import DiscordMessage from "@/components/discord/message";
12
12
-
import { ScreenMessage } from "@/components/screen-message";
13
13
-
import { ApiV1UsersMeConnectionsSpotifyGetResponse, RouteErrorResponse } from "@/typings";
11
11
+
import { HomeButton, ScreenMessage, SupportButton } from "@/components/screen-message";
12
12
+
import { cacheOptions, getData } from "@/lib/api";
13
13
+
import SadWumpusPic from "@/public/sad-wumpus.gif";
14
14
+
import { ApiV1UsersMeConnectionsSpotifyGetResponse } from "@/typings";
14
15
15
16
export default function Home({
16
17
searchParams
···
19
20
}) {
20
21
const user = userStore((s) => s);
21
22
22
22
-
const [spotify, setSpotify] = useState<ApiV1UsersMeConnectionsSpotifyGetResponse & { _fetched: boolean }>();
23
23
-
const [error, setError] = useState<string>();
23
23
+
const url = "/users/@me/connections/spotify" as const;
24
24
25
25
-
useEffect(() => {
26
26
-
fetch(`${process.env.NEXT_PUBLIC_API}/users/@me/connections/spotify`, {
27
27
-
credentials: "include"
28
28
-
})
29
29
-
.then(async (res) => {
30
30
-
const response = await res.json() as ApiV1UsersMeConnectionsSpotifyGetResponse;
31
31
-
if (!response) return;
32
32
-
33
33
-
switch (res.status) {
34
34
-
case 200: {
35
35
-
setError(undefined);
36
36
-
setSpotify({ ...response, _fetched: true });
37
37
-
break;
38
38
-
}
39
39
-
case 404: {
40
40
-
// @ts-expect-error Cuz
41
41
-
setSpotify({ _fetched: true });
42
42
-
break;
43
43
-
}
44
44
-
default: {
45
45
-
// @ts-expect-error Cuz
46
46
-
setSpotify({ _fetched: true });
47
47
-
setError((response as unknown as RouteErrorResponse).message);
48
48
-
break;
49
49
-
}
50
50
-
}
25
25
+
const { isLoading, data, error } = useQuery(
26
26
+
url,
27
27
+
() => getData<ApiV1UsersMeConnectionsSpotifyGetResponse>(url),
28
28
+
cacheOptions
29
29
+
);
51
30
52
52
-
})
53
53
-
.catch(() => {
54
54
-
setError("Error while fetching user");
55
55
-
});
56
56
-
}, []);
57
57
-
58
58
-
if (error) {
59
59
-
return <>
31
31
+
if (error || (data && "message" in data)) {
32
32
+
return (
60
33
<ScreenMessage
61
61
-
title="Something went wrong.."
62
62
-
description={error}
63
63
-
href="/profile"
64
64
-
button="Go back to overview"
65
65
-
icon={<HiIdentification />}
66
66
-
/>
67
67
-
</>;
34
34
+
top="0rem"
35
35
+
title="Something went wrong on this page.."
36
36
+
description={
37
37
+
(data && "message" in data ? data.message : `${error}`)
38
38
+
|| "An unknown error occurred."}
39
39
+
buttons={<>
40
40
+
<HomeButton />
41
41
+
<SupportButton />
42
42
+
</>}
43
43
+
>
44
44
+
<Image src={SadWumpusPic} alt="" height={141} width={124} />
45
45
+
</ScreenMessage>
46
46
+
);
68
47
}
69
48
70
70
-
if (!spotify?._fetched) return <></>;
49
49
+
if (isLoading || !data) return <></>;
71
50
72
51
return (
73
52
<div className="h-full">
74
53
75
75
-
{!spotify.displayName &&
54
54
+
{!data.displayName &&
76
55
<ScreenMessage
77
56
title="Nothing to see here.. yet.."
78
57
description="Cool things will come soon"
···
83
62
/>
84
63
}
85
64
86
86
-
{spotify.displayName && user?.id &&
65
65
+
{data.displayName && user?.id &&
87
66
<>
88
67
89
68
<div className="flex items-center gap-2">
90
69
{/* eslint-disable-next-line @next/next/no-img-element */}
91
91
-
<img src={spotify.avatar ? spotify.avatar : "/discord.webp"} alt="your spotify avatar" className="rounded-lg mr-1 h-14 w-14" />
70
70
+
<img src={data.avatar ? data.avatar : "/discord.webp"} alt="your spotify avatar" className="rounded-lg mr-1 h-14 w-14" />
92
71
<div>
93
72
<div className="text-2xl dark:text-neutral-200 text-neutral-800 font-medium flex gap-1 items-center">
94
94
-
{spotify.displayName}
73
73
+
{data.displayName}
95
74
<BsSpotify className="h-4 relative top-0.5 text-[#1ed760]" />
96
75
</div>
97
76
<div className="flex items-center">
···
102
81
>
103
82
Not you?
104
83
</Link>
105
105
-
{searchParams.spotify_login_success === "true" && spotify.displayName && <>
84
84
+
{searchParams.spotify_login_success === "true" && data.displayName && <>
106
85
<span className="mx-2 text-neutral-500">•</span>
107
86
<div className="text-green-500 duration-200">Link was successfull!</div>
108
87
</>}
···
123
102
}}
124
103
>
125
104
126
126
-
<Highlight mode={"DARK"} text={`wm play [https://open.spotify.com/track/${spotify.playing?.id || "4cOdK2wGLETKBW3PvgPWqT"}](#)`} />
105
105
+
<Highlight mode={"DARK"} text={`wm play [https://open.data.com/track/${data.playing?.id || "4cOdK2wGLETKBW3PvgPWqT"}](#)`} />
127
106
128
107
</DiscordMessage>
129
108
<DiscordMessage
···
137
116
138
117
<div className="flex items-center gap-1">
139
118
<Image src="https://cdn.discordapp.com/emojis/845043307351900183.gif?size=44&quality=lossless" height={18} width={18} alt="" />
140
140
-
<Highlight mode={"DARK"} text={`@${user.username} now playing [${spotify.playing?.name || "Never Gonna Give You Up"}](#) for **${spotify.playing?.duration || "3 minutes 33 seconds"}**`} />
119
119
+
<Highlight mode={"DARK"} text={`@${user.username} now playing [${data.playing?.name || "Never Gonna Give You Up"}](#) for **${data.playing?.duration || "3 minutes 33 seconds"}**`} />
141
120
</div>
142
121
143
122
<div className="flex flex-row gap-1.5 h-8 mt-3">
···
177
156
178
157
<div className="flex items-center gap-1">
179
158
<Image src="https://cdn.discordapp.com/emojis/845043307351900183.gif?size=44&quality=lossless" height={18} width={18} alt="" />
180
180
-
<Highlight mode={"DARK"} text={`@${user.username} is playing [${spotify.playing?.name || "Never Gonna Give You Up"}](#) by ${spotify.playing?.artists || "[Rick Astley]()"}`} />
159
159
+
<Highlight mode={"DARK"} text={`@${user.username} is playing [${data.playing?.name || "Never Gonna Give You Up"}](#) by ${data.playing?.artists || "[Rick Astley]()"}`} />
181
160
</div>
182
161
183
162
<div className="flex gap-1.5 h-8 mt-3">
+5
-4
components/list.tsx
···
28
28
if (!key && typeof key !== "string") return;
29
29
30
30
if (!searchParamName) {
31
31
-
router.push(`${url}${key}`);
31
31
+
router.push(`${url}${key}?${params.toString()}`);
32
32
return;
33
33
}
34
34
···
48
48
tabList: "w-full relative rounded-none p-0 border-b border-divider",
49
49
tab: "w-fit px-4 h-12 relative right-2.5"
50
50
}}
51
51
-
defaultSelectedKey={searchParamName
52
52
-
? (params.get(searchParamName) || "")
53
53
-
: path.split(url)[1].split("/").slice(0, 2).join("/")
51
51
+
defaultSelectedKey={
52
52
+
searchParamName
53
53
+
? (params.get(searchParamName) || "")
54
54
+
: path.split(url)[1].split("/").slice(0, 2).join("/")
54
55
}
55
56
onSelectionChange={handleChange}
56
57
variant="underlined"
+5
-4
lib/api.ts
···
1
1
-
import { ApiError, ApiV1GuildsGetResponse } from "@/typings";
1
1
+
import { ApiError, ApiV1GuildsGetResponse, RouteErrorResponse } from "@/typings";
2
2
3
3
export const cacheOptions = {
4
4
cacheTime: 1000 * 60 * 5,
···
8
8
9
9
export const defaultFetchOptions = { headers: { Authorization: process.env.API_SECRET as string }, next: { revalidate: 60 * 60 } };
10
10
11
11
-
export async function getData<T>(path: string) {
12
12
-
const response = await fetch(`${process.env.NEXT_PUBLIC_API}${path}`, {
11
11
+
export async function getData<T>(path: string, domain?: string) {
12
12
+
console.log(`${domain || process.env.NEXT_PUBLIC_API}${path}`);
13
13
+
const response = await fetch(`${domain || process.env.NEXT_PUBLIC_API}${path}`, {
13
14
credentials: "include"
14
15
});
15
16
16
16
-
return response.json() as Promise<T>;
17
17
+
return response.json() as Promise<T | RouteErrorResponse>;
17
18
}
18
19
19
20
export async function getGuild(guildId?: string | null): Promise<ApiV1GuildsGetResponse | ApiError | undefined> {