diff --git a/src/equicordplugins/CharacterCounter/index.tsx b/src/equicordplugins/CharacterCounter/index.tsx
index 48f0b8cc..aa3b6f41 100644
--- a/src/equicordplugins/CharacterCounter/index.tsx
+++ b/src/equicordplugins/CharacterCounter/index.tsx
@@ -8,6 +8,7 @@ import "./style.css";
import { definePluginSettings } from "@api/Settings";
import { EquicordDevs } from "@utils/constants";
+import { getCurrentChannel } from "@utils/discord";
import definePlugin, { OptionType } from "@utils/types";
import { waitFor } from "@webpack";
import { UserStore } from "@webpack/common";
@@ -15,6 +16,7 @@ import { UserStore } from "@webpack/common";
let ChannelTextAreaClasses;
let shouldShowColorEffects: boolean;
let position: boolean;
+let forceLeft = false;
waitFor(["buttonContainer", "channelTextArea"], m => (ChannelTextAreaClasses = m));
@@ -68,7 +70,7 @@ export default definePlugin({
charCounterDiv = document.createElement("div");
charCounterDiv.classList.add("char-counter");
- if (position) charCounterDiv.classList.add("left");
+ if (position || forceLeft) charCounterDiv.classList.add("left");
charCounterDiv.innerHTML = `0 /${charMax} `;
}
@@ -120,7 +122,10 @@ export default definePlugin({
const observeDOMChanges = () => {
const observer = new MutationObserver(() => {
const chatTextArea = document.querySelector(`.${ChannelTextAreaClasses?.channelTextArea}`);
- if (chatTextArea) {
+ if (chatTextArea && !document.querySelector(".char-counter")) {
+ const currentChannel = getCurrentChannel();
+ forceLeft = currentChannel?.rateLimitPerUser !== 0;
+
addCharCounter();
}
});
diff --git a/src/equicordplugins/CharacterCounter/style.css b/src/equicordplugins/CharacterCounter/style.css
index 080a1ece..b8a3fa8a 100644
--- a/src/equicordplugins/CharacterCounter/style.css
+++ b/src/equicordplugins/CharacterCounter/style.css
@@ -3,7 +3,7 @@
color: var(--text-muted);
text-align: right;
position: absolute;
- bottom: 0;
+ bottom: -15px;
right: 0;
pointer-events: none;
z-index: 1;
diff --git a/src/equicordplugins/themeLibrary/components/LikesComponent.tsx b/src/equicordplugins/themeLibrary/components/LikesComponent.tsx
index a906cf1a..14e3de26 100644
--- a/src/equicordplugins/themeLibrary/components/LikesComponent.tsx
+++ b/src/equicordplugins/themeLibrary/components/LikesComponent.tsx
@@ -5,16 +5,13 @@
*/
import * as DataStore from "@api/DataStore";
-import { Logger } from "@utils/Logger";
import { Button, useEffect, useRef, UserStore, useState } from "@webpack/common";
import type { User } from "discord-types/general";
import type { Theme, ThemeLikeProps } from "../types";
import { isAuthorized } from "../utils/auth";
import { LikeIcon } from "../utils/Icons";
-import { themeRequest } from "./ThemeTab";
-
-export const logger = new Logger("ThemeLibrary", "#e5c890");
+import { logger, themeRequest } from "./ThemeTab";
export const LikesComponent = ({ themeId, likedThemes: initialLikedThemes }: { themeId: Theme["id"], likedThemes: ThemeLikeProps | undefined; }) => {
const [likesCount, setLikesCount] = useState(0);
@@ -35,7 +32,7 @@ export const LikesComponent = ({ themeId, likedThemes: initialLikedThemes }: { t
if (!isAuthorized()) return;
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 hasLiked: boolean = (theme?.userIds.includes(currentUser.id) || themeId === "preview") ?? false;
const endpoint = hasLiked ? "/likes/remove" : "/likes/add";
const token = await DataStore.get("ThemeLibrary_uniqueToken");
@@ -83,9 +80,10 @@ export const LikesComponent = ({ themeId, likedThemes: initialLikedThemes }: { t
size={Button.Sizes.MEDIUM}
color={Button.Colors.PRIMARY}
look={Button.Looks.OUTLINED}
+ disabled={themeId === "preview"}
style={{ marginLeft: "8px" }}
>
- {LikeIcon(hasLiked)} {likesCount}
+ {LikeIcon(hasLiked || themeId === "preview")} {themeId === "preview" ? 143 : likesCount}
);
diff --git a/src/equicordplugins/themeLibrary/components/ThemeCard.tsx b/src/equicordplugins/themeLibrary/components/ThemeCard.tsx
new file mode 100644
index 00000000..e2f85424
--- /dev/null
+++ b/src/equicordplugins/themeLibrary/components/ThemeCard.tsx
@@ -0,0 +1,180 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2024 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import { generateId } from "@api/Commands";
+import { Settings } from "@api/Settings";
+import { OpenExternalIcon } from "@components/Icons";
+import { proxyLazy } from "@utils/lazy";
+import { Margins } from "@utils/margins";
+import { ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
+import { Button, Card, FluxDispatcher, Forms, Parser, React, UserStore, UserUtils } from "@webpack/common";
+import { User } from "discord-types/general";
+import { Constructor } from "type-fest";
+
+import type { Theme, ThemeLikeProps } from "../types";
+import { LikesComponent } from "./LikesComponent";
+import { ThemeInfoModal } from "./ThemeInfoModal";
+import { apiUrl } from "./ThemeTab";
+
+interface ThemeCardProps {
+ theme: Theme;
+ themeLinks: string[];
+ likedThemes?: ThemeLikeProps;
+ setThemeLinks: (links: string[]) => void;
+ removePreview?: boolean;
+ removeButtons?: boolean;
+}
+
+const UserRecord: Constructor> = proxyLazy(() => UserStore.getCurrentUser().constructor) as any;
+
+function makeDummyUser(user: { username: string; id?: string; avatar?: string; }) {
+ const newUser = new UserRecord({
+ username: user.username,
+ id: user.id ?? generateId(),
+ avatar: user.avatar,
+ bot: true,
+ });
+ FluxDispatcher.dispatch({
+ type: "USER_UPDATE",
+ user: newUser,
+ });
+ return newUser;
+}
+
+export const ThemeCard: React.FC = ({ theme, themeLinks, likedThemes, setThemeLinks, removeButtons, removePreview }) => {
+
+ const getUser = (id: string, username: string) => UserUtils.getUser(id) ?? makeDummyUser({ username, id });
+
+ const handleAddRemoveTheme = () => {
+ const onlineThemeLinks = themeLinks.includes(`${apiUrl}/${theme.name}`)
+ ? themeLinks.filter(link => link !== `${apiUrl}/${theme.name}`)
+ : [...themeLinks, `${apiUrl}/${theme.name}`];
+
+ setThemeLinks(onlineThemeLinks);
+ Vencord.Settings.themeLinks = onlineThemeLinks;
+ };
+
+ const handleThemeAttributesCheck = () => {
+ const requiresThemeAttributes = theme.requiresThemeAttributes ?? false;
+
+ if (requiresThemeAttributes && !Settings.plugins.ThemeAttributes.enabled) {
+ openModal(modalProps => (
+
+
+ Hold on!
+
+
+
+ This theme requires the ThemeAttributes plugin to work properly!
+ Do you want to enable it?
+
+
+
+ {
+ Settings.plugins.ThemeAttributes.enabled = true;
+ modalProps.onClose();
+ handleAddRemoveTheme();
+ }}
+ >
+ Enable Plugin
+
+ modalProps.onClose()}
+ >
+ Cancel
+
+
+
+ ));
+ } else {
+ handleAddRemoveTheme();
+ }
+ };
+
+ const handleViewSource = () => {
+ const content = window.atob(theme.content);
+ const metadata = content.match(/\/\*\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+\//g)?.[0] || "";
+ const source = metadata.match(/@source\s+(.+)/)?.[1] || "";
+
+ if (source) {
+ VencordNative.native.openExternal(source);
+ } else {
+ VencordNative.native.openExternal(`${apiUrl}/${theme.name}`);
+ }
+ };
+
+ return (
+
+
+ {theme.name}
+
+
+ {Parser.parse(theme.description)}
+
+ {!removePreview && (
+
+ )}
+
+
+ {theme.tags && (
+
+ {theme.tags.map(tag => (
+
+ {tag}
+
+ ))}
+
+ )}
+ {!removeButtons && (
+ < div style={{ marginTop: "8px", display: "flex", flexDirection: "row" }}>
+
+ {themeLinks.includes(`${apiUrl}/${theme.name}`) ? "Remove Theme" : "Add Theme"}
+
+ {
+ const authors = Array.isArray(theme.author)
+ ? await Promise.all(theme.author.map(author => getUser(author.discord_snowflake, author.discord_name)))
+ : [await getUser(theme.author.discord_snowflake, theme.author.discord_name)];
+
+ openModal(props => );
+ }}
+ size={Button.Sizes.MEDIUM}
+ color={Button.Colors.BRAND}
+ look={Button.Looks.FILLED}
+ >
+ Theme Info
+
+
+
+ View Source
+
+
+ )}
+
+
+
+ );
+};
diff --git a/src/equicordplugins/themeLibrary/components/ThemeInfoModal.tsx b/src/equicordplugins/themeLibrary/components/ThemeInfoModal.tsx
index b0a2dbdb..ada8ca91 100644
--- a/src/equicordplugins/themeLibrary/components/ThemeInfoModal.tsx
+++ b/src/equicordplugins/themeLibrary/components/ThemeInfoModal.tsx
@@ -16,7 +16,7 @@ import { Button, Clipboard, Forms, Parser, React, showToast, Toasts } from "@web
import { Theme, ThemeInfoModalProps } from "../types";
import { ClockIcon, DownloadIcon, WarningIcon } from "../utils/Icons";
-import { logger } from "./LikesComponent";
+import { logger } from "./ThemeTab";
const Native = VencordNative.pluginHelpers.ThemeLibrary as PluginNative;
const UserSummaryItem = findComponentByCodeLazy("defaultRenderUser", "showDefaultAvatarsForNullUsers");
@@ -25,7 +25,7 @@ async function downloadTheme(themesDir: string, theme: Theme) {
try {
await Native.downloadTheme(themesDir, theme);
showToast(`Downloaded ${theme.name}!`, Toasts.Type.SUCCESS);
- } catch (err: any) {
+ } catch (err: unknown) {
logger.error(err);
showToast(`Failed to download ${theme.name}! (check console)`, Toasts.Type.FAILURE);
}
@@ -121,34 +121,36 @@ export const ThemeInfoModal: React.FC = ({ author, theme, .
)}
Source
- openModal(modalProps => (
-
-
- Theme Source
-
-
-
-
-
-
-
- modalProps.onClose()}
- >
- Close
-
- {
- Clipboard.copy(themeContent);
- showToast("Copied to Clipboard", Toasts.Type.SUCCESS);
- }}>Copy to Clipboard
-
-
- ))}
+ openModal(modalProps => (
+
+
+ Theme Source
+
+
+
+
+
+
+
+ modalProps.onClose()}
+ >
+ Close
+
+ {
+ Clipboard.copy(themeContent);
+ showToast("Copied to Clipboard", Toasts.Type.SUCCESS);
+ }}>Copy to Clipboard
+
+
+ ))}
>
View Theme Source
@@ -190,6 +192,7 @@ export const ThemeInfoModal: React.FC = ({ author, theme, .
color={Button.Colors.GREEN}
look={Button.Looks.OUTLINED}
className={classes("vce-button", Margins.right8)}
+ disabled={!theme.content || theme.id === "preview"}
onClick={async () => {
const themesDir = await VencordNative.themes.getThemesDir();
const exists = await Native.themeExists(themesDir, theme);
diff --git a/src/equicordplugins/themeLibrary/components/ThemeTab.tsx b/src/equicordplugins/themeLibrary/components/ThemeTab.tsx
index b1917b9f..47a5058f 100644
--- a/src/equicordplugins/themeLibrary/components/ThemeTab.tsx
+++ b/src/equicordplugins/themeLibrary/components/ThemeTab.tsx
@@ -6,34 +6,27 @@
import "./styles.css";
-import { generateId } from "@api/Commands";
import * as DataStore from "@api/DataStore";
import { Settings } from "@api/Settings";
-import { CodeBlock } from "@components/CodeBlock";
import { ErrorCard } from "@components/ErrorCard";
import { OpenExternalIcon } from "@components/Icons";
import { SettingsTab, wrapTab } from "@components/VencordSettings/shared";
-import { proxyLazy } from "@utils/lazy";
+import { fetchUserProfile } from "@utils/discord";
import { Logger } from "@utils/Logger";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { findByPropsLazy } from "@webpack";
-import { Button, Card, FluxDispatcher, Forms, Parser, React, SearchableSelect, TabBar, TextArea, TextInput, Toasts, useEffect, UserStore, UserUtils, useState } from "@webpack/common";
-import { User } from "discord-types/general";
-import { Constructor } from "type-fest";
+import { Button, Forms, React, SearchableSelect, Switch, TabBar, Text, TextArea, TextInput, Toasts, useEffect, UserProfileStore, UserStore, useState, useStateFromStores } from "@webpack/common";
-import { SearchStatus, TabItem, Theme, ThemeLikeProps } from "../types";
+import { Contributor, SearchStatus, TabItem, Theme, ThemeLikeProps } from "../types";
import { isAuthorized } from "../utils/auth";
-import { LikesComponent } from "./LikesComponent";
-import { ThemeInfoModal } from "./ThemeInfoModal";
+import { ThemeCard } from "./ThemeCard";
const InputStyles = findByPropsLazy("inputDefault", "inputWrapper", "error");
-const UserRecord: Constructor> = proxyLazy(() => UserStore.getCurrentUser().constructor) as any;
-const API_URL = "https://themes-delta.vercel.app/api";
-
-const logger = new Logger("ThemeLibrary", "#e5c890");
+export const apiUrl = "https://themes-delta.vercel.app/api";
+export const logger = new Logger("ThemeLibrary", "#e5c890");
export async function fetchAllThemes(): Promise {
const response = await themeRequest("/themes");
@@ -41,14 +34,14 @@ export async function fetchAllThemes(): Promise {
const themes: Theme[] = Object.values(data);
themes.forEach(theme => {
if (!theme.source) {
- theme.source = `${API_URL}/${theme.name}`;
+ theme.source = `${apiUrl}/${theme.name}`;
}
});
return themes.sort((a, b) => new Date(b.release_date).getTime() - new Date(a.release_date).getTime());
}
export async function themeRequest(path: string, options: RequestInit = {}) {
- return fetch(API_URL + path, {
+ return fetch(apiUrl + path, {
...options,
headers: {
...options.headers,
@@ -56,34 +49,6 @@ export async function themeRequest(path: string, options: RequestInit = {}) {
});
}
-function makeDummyUser(user: { username: string; id?: string; avatar?: string; }) {
- const newUser = new UserRecord({
- username: user.username,
- id: user.id ?? generateId(),
- avatar: user.avatar,
- bot: true,
- });
- FluxDispatcher.dispatch({
- type: "USER_UPDATE",
- user: newUser,
- });
- return newUser;
-}
-
-const themeTemplate = `/**
-* @name [Theme name]
-* @author [Your name]
-* @description [Your Theme Description]
-* @version [Your Theme Version]
-* @donate [Optionally, your Donation Link]
-* @tags [Optionally, tags that apply to your theme]
-* @invite [Optionally, your Support Server Invite]
-* @source [Optionally, your source code link]
-*/
-
-/* Your CSS goes here */
-`;
-
const SearchTags = {
[SearchStatus.THEME]: "THEME",
[SearchStatus.SNIPPET]: "SNIPPET",
@@ -92,7 +57,6 @@ const SearchTags = {
[SearchStatus.LIGHT]: "LIGHT",
};
-
function ThemeTab() {
const [themes, setThemes] = useState([]);
const [filteredThemes, setFilteredThemes] = useState([]);
@@ -102,12 +66,11 @@ function ThemeTab() {
const [hideWarningCard, setHideWarningCard] = useState(Settings.plugins.ThemeLibrary.hideWarningCard);
const [loading, setLoading] = useState(true);
- const getUser = (id: string, username: string) => UserUtils.getUser(id) ?? makeDummyUser({ username, id });
const onSearch = (query: string) => setSearchValue(prev => ({ ...prev, value: query }));
const onStatusChange = (status: SearchStatus) => setSearchValue(prev => ({ ...prev, status }));
const themeFilter = (theme: Theme) => {
- const enabled = themeLinks.includes(`${API_URL}/${theme.name}`);
+ const enabled = themeLinks.includes(`${apiUrl}/${theme.name}`);
const tags = new Set(theme.tags.map(tag => tag?.toLowerCase()));
if (!enabled && searchValue.status === SearchStatus.ENABLED) return false;
@@ -177,15 +140,22 @@ function ThemeTab() {
<>
{loading ? (
+
Getting the latest themes...
+
Loading themes...
+ }}> This won't take long!
+
+
) : (
<>
{hideWarningCard ? null : (
@@ -204,7 +174,7 @@ function ThemeTab() {
look={Button.Looks.FILLED}
className={classes(Margins.top16, "vce-warning-button")}
>Hide
-
+
)}
{themes.slice(0, 2).map((theme: Theme) => (
-
-
- {theme.name}
-
-
- {Parser.parse(theme.description)}
-
-
-
- {theme.tags && (
-
- {theme.tags.map(tag => (
-
- {tag}
-
- ))}
-
- )}
-
- {themeLinks.includes(`${API_URL}/${theme.name}`) ? (
-
{
- const onlineThemeLinks = themeLinks.filter(x => x !== `${API_URL}/${theme.name}`);
- setThemeLinks(onlineThemeLinks);
- Vencord.Settings.themeLinks = onlineThemeLinks;
- }}
- size={Button.Sizes.MEDIUM}
- color={Button.Colors.RED}
- look={Button.Looks.FILLED}
- className={Margins.right8}
- >
- Remove Theme
-
- ) : (
-
{
- const onlineThemeLinks = [...themeLinks, `${API_URL}/${theme.name}`];
-
- const requiresThemeAttributes = theme.requiresThemeAttributes ?? false;
-
- if (requiresThemeAttributes && !Settings.plugins.ThemeAttributes.enabled) {
- openModal(modalProps => (
-
-
- Hold on!
-
-
-
-
-
This theme requires the ThemeAttributes plugin to work properly!
-
- Do you want to enable it?
-
-
-
-
-
- {
- Settings.plugins.ThemeAttributes.enabled = true;
- modalProps.onClose();
- setThemeLinks(onlineThemeLinks);
- Vencord.Settings.themeLinks = onlineThemeLinks;
- }}
- >
- Enable Plugin
-
- modalProps.onClose()}
- >
- Cancel
-
-
-
- ));
- } else {
- setThemeLinks(onlineThemeLinks);
- Vencord.Settings.themeLinks = onlineThemeLinks;
- }
- }}
- size={Button.Sizes.MEDIUM}
- color={Button.Colors.GREEN}
- look={Button.Looks.FILLED}
- className={Margins.right8}
- >
- Add Theme
-
- )}
-
{
- const authors = Array.isArray(theme.author)
- ? await Promise.all(theme.author.map(author => getUser(author.discord_snowflake, author.discord_name)))
- : [await getUser(theme.author.discord_snowflake, theme.author.discord_name)];
-
- openModal(props => );
- }}
- size={Button.Sizes.MEDIUM}
- color={Button.Colors.BRAND}
- look={Button.Looks.FILLED}
- >
- Theme Info
-
-
{
- const content = window.atob(theme.content);
- const metadata = content.match(/\/\*\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+\//g)?.[0] || "";
- const source = metadata.match(/@source\s+(.+)/)?.[1] || "";
-
- if (source) {
- VencordNative.native.openExternal(source);
- } else {
- VencordNative.native.openExternal(`${API_URL}/${theme.name}`);
- }
- }}
- size={Button.Sizes.MEDIUM}
- color={Button.Colors.LINK}
- look={Button.Looks.LINK}
- style={{ display: "flex", alignItems: "center", justifyContent: "center" }}
- >
- View Source
-
-
-
-
-
+
))}
- {filteredThemes.map((theme: Theme) => (
-
(
+
+ )) :
-
- {theme.name}
-
-
- {Parser.parse(theme.description)}
-
-
-
-
- {theme.tags && (
-
- {theme.tags.map(tag => (
-
- {tag}
-
- ))}
-
- )}
-
- {themeLinks.includes(`${API_URL}/${theme.name}`) ? (
-
{
- const onlineThemeLinks = themeLinks.filter(x => x !== `${API_URL}/${theme.name}`);
- setThemeLinks(onlineThemeLinks);
- Vencord.Settings.themeLinks = onlineThemeLinks;
- }}
- size={Button.Sizes.MEDIUM}
- color={Button.Colors.RED}
- look={Button.Looks.FILLED}
- className={Margins.right8}
- >
- Remove Theme
-
- ) : (
-
{
- const onlineThemeLinks = [...themeLinks, `${API_URL}/${theme.name}`];
-
- const requiresThemeAttributes = theme.requiresThemeAttributes ?? false;
-
- if (requiresThemeAttributes && !Settings.plugins.ThemeAttributes.enabled) {
- openModal(modalProps => (
-
-
- Hold on!
-
-
-
-
-
This theme requires the ThemeAttributes plugin to work properly!
-
- Do you want to enable it?
-
-
-
-
-
- {
- Settings.plugins.ThemeAttributes.enabled = true;
- modalProps.onClose();
- setThemeLinks(onlineThemeLinks);
- Vencord.Settings.themeLinks = onlineThemeLinks;
- }}
- >
- Enable Plugin
-
- modalProps.onClose()}
- >
- Cancel
-
-
-
- ));
- } else {
- setThemeLinks(onlineThemeLinks);
- Vencord.Settings.themeLinks = onlineThemeLinks;
- }
- }}
- size={Button.Sizes.MEDIUM}
- color={Button.Colors.GREEN}
- look={Button.Looks.FILLED}
- className={Margins.right8}
- >
- Add Theme
-
- )}
-
{
- const authors = Array.isArray(theme.author)
- ? await Promise.all(theme.author.map(author => getUser(author.discord_snowflake, author.discord_name)))
- : [await getUser(theme.author.discord_snowflake, theme.author.discord_name)];
-
- openModal(props => );
- }}
- size={Button.Sizes.MEDIUM}
- color={Button.Colors.BRAND}
- look={Button.Looks.FILLED}
- >
- Theme Info
-
-
-
{
- const content = window.atob(theme.content);
- const metadata = content.match(/\/\*\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+\//g)?.[0] || "";
- const source = metadata.match(/@source\s+(.+)/)?.[1] || "";
-
- if (source) {
- VencordNative.native.openExternal(source);
- } else {
- VencordNative.native.openExternal(`${API_URL}/${theme.name}`);
- }
- }}
- size={Button.Sizes.MEDIUM}
- color={Button.Colors.LINK}
- look={Button.Looks.LINK}
- style={{ display: "flex", alignItems: "center", justifyContent: "center" }}
- >
- View Source
-
-
-
-
-
- ))}
+ justifyContent: "center",
+ alignItems: "center",
+ }}>
+
No theme found.
+
Try narrowing your search down.
+
+ }
>)}
>
-
+
);
}
+// rework this!
function SubmitThemes() {
- const [themeContent, setContent] = useState("");
- const handleChange = (v: string) => setContent(v);
+ const currentUser = UserStore.getCurrentUser();
+ const currentUserProfile = useStateFromStores([UserProfileStore], () => UserProfileStore.getUserProfile(currentUser.id));
+
+ if (!currentUserProfile && currentUser.id) fetchUserProfile(currentUser.id);
+
+ const [theme, setTheme] = useState({
+ title: "",
+ description: "",
+ content: "",
+ version: "",
+ type: "theme",
+ attribution: {
+ include_github: false,
+ sourceLink: "",
+ donationLink: "",
+ contributors: [{
+ username: currentUser.username,
+ id: currentUser.id,
+ avatar: currentUser.getAvatarURL(),
+ github_username: currentUserProfile.connectedAccounts.find(x => x.type === "github")?.name ?? null,
+ }],
+ // isAllowedToRedistribute: false,
+ },
+ screenshotMetadata: {
+ data: "",
+ name: "",
+ size: 0,
+ }
+ });
+ const [valid, setValid] = useState(false);
+
+ const handleImageChange = (e: React.ChangeEvent) => {
+ if (!e.target.files) return;
+ const image = e.target.files[0];
+ const reader = new FileReader();
+
+ reader.onloadend = () => {
+ const imgElement = new Image();
+ imgElement.src = reader.result as string;
+
+ imgElement.onload = () => {
+ const canvas = document.createElement("canvas");
+ const maxWidth = 800;
+ const scaleSize = maxWidth / imgElement.width;
+
+ canvas.width = maxWidth;
+ canvas.height = imgElement.height * scaleSize;
+
+ const ctx = canvas.getContext("2d");
+ if (ctx) {
+ ctx.drawImage(imgElement, 0, 0, canvas.width, canvas.height);
+
+ const resizedBase64String = canvas.toDataURL("image/jpeg", 0.7);
+
+ handleChange("screenshotMetadata", {
+ data: resizedBase64String,
+ name: image.name,
+ size: image.size,
+ });
+ setValid(!!theme.title && theme.content.length >= 50 && !!theme.version && !!theme.description);
+ }
+ };
+ };
+
+ reader.readAsDataURL(image);
+ };
+
+ const handleChange = (p, v) => {
+ setTheme(prevTheme => {
+ const [first, ...rest] = p.split(".");
+
+ const updateNestedObject = (obj, keys, value) => {
+ const key = keys[0];
+ if (keys.length === 1) {
+ return { ...obj, [key]: value };
+ }
+
+ return {
+ ...obj,
+ [key]: updateNestedObject(obj[key] || {}, keys.slice(1), value)
+ };
+ };
+
+ return updateNestedObject(prevTheme, [first, ...rest], v);
+ });
+ };
+
+
+ const setContributors = contributors => {
+ setTheme(prevTheme => ({
+ ...prevTheme,
+ attribution: {
+ ...prevTheme.attribution,
+ contributors: [prevTheme.attribution.contributors[0], ...contributors]
+ }
+ }));
+ };
+
+ useEffect(() => {
+ logger.debug(valid);
+ }, [theme]);
return (
- Theme Guidelines
+ Submission Guidelines
-
- Follow the formatting for your CSS to get credit for your theme. You can find the template below.
-
-
- (your theme will be reviewed and can take up to 24 hours to be approved)
-
-
-
-
+
+
+ Do not distribute themes or snippets that aren't yours.
+ Your submission must be at least 50 characters long.
+ Do not submit low-quality themes or snippets.
+
+
+ Fields with * are required!
+
+
+
+
+
+
+ {theme.type.charAt(0).toUpperCase()}{theme.type.slice(1)} Name *
+
+ { }}
+ onBlur={e => {
+ const v = e.target.value;
+ handleChange("title", v);
+ setValid(!!theme.title && !!v && theme.content.length >= 50 && !!theme.version && !!theme.description && !!theme.screenshotMetadata.data);
+ }}
+ placeholder={`My awesome ${theme.type}`}
+ rows={1}
+ />
+
+
+
+ {theme.type.charAt(0).toUpperCase()}{theme.type.slice(1)} Version *
+
+ { }}
+ onBlur={e => {
+ const v = e.target.value;
+ handleChange("version", v);
+ setValid(!!theme.title && theme.content.length >= 50 && !!v && !!theme.description && !!theme.screenshotMetadata.data);
+ }}
+ placeholder="v1.0.0"
+ rows={1}
+ />
+
+
+
- Submit Themes
-
-
- If you plan on updating your theme / snippet frequently, consider using an @import
instead!
-
-
-
-
-
{
- if (!(await isAuthorized())) return;
+ {theme.type.charAt(0).toUpperCase()}{theme.type.slice(1)} Description *
+
+ Try to keep your description short and to the point.
+
+