mirror of
https://github.com/Equicord/Equicord.git
synced 2025-02-21 15:48:52 -05:00
Update Theme Library (#7)
* UpdateThemeLiberry * Delete old index * Update Authors * Update index.tsx --------- Co-authored-by: thororen <78185467+thororen1234@users.noreply.github.com>
This commit is contained in:
parent
35b5cd1182
commit
970bfb757a
5 changed files with 208 additions and 29 deletions
100
src/equicordplugins/themeLibrary/components/LikesComponent.tsx
Normal file
100
src/equicordplugins/themeLibrary/components/LikesComponent.tsx
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Logger } from "@utils/Logger";
|
||||||
|
import { Button, useEffect, UserStore, useState } from "@webpack/common";
|
||||||
|
import type { User } from "discord-types/general";
|
||||||
|
|
||||||
|
import type { Theme, ThemeLikeProps } from "../types";
|
||||||
|
import { themeRequest } from "./ThemeTab";
|
||||||
|
|
||||||
|
const logger = new Logger("ThemeLibrary", "#e5c890");
|
||||||
|
|
||||||
|
const fetchLikes = async () => {
|
||||||
|
try {
|
||||||
|
const response = await themeRequest("/likes/get");
|
||||||
|
const data = await response.json();
|
||||||
|
return data;
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LikesComponent = ({ themeId, likedThemes: initialLikedThemes }: { themeId: Theme["id"], likedThemes: ThemeLikeProps | undefined; }) => {
|
||||||
|
const [likesCount, setLikesCount] = useState(0);
|
||||||
|
const [likedThemes, setLikedThemes] = useState(initialLikedThemes);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const likes = getThemeLikes(themeId);
|
||||||
|
logger.debug("likes", likes, "for:", themeId);
|
||||||
|
setLikesCount(likes);
|
||||||
|
}, [likedThemes, themeId]);
|
||||||
|
|
||||||
|
function getThemeLikes(themeId: Theme["id"]): number {
|
||||||
|
const themeLike = likedThemes?.likes.find(like => like.themeId === themeId as unknown as Number);
|
||||||
|
return themeLike ? themeLike.userIds.length : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const likeIcon = (isLiked: boolean) => (
|
||||||
|
<svg viewBox="0 0 20 20" fill={isLiked ? "red" : "currentColor"} aria-hidden="true" width="18" height="18" className="vce-likes-icon vce-likes-icon-animation">
|
||||||
|
<path d="M16.44 3.10156C14.63 3.10156 13.01 3.98156 12 5.33156C10.99 3.98156 9.37 3.10156 7.56 3.10156C4.49 3.10156 2 5.60156 2 8.69156C2 9.88156 2.19 10.9816 2.52 12.0016C4.1 17.0016 8.97 19.9916 11.38 20.8116C11.72 20.9316 12.28 20.9316 12.62 20.8116C15.03 19.9916 19.9 17.0016 21.48 12.0016C21.81 10.9816 22 9.88156 22 8.69156C22 5.60156 19.51 3.10156 16.44 3.10156Z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleLikeClick = async (themeId: Theme["id"]) => {
|
||||||
|
const theme = likedThemes?.likes.find(like => like.themeId === themeId as unknown as Number);
|
||||||
|
const currentUser: User = UserStore.getCurrentUser();
|
||||||
|
const hasLiked: boolean = theme?.userIds.includes(currentUser.id) ?? false;
|
||||||
|
const endpoint = hasLiked ? "/likes/remove" : "/likes/add";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await themeRequest(endpoint, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
cache: "no-store",
|
||||||
|
body: JSON.stringify({
|
||||||
|
userId: currentUser.id,
|
||||||
|
themeId: themeId,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) return logger.error("Couldnt update likes, res:", response.statusText);
|
||||||
|
|
||||||
|
const fetchLikes = async () => {
|
||||||
|
try {
|
||||||
|
const response = await themeRequest("/likes/get");
|
||||||
|
const data = await response.json();
|
||||||
|
setLikedThemes(data);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchLikes();
|
||||||
|
// doing it locally isnt the best way probably, but it does the same
|
||||||
|
setLikesCount(likesCount + (hasLiked ? -1 : 1));
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasLiked = likedThemes?.likes.some(like => like.themeId === themeId as unknown as Number && like.userIds.includes(UserStore.getCurrentUser().id)) ?? false;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Button onClick={() => handleLikeClick(themeId)}
|
||||||
|
size={Button.Sizes.MEDIUM}
|
||||||
|
color={Button.Colors.PRIMARY}
|
||||||
|
look={Button.Looks.OUTLINED}
|
||||||
|
style={{ marginLeft: "8px" }}
|
||||||
|
>
|
||||||
|
{likeIcon(hasLiked)} {likesCount}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -15,6 +15,7 @@ import { ErrorCard } from "@components/ErrorCard";
|
||||||
import { OpenExternalIcon } from "@components/Icons";
|
import { OpenExternalIcon } from "@components/Icons";
|
||||||
import { SettingsTab, wrapTab } from "@components/VencordSettings/shared";
|
import { SettingsTab, wrapTab } from "@components/VencordSettings/shared";
|
||||||
import { proxyLazy } from "@utils/lazy";
|
import { proxyLazy } from "@utils/lazy";
|
||||||
|
import { Logger } from "@utils/Logger";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
import { classes } from "@utils/misc";
|
import { classes } from "@utils/misc";
|
||||||
import { openModal } from "@utils/modal";
|
import { openModal } from "@utils/modal";
|
||||||
|
@ -23,7 +24,8 @@ import { Button, Card, FluxDispatcher, Forms, React, Select, showToast, TabBar,
|
||||||
import { User } from "discord-types/general";
|
import { User } from "discord-types/general";
|
||||||
import { Constructor } from "type-fest";
|
import { Constructor } from "type-fest";
|
||||||
|
|
||||||
import { SearchStatus, TabItem, Theme } from "../types";
|
import { SearchStatus, TabItem, Theme, ThemeLikeProps } from "../types";
|
||||||
|
import { LikesComponent } from "./LikesComponent";
|
||||||
import { ThemeInfoModal } from "./ThemeInfoModal";
|
import { ThemeInfoModal } from "./ThemeInfoModal";
|
||||||
|
|
||||||
const cl = classNameFactory("vc-plugins-");
|
const cl = classNameFactory("vc-plugins-");
|
||||||
|
@ -33,6 +35,8 @@ const TextAreaProps = findLazy(m => typeof m.textarea === "string");
|
||||||
|
|
||||||
const API_URL = "https://themes-delta.vercel.app/api";
|
const API_URL = "https://themes-delta.vercel.app/api";
|
||||||
|
|
||||||
|
const logger = new Logger("ThemeLibrary", "#e5c890");
|
||||||
|
|
||||||
async function fetchThemes(url: string): Promise<Theme[]> {
|
async function fetchThemes(url: string): Promise<Theme[]> {
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
@ -47,7 +51,7 @@ async function fetchThemes(url: string): Promise<Theme[]> {
|
||||||
return themes.sort((a, b) => new Date(b.release_date).getTime() - new Date(a.release_date).getTime());
|
return themes.sort((a, b) => new Date(b.release_date).getTime() - new Date(a.release_date).getTime());
|
||||||
}
|
}
|
||||||
|
|
||||||
function API_TYPE(theme, returnAll?: boolean) {
|
function API_TYPE(theme: Theme | Object, returnAll?: boolean) {
|
||||||
if (!theme) return;
|
if (!theme) return;
|
||||||
const settings = Settings.plugins.ThemeLibrary.domain ?? false;
|
const settings = Settings.plugins.ThemeLibrary.domain ?? false;
|
||||||
|
|
||||||
|
@ -55,11 +59,12 @@ function API_TYPE(theme, returnAll?: boolean) {
|
||||||
const url = settings ? "https://raw.githubusercontent.com/Faf4a/plugins/main/assets/meta.json" : `${API_URL}/themes`;
|
const url = settings ? "https://raw.githubusercontent.com/Faf4a/plugins/main/assets/meta.json" : `${API_URL}/themes`;
|
||||||
return fetchThemes(url);
|
return fetchThemes(url);
|
||||||
} else {
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
return settings ? theme.source : `${API_URL}/${theme.name}`;
|
return settings ? theme.source : `${API_URL}/${theme.name}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function themeRequest(path: string, options: RequestInit = {}) {
|
export async function themeRequest(path: string, options: RequestInit = {}) {
|
||||||
return fetch(API_URL + path, {
|
return fetch(API_URL + path, {
|
||||||
...options,
|
...options,
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -100,7 +105,9 @@ function ThemeTab() {
|
||||||
const [themes, setThemes] = useState<Theme[]>([]);
|
const [themes, setThemes] = useState<Theme[]>([]);
|
||||||
const [filteredThemes, setFilteredThemes] = useState<Theme[]>([]);
|
const [filteredThemes, setFilteredThemes] = useState<Theme[]>([]);
|
||||||
const [themeLinks, setThemeLinks] = useState(Vencord.Settings.themeLinks);
|
const [themeLinks, setThemeLinks] = useState(Vencord.Settings.themeLinks);
|
||||||
|
const [likedThemes, setLikedThemes] = useState<ThemeLikeProps>();
|
||||||
const [searchValue, setSearchValue] = useState({ value: "", status: SearchStatus.ALL });
|
const [searchValue, setSearchValue] = useState({ value: "", status: SearchStatus.ALL });
|
||||||
|
const [hideWarningCard, setHideWarningCard] = useState(Settings.plugins.ThemeLibrary.hideWarningCard);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
const getUser = (id: string, username: string) => UserUtils.getUser(id) ?? makeDummyUser({ username, id });
|
const getUser = (id: string, username: string) => UserUtils.getUser(id) ?? makeDummyUser({ username, id });
|
||||||
|
@ -126,22 +133,38 @@ function ThemeTab() {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchLikes = async () => {
|
||||||
|
try {
|
||||||
|
const response = await themeRequest("/likes/get");
|
||||||
|
const data = await response.json();
|
||||||
|
return data;
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Promise.resolve(API_TYPE({}, true)).then(themes => {
|
const fetchThemes = async () => {
|
||||||
setThemes(themes);
|
try {
|
||||||
setFilteredThemes(themes);
|
const themes = await API_TYPE({}, true);
|
||||||
setLoading(false);
|
// fetch likes
|
||||||
}).catch(err => {
|
setThemes(themes);
|
||||||
console.error("Failed to load 'ThemeLibrary'", err);
|
const likes = await fetchLikes();
|
||||||
setLoading(true);
|
setLikedThemes(likes);
|
||||||
});
|
setFilteredThemes(themes);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchThemes();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setThemeLinks(Vencord.Settings.themeLinks);
|
setThemeLinks(Vencord.Settings.themeLinks);
|
||||||
}, [Vencord.Settings.themeLinks]);
|
}, [Vencord.Settings.themeLinks]);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const filteredThemes = themes.filter(themeFilter);
|
const filteredThemes = themes.filter(themeFilter);
|
||||||
setFilteredThemes(filteredThemes);
|
setFilteredThemes(filteredThemes);
|
||||||
|
@ -160,15 +183,27 @@ function ThemeTab() {
|
||||||
height: "100%",
|
height: "100%",
|
||||||
fontSize: "1.5em",
|
fontSize: "1.5em",
|
||||||
color: "var(--text-muted)"
|
color: "var(--text-muted)"
|
||||||
}}>Loading Themes...</div>
|
}}>Loading themes...</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<ErrorCard id="vc-themetab-warning">
|
{hideWarningCard ? null : (
|
||||||
<Forms.FormTitle tag="h4">Want your theme removed?</Forms.FormTitle>
|
<ErrorCard id="vc-themetab-warning">
|
||||||
<Forms.FormText className={Margins.top8}>
|
<Forms.FormTitle tag="h4">Want your theme removed?</Forms.FormTitle>
|
||||||
If you want your theme(s) permanently removed, please open an issue on <a href="https://github.com/Faf4a/plugins/issues/new?labels=removal&projects=&template=request_removal.yml&title=Theme+Removal">GitHub <OpenExternalIcon height={16} width={16} /></a>
|
<Forms.FormText className={Margins.top8}>
|
||||||
</Forms.FormText>
|
If you want your theme(s) permanently removed, please open an issue on <a href="https://github.com/Faf4a/plugins/issues/new?labels=removal&projects=&template=request_removal.yml&title=Theme+Removal">GitHub <OpenExternalIcon height={16} width={16} /></a>
|
||||||
</ErrorCard >
|
</Forms.FormText>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
Settings.plugins.ThemeLibrary.hideWarningCard = true;
|
||||||
|
setHideWarningCard(true);
|
||||||
|
}}
|
||||||
|
size={Button.Sizes.SMALL}
|
||||||
|
color={Button.Colors.RED}
|
||||||
|
look={Button.Looks.FILLED}
|
||||||
|
className={Margins.top8}
|
||||||
|
>Hide</Button>
|
||||||
|
</ErrorCard >
|
||||||
|
)}
|
||||||
<div className={`${Margins.bottom8} ${Margins.top16}`}>
|
<div className={`${Margins.bottom8} ${Margins.top16}`}>
|
||||||
<Forms.FormTitle tag="h2"
|
<Forms.FormTitle tag="h2"
|
||||||
style={{
|
style={{
|
||||||
|
@ -291,10 +326,12 @@ function ThemeTab() {
|
||||||
{ label: "Show All", value: SearchStatus.ALL, default: true },
|
{ label: "Show All", value: SearchStatus.ALL, default: true },
|
||||||
{ label: "Show Themes", value: SearchStatus.THEME },
|
{ label: "Show Themes", value: SearchStatus.THEME },
|
||||||
{ label: "Show Snippets", value: SearchStatus.SNIPPET },
|
{ label: "Show Snippets", value: SearchStatus.SNIPPET },
|
||||||
{ label: "Show Enabled", value: SearchStatus.ENABLED },
|
// TODO: filter for most liked themes
|
||||||
{ label: "Show Disabled", value: SearchStatus.DISABLED },
|
// { label: "Show Most Liked", value: SearchStatus.LIKED },
|
||||||
{ label: "Show Dark", value: SearchStatus.DARK },
|
{ label: "Show Dark", value: SearchStatus.DARK },
|
||||||
{ label: "Show Light", value: SearchStatus.LIGHT },
|
{ label: "Show Light", value: SearchStatus.LIGHT },
|
||||||
|
{ label: "Show Enabled", value: SearchStatus.ENABLED },
|
||||||
|
{ label: "Show Disabled", value: SearchStatus.DISABLED },
|
||||||
]}
|
]}
|
||||||
serialize={String}
|
serialize={String}
|
||||||
select={onStatusChange}
|
select={onStatusChange}
|
||||||
|
@ -385,6 +422,7 @@ function ThemeTab() {
|
||||||
>
|
>
|
||||||
Theme Info
|
Theme Info
|
||||||
</Button>
|
</Button>
|
||||||
|
<LikesComponent themeId={theme.id} likedThemes={likedThemes} />
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const content = atob(theme.content);
|
const content = atob(theme.content);
|
||||||
|
@ -445,6 +483,9 @@ function SubmitThemes() {
|
||||||
}}>
|
}}>
|
||||||
Submit Themes
|
Submit Themes
|
||||||
</Forms.FormTitle>
|
</Forms.FormTitle>
|
||||||
|
<Forms.FormText>
|
||||||
|
If you plan on updating your theme / snippet frequently, consider using an <code>@import</code> instead!
|
||||||
|
</Forms.FormText>
|
||||||
<Forms.FormText>
|
<Forms.FormText>
|
||||||
<TextArea
|
<TextArea
|
||||||
content={themeTemplate}
|
content={themeTemplate}
|
||||||
|
@ -546,4 +587,4 @@ function ThemeLibrary() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default wrapTab(ThemeLibrary, "Theme Library");
|
export default wrapTab(ThemeLibrary, "Theme Library");
|
|
@ -1,5 +1,10 @@
|
||||||
/* stylelint-disable property-no-vendor-prefix */
|
@keyframes bounce {
|
||||||
|
0%, 100% { transform: scale(1); }
|
||||||
|
50% { transform: scale(1.2); }
|
||||||
|
}
|
||||||
|
|
||||||
[data-tab-id="ThemeLibrary"]::before {
|
[data-tab-id="ThemeLibrary"]::before {
|
||||||
|
/* stylelint-disable-next-line property-no-vendor-prefix */
|
||||||
-webkit-mask: var(--si-widget) center/contain no-repeat !important;
|
-webkit-mask: var(--si-widget) center/contain no-repeat !important;
|
||||||
mask: var(--si-widget) center/contain no-repeat !important;
|
mask: var(--si-widget) center/contain no-repeat !important;
|
||||||
}
|
}
|
||||||
|
@ -52,3 +57,9 @@
|
||||||
border: 2px solid var(--background-tertiary);
|
border: 2px solid var(--background-tertiary);
|
||||||
max-height: unset;
|
max-height: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vce-likes-icon {
|
||||||
|
overflow: visible;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
transition: fill 0.3s ease;
|
||||||
|
}
|
|
@ -10,11 +10,17 @@ import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { SettingsRouter } from "@webpack/common";
|
import { SettingsRouter } from "@webpack/common";
|
||||||
|
|
||||||
const settings = definePluginSettings({
|
const settings = definePluginSettings({
|
||||||
|
hideWarningCard: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
default: false,
|
||||||
|
description: "Hide the warning card displayed at the top of the theme library tab",
|
||||||
|
restartNeeded: false,
|
||||||
|
},
|
||||||
domain: {
|
domain: {
|
||||||
type: OptionType.BOOLEAN,
|
type: OptionType.BOOLEAN,
|
||||||
default: false,
|
default: false,
|
||||||
description: "Use Github instead of the default domain for themes",
|
description: "Use Github instead of the default domain for themes",
|
||||||
restartNeeded: false
|
restartNeeded: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -26,19 +32,21 @@ export default definePlugin({
|
||||||
toolboxActions: {
|
toolboxActions: {
|
||||||
"Open Theme Library": () => {
|
"Open Theme Library": () => {
|
||||||
SettingsRouter.open("ThemeLibrary");
|
SettingsRouter.open("ThemeLibrary");
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
const customSettingsSections = (
|
const customSettingsSections = (
|
||||||
Vencord.Plugins.plugins.Settings as any as { customSections: ((ID: Record<string, unknown>) => any)[]; }
|
Vencord.Plugins.plugins.Settings as any as {
|
||||||
|
customSections: ((ID: Record<string, unknown>) => any)[];
|
||||||
|
}
|
||||||
).customSections;
|
).customSections;
|
||||||
|
|
||||||
const ThemeSection = () => ({
|
const ThemeSection = () => ({
|
||||||
section: "ThemeLibrary",
|
section: "ThemeLibrary",
|
||||||
label: "Theme Library",
|
label: "Theme Library",
|
||||||
element: require("./components/ThemeTab").default,
|
element: require("./components/ThemeTab").default,
|
||||||
id: "ThemeSection"
|
id: "ThemeSection",
|
||||||
});
|
});
|
||||||
|
|
||||||
customSettingsSections.push(ThemeSection);
|
customSettingsSections.push(ThemeSection);
|
||||||
|
@ -46,10 +54,14 @@ export default definePlugin({
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
const customSettingsSections = (
|
const customSettingsSections = (
|
||||||
Vencord.Plugins.plugins.Settings as any as { customSections: ((ID: Record<string, unknown>) => any)[]; }
|
Vencord.Plugins.plugins.Settings as any as {
|
||||||
|
customSections: ((ID: Record<string, unknown>) => any)[];
|
||||||
|
}
|
||||||
).customSections;
|
).customSections;
|
||||||
|
|
||||||
const i = customSettingsSections.findIndex(section => section({}).id === "ThemeSection");
|
const i = customSettingsSections.findIndex(
|
||||||
|
section => section({}).id === "ThemeSection"
|
||||||
|
);
|
||||||
|
|
||||||
if (i !== -1) customSettingsSections.splice(i, 1);
|
if (i !== -1) customSettingsSections.splice(i, 1);
|
||||||
},
|
},
|
|
@ -46,6 +46,11 @@ export const enum TabItem {
|
||||||
SUBMIT_THEMES,
|
SUBMIT_THEMES,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LikesComponentProps {
|
||||||
|
theme: Theme;
|
||||||
|
userId: User["id"];
|
||||||
|
}
|
||||||
|
|
||||||
export const enum SearchStatus {
|
export const enum SearchStatus {
|
||||||
ALL,
|
ALL,
|
||||||
ENABLED,
|
ENABLED,
|
||||||
|
@ -54,4 +59,14 @@ export const enum SearchStatus {
|
||||||
SNIPPET,
|
SNIPPET,
|
||||||
DARK,
|
DARK,
|
||||||
LIGHT,
|
LIGHT,
|
||||||
|
LIKED,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ThemeLikeProps = {
|
||||||
|
status: number;
|
||||||
|
likes: [{
|
||||||
|
_id?: string;
|
||||||
|
themeId: number;
|
||||||
|
userIds: User["id"][];
|
||||||
|
}];
|
||||||
|
};
|
||||||
|
|
Loading…
Add table
Reference in a new issue