diff --git a/.gitattributes b/.gitattributes
index a6344aac8c09253b3b630fb776ae94478aa0275b..9a0b23b2bb2caf8abd45defa7555b0e2941ed423 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
*.zip filter=lfs diff=lfs merge=lfs -text
*.zst filter=lfs diff=lfs merge=lfs -text
*tfevents* filter=lfs diff=lfs merge=lfs -text
+public/badge-editor.png filter=lfs diff=lfs merge=lfs -text
+public/banner.png filter=lfs diff=lfs merge=lfs -text
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..a78bb4ae2a299508cdacdbb9e3adb89ac746c2bb
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,31 @@
+# Dockerfile
+
+# Use an official Node.js runtime as the base image
+FROM node:18
+
+USER 1000
+
+# Set the working directory in the container
+WORKDIR /usr/src/app
+
+# Copy package.json and package-lock.json to the container
+COPY --chown=1000 package.json package-lock.json ./
+
+# Install dependencies
+RUN npm install
+
+VOLUME /data
+
+# Copy the rest of the application files to the container
+COPY --chown=1000 . .
+RUN chmod +x entrypoint.sh
+
+# Build the Next.js application for production
+# RUN npm run build
+
+# Expose the application port (assuming your app runs on port 3000)
+EXPOSE 3000
+
+RUN npm run build
+
+CMD ["npm", "run", "start"]
\ No newline at end of file
diff --git a/README.md b/README.md
index 3e591aa0cc06836bd62d61550af4b71a428d315d..9c17535f37af4edbebf2690e27634532f706497f 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,13 @@
---
title: Discotools
-emoji: 🐢
-colorFrom: gray
-colorTo: yellow
+emoji: ✨
+colorFrom: blue
+colorTo: pink
sdk: docker
pinned: false
+app_port: 3000
+short_description: Generate discord role icon and badge
license: mit
---
-Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
+## This is an old project.
diff --git a/assets/images/avatar_bot.png b/assets/images/avatar_bot.png
new file mode 100644
index 0000000000000000000000000000000000000000..f7963ac2e0e2eedb6afa3aba6fb415236d7b726e
Binary files /dev/null and b/assets/images/avatar_bot.png differ
diff --git a/assets/images/avatars/default-avatar-2.svg b/assets/images/avatars/default-avatar-2.svg
new file mode 100644
index 0000000000000000000000000000000000000000..87307cdebfc4b746dbdc274e4d241832552596dd
--- /dev/null
+++ b/assets/images/avatars/default-avatar-2.svg
@@ -0,0 +1,79 @@
+
+
\ No newline at end of file
diff --git a/assets/images/avatars/default-avatar.svg b/assets/images/avatars/default-avatar.svg
new file mode 100644
index 0000000000000000000000000000000000000000..1c1495b029a7a4d0971e0d1cd45ea84978dd2be4
--- /dev/null
+++ b/assets/images/avatars/default-avatar.svg
@@ -0,0 +1,85 @@
+
+
\ No newline at end of file
diff --git a/assets/images/banner_footer.svg b/assets/images/banner_footer.svg
new file mode 100644
index 0000000000000000000000000000000000000000..b4ddf91d12968eea0c5ecae66cd40e2789d24c12
--- /dev/null
+++ b/assets/images/banner_footer.svg
@@ -0,0 +1,118 @@
+
+
\ No newline at end of file
diff --git a/assets/images/clyde/affraid.svg b/assets/images/clyde/affraid.svg
new file mode 100644
index 0000000000000000000000000000000000000000..dd74e3bddc28ac14fb96a87f3c03dc85e8f8547a
--- /dev/null
+++ b/assets/images/clyde/affraid.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/images/clyde/wave.svg b/assets/images/clyde/wave.svg
new file mode 100644
index 0000000000000000000000000000000000000000..59c38b367f7656d076a0541cf6a9357855b6a4e0
--- /dev/null
+++ b/assets/images/clyde/wave.svg
@@ -0,0 +1,39 @@
+
diff --git a/assets/images/color-picker.svg b/assets/images/color-picker.svg
new file mode 100644
index 0000000000000000000000000000000000000000..c307b3727f15e31c5f0c81d80db8b50f44a2632f
--- /dev/null
+++ b/assets/images/color-picker.svg
@@ -0,0 +1,9 @@
+
+
\ No newline at end of file
diff --git a/assets/images/editor/transparent_bg.svg b/assets/images/editor/transparent_bg.svg
new file mode 100644
index 0000000000000000000000000000000000000000..4cdb086137c7f3391dcd693548defd43ee640fa9
--- /dev/null
+++ b/assets/images/editor/transparent_bg.svg
@@ -0,0 +1,11 @@
+
+
\ No newline at end of file
diff --git a/assets/images/header/ananas.svg b/assets/images/header/ananas.svg
new file mode 100644
index 0000000000000000000000000000000000000000..cdb82ec56d6d171c6e67f541b61b8b69e70b0cf6
--- /dev/null
+++ b/assets/images/header/ananas.svg
@@ -0,0 +1,32 @@
+
+
\ No newline at end of file
diff --git a/assets/images/header/background.svg b/assets/images/header/background.svg
new file mode 100644
index 0000000000000000000000000000000000000000..8c609437dc3d9e38f685dfd81dc40dcf0d42ae9b
--- /dev/null
+++ b/assets/images/header/background.svg
@@ -0,0 +1,31 @@
+
+
\ No newline at end of file
diff --git a/assets/images/header/icons-slider/crown.svg b/assets/images/header/icons-slider/crown.svg
new file mode 100644
index 0000000000000000000000000000000000000000..f750b10365a1be9aaac57df1b3fe41cb0f2cb6bb
--- /dev/null
+++ b/assets/images/header/icons-slider/crown.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/images/header/icons-slider/hammer.svg b/assets/images/header/icons-slider/hammer.svg
new file mode 100644
index 0000000000000000000000000000000000000000..e23568493a07f5b57ccf0506e12ed898e7fbf18f
--- /dev/null
+++ b/assets/images/header/icons-slider/hammer.svg
@@ -0,0 +1,8 @@
+
+
\ No newline at end of file
diff --git a/assets/images/header/icons-slider/leaf.svg b/assets/images/header/icons-slider/leaf.svg
new file mode 100644
index 0000000000000000000000000000000000000000..deb90e564053c1d0fdbf798cf76ab9c1b8ede9c6
--- /dev/null
+++ b/assets/images/header/icons-slider/leaf.svg
@@ -0,0 +1,9 @@
+
+
\ No newline at end of file
diff --git a/assets/images/header/icons-slider/partner.svg b/assets/images/header/icons-slider/partner.svg
new file mode 100644
index 0000000000000000000000000000000000000000..dbcbcda07d874b9ba7f49495deb2a7a518997726
--- /dev/null
+++ b/assets/images/header/icons-slider/partner.svg
@@ -0,0 +1,9 @@
+
+
\ No newline at end of file
diff --git a/assets/images/header/icons-slider/shape.svg b/assets/images/header/icons-slider/shape.svg
new file mode 100644
index 0000000000000000000000000000000000000000..d5fe4affbb7f550f694fef487305671e9d334226
--- /dev/null
+++ b/assets/images/header/icons-slider/shape.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/images/header/icons-slider/shield.svg b/assets/images/header/icons-slider/shield.svg
new file mode 100644
index 0000000000000000000000000000000000000000..dfc433aa7fb08e5cbc5b965260a752e3a42be937
--- /dev/null
+++ b/assets/images/header/icons-slider/shield.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/assets/images/header/icons-slider/star.svg b/assets/images/header/icons-slider/star.svg
new file mode 100644
index 0000000000000000000000000000000000000000..e60d6d543f63c1750e5e63b82a1afb9e0bc59f7e
--- /dev/null
+++ b/assets/images/header/icons-slider/star.svg
@@ -0,0 +1,9 @@
+
+
\ No newline at end of file
diff --git a/assets/images/header/pizza.svg b/assets/images/header/pizza.svg
new file mode 100644
index 0000000000000000000000000000000000000000..b60510efe3a1aedb08ce06c534fb4b22c04f9301
--- /dev/null
+++ b/assets/images/header/pizza.svg
@@ -0,0 +1,31 @@
+
+
\ No newline at end of file
diff --git a/assets/images/header/search.svg b/assets/images/header/search.svg
new file mode 100644
index 0000000000000000000000000000000000000000..0e91176c63a6acd7f308a7e429493aed52493f23
--- /dev/null
+++ b/assets/images/header/search.svg
@@ -0,0 +1,9 @@
+
+
\ No newline at end of file
diff --git a/assets/images/header/stars.svg b/assets/images/header/stars.svg
new file mode 100644
index 0000000000000000000000000000000000000000..9db94556df352a85e008024595787e949a9447f8
--- /dev/null
+++ b/assets/images/header/stars.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/assets/images/premium-modal/modal-illustration.webp b/assets/images/premium-modal/modal-illustration.webp
new file mode 100644
index 0000000000000000000000000000000000000000..0e913c2cda34828b5161688d3f0c3962117bdbf5
Binary files /dev/null and b/assets/images/premium-modal/modal-illustration.webp differ
diff --git a/components/clyde/clyde.tsx b/components/clyde/clyde.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..82cac0335c7a5a30a928637abbbdf2163d19bd04
--- /dev/null
+++ b/components/clyde/clyde.tsx
@@ -0,0 +1,296 @@
+import { useState } from "react";
+
+export const Clyde = () => {
+ const [isHovered, setIsHovered] = useState(false);
+ return (
+
+ );
+};
diff --git a/components/clyde/message.tsx b/components/clyde/message.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..97028e177318985c09f91a582406c18d82901aa9
--- /dev/null
+++ b/components/clyde/message.tsx
@@ -0,0 +1,54 @@
+import classNames from "classnames";
+import { useState } from "react";
+import { useMount, useUpdateEffect } from "react-use";
+
+import { Clyde } from "./clyde";
+
+export const ClydeMessage = ({
+ message,
+ auto = false,
+ onClick,
+}: {
+ message: string;
+ auto?: boolean;
+ onClick: any;
+}) => {
+ const [show, setShow] = useState(false);
+
+ useMount(() => {
+ if (auto) {
+ setTimeout(() => {
+ setShow(true);
+ }, 2000);
+ }
+ });
+
+ useUpdateEffect(() => {
+ if (auto && show) {
+ setTimeout(() => {
+ setShow(false);
+ }, 5000);
+ }
+ }, [show]);
+
+ return (
+
+ );
+};
diff --git a/components/collapse/index.tsx b/components/collapse/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..8cc558c317cc658992692d8d8170ac64b0304f95
--- /dev/null
+++ b/components/collapse/index.tsx
@@ -0,0 +1,47 @@
+import { ChevronDownIcon } from "@heroicons/react/solid";
+import classNames from "classnames";
+import { useState } from "react";
+import { useIntl } from "react-intl";
+
+export const Collapse = ({
+ children,
+ title,
+ className,
+ open: defaultOpen = false,
+ onOpenClassName,
+ parentClassName,
+}: {
+ children: React.ReactNode;
+ title: string | React.ReactNode;
+ className?: string;
+ open?: boolean;
+ onOpenClassName?: string;
+ parentClassName?: string;
+}) => {
+ const [open, setOpen] = useState(defaultOpen);
+ const intl = useIntl();
+
+ return (
+
+
setOpen(!open)}
+ >
+ {typeof title === "string" ? intl.formatMessage({ id: title }) : title}
+
+
+
+
+ {open && children}
+
+ );
+};
diff --git a/components/color-picker/index.tsx b/components/color-picker/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..587fb4accbe15d185af2fcb1212d71427d5877c3
--- /dev/null
+++ b/components/color-picker/index.tsx
@@ -0,0 +1,104 @@
+import { useState, useRef } from "react";
+import { useClickAway } from "react-use";
+import Picker, { useColorPicker } from "react-best-gradient-color-picker";
+import classNames from "classnames";
+
+import ColorPickerIcon from "@/assets/images/color-picker.svg";
+import { IconType } from "@/types/editor";
+import Image from "next/image";
+
+export const ColorPicker = ({
+ value,
+ data,
+ gradients = true,
+ full = false,
+ onChange,
+ className,
+}: {
+ value?: string;
+ data?: any;
+ gradients?: boolean;
+ full?: boolean;
+ onChange: (c: any, datas: any) => void;
+ className?: string;
+}) => {
+ const [open, setOpen] = useState(false);
+ const ref = useRef(null);
+
+ const [color, setColor] = useState(value ?? "#fff");
+ const { getGradientObject } = useColorPicker(color, setColor);
+
+ useClickAway(ref, () => setOpen(false));
+
+ const renderBackground = (data: any) => {
+ if (data?.gradient?.enabled) {
+ if (data?.gradient?.type === "linearGradient") {
+ return {
+ background: `linear-gradient(${data?.gradient?.colours
+ ?.map((e: any) => e.value)
+ .join(", ")})`,
+ };
+ } else {
+ return {
+ background: `radial-gradient(${data?.gradient?.colours
+ ?.map((e: any) => e.value)
+ .join(", ")})`,
+ };
+ }
+ }
+ return { backgroundColor: data?.colour ?? data?.color ?? "#fff" };
+ };
+
+ return (
+
+
+ {open && (
+
{
+ setColor(c);
+ const datas = getGradientObject();
+ onChange(c, datas);
+ }}
+ className={`${
+ data?.gradient?.enabled
+ ? "color__picker_discotools--gradient"
+ : "color__picker_discotools"
+ } ${gradients ? "" : "color__picker without-gradients"}`}
+ />
+ )}
+
+
setOpen(true)}
+ >
+
+
+
+ );
+};
diff --git a/components/editor-badge/badge-editor.constants.ts b/components/editor-badge/badge-editor.constants.ts
new file mode 100644
index 0000000000000000000000000000000000000000..08840253ff20d9ff7494b5cdb38da62ac2190858
--- /dev/null
+++ b/components/editor-badge/badge-editor.constants.ts
@@ -0,0 +1,39 @@
+import { BadgeType } from "types/badge";
+
+export const DEFAULT_VALUE: BadgeType = {
+ colour: "#5865F1",
+ gradient: {
+ enabled: false,
+ colours: [],
+ angle: 0,
+ type: "linear-gradient",
+ },
+ text: {
+ value: "DISCOTOOLS.XYZ",
+ colour: "#ffffff",
+ gradient: {
+ enabled: false,
+ colours: [],
+ angle: 0,
+ type: "linear-gradient",
+ },
+ },
+ icon: {
+ component: "King",
+ position: "left",
+ enabled: true,
+ colour: "#ffffff",
+ },
+ shinyEffect: false,
+ type: "circle",
+ radius: 40,
+ fontFamily: "Montserrat",
+ fontWeight: "700",
+ letterSpacing: 0,
+ border: {
+ width: 0,
+ colour: "#ffffff",
+ },
+};
+
+export const RATIONAL_BADGE_WIDTH = 32;
diff --git a/components/editor-badge/comps/badge.tsx b/components/editor-badge/comps/badge.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..d0f8f77fda5e790701755977a6a6bbcb16a91cd8
--- /dev/null
+++ b/components/editor-badge/comps/badge.tsx
@@ -0,0 +1,163 @@
+import { forwardRef, useEffect, useRef, useState } from "react";
+
+import { BadgeType } from "@/types/badge";
+import { FONT_FAMILY } from "@/components/font-family/font-family.constants";
+
+import { RATIONAL_BADGE_WIDTH } from "../badge-editor.constants";
+import { Icons } from "@/components/svg/icons";
+import { IconItem } from "@/types/editor";
+import { BADGE_COMPONENTS } from "@/components/svg/badges";
+import classNames from "classnames";
+import { ShineEffect } from "./shine_effect";
+
+export const Badge = forwardRef(
+ (
+ {
+ badge,
+ size = "medium",
+ }: {
+ badge: BadgeType;
+ size?: "medium" | "small";
+ },
+ ref
+ ) => {
+ const textRef = useRef(null);
+ const svgRef = useRef(null);
+ const badgeContentRef = useRef(null);
+ const [viewBox, setViewBox] = useState("0 0 24 24");
+
+ useEffect(() => {
+ if (!textRef.current) return;
+ if (!badgeContentRef.current) return;
+
+ const textWidth = textRef.current.getBoundingClientRect().width;
+
+ let newBadgeWidth = RATIONAL_BADGE_WIDTH;
+ if (textWidth > RATIONAL_BADGE_WIDTH) {
+ newBadgeWidth = Math.ceil(textWidth / 32) * 32;
+ }
+ if (
+ (findShape?.autoResize || badge.type === "circle") &&
+ newBadgeWidth - textWidth < (badge.type === "circle" ? 6 : 3)
+ )
+ newBadgeWidth += 32;
+ badgeContentRef.current.style.width = `${newBadgeWidth}px`;
+ }, [
+ badge.text,
+ badge.fontFamily,
+ badge.letterSpacing,
+ badge.fontWeight,
+ badge.type,
+ badge?.icon?.component,
+ badge?.icon?.enabled,
+ badge?.icon?.position,
+ ]);
+
+ const fontFamily = FONT_FAMILY?.find((f) => f.label === badge?.fontFamily);
+
+ const defaultStyle = {
+ letterSpacing: badge?.letterSpacing,
+ fontWeight: badge?.fontWeight,
+ };
+
+ const findIcon = Icons?.find(
+ (i: IconItem) => badge?.icon?.component === i.name
+ );
+ const findShape = BADGE_COMPONENTS?.find(
+ (b: any) => b.name === badge?.type
+ );
+
+ useEffect(() => {
+ return setViewBox(
+ svgRef?.current?.getAttribute("viewBox") ?? "0 0 200 200"
+ );
+ }, [badge?.icon]);
+
+ const IconComponent = findIcon?.component as any;
+ const ShapeComponent = findShape?.component as any;
+
+ return (
+
+ {findShape?.component && (
+
+ )}
+ {badge?.shinyEffect && (
+ <>
+
+
+ >
+ )}
+
+
+ {badge?.icon?.enabled &&
+ badge?.icon?.position?.includes("left") &&
+ findIcon?.component &&
+ badge?.icon?.component && (
+
+ )}
+
+ {badge?.text?.value && (
+
+ {badge?.text?.value}
+
+ )}
+ {badge?.icon?.enabled &&
+ badge?.icon?.position?.includes("right") &&
+ findIcon?.component &&
+ badge?.icon?.component && (
+
+ )}
+
+
+ {findShape?.component && (
+
+ )}
+
+ );
+ }
+);
diff --git a/components/editor-badge/comps/download_button.tsx b/components/editor-badge/comps/download_button.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..94b3b9ea8c9f340c1c56dec1d76f7a89acab509f
--- /dev/null
+++ b/components/editor-badge/comps/download_button.tsx
@@ -0,0 +1,60 @@
+import { useRef, useState } from "react";
+import { ChevronDownIcon } from "@heroicons/react/solid";
+import classNames from "classnames";
+import { useClickAway } from "react-use";
+
+export const DownloadButton = ({
+ onSave,
+}: {
+ onSave: (e?: boolean) => void;
+}) => {
+ const [dropdown, setDropdown] = useState(false);
+ const ref = useRef(null);
+
+ useClickAway(ref, () => {
+ setDropdown(false);
+ });
+
+ return (
+
+
+
{
+ e.preventDefault();
+ e.stopPropagation();
+ setDropdown(!dropdown);
+ }}
+ >
+
+
+
+
+
+
+ );
+};
diff --git a/components/editor-badge/comps/header/comps/icons-slider.tsx b/components/editor-badge/comps/header/comps/icons-slider.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..59954f59e688c31750ff1391a30988c6190bfec2
--- /dev/null
+++ b/components/editor-badge/comps/header/comps/icons-slider.tsx
@@ -0,0 +1,152 @@
+import { useRef, useState } from "react";
+import { useInterval } from "react-use";
+import { motion } from "framer-motion";
+import Image from "next/image";
+
+import { sleep } from "utils";
+import HammerIcon from "assets/images/header/icons-slider/hammer.svg";
+import ShieldIcon from "assets/images/header/icons-slider/shield.svg";
+import CrownIcon from "assets/images/header/icons-slider/crown.svg";
+import PartnerIcon from "assets/images/header/icons-slider/partner.svg";
+import StarIcon from "assets/images/header/icons-slider/star.svg";
+import LeafIcon from "assets/images/header/icons-slider/leaf.svg";
+
+const ICONS = [
+ HammerIcon,
+ ShieldIcon,
+ CrownIcon,
+ PartnerIcon,
+ StarIcon,
+ LeafIcon,
+];
+
+const icon = {
+ hidden: {
+ opacity: 0,
+ pathLength: 0,
+ fill: "rgba(255, 255, 255, 0)",
+ },
+ visible: {
+ opacity: 1,
+ pathLength: 1,
+ fill: "rgba(255, 255, 255, 1)",
+ },
+};
+
+export const IconsSlider = () => {
+ const [selected, setSelected] = useState(0);
+ const ref = useRef(null);
+
+ useInterval(async () => {
+ if (ref?.current) {
+ ref.current?.classList?.add("opacity-0", "-translate-x-full", "scale-50");
+ await sleep(300);
+ ref.current?.classList?.remove("-translate-x-full");
+ ref.current?.classList?.add("translate-x-full");
+
+ setSelected((prev) => (prev + 1) % ICONS.length);
+ // if (this.selected >= this.avatars.length - 1) {
+ // this.selected = 0;
+ // } else {
+ // this.selected += 1;
+ // }
+ await sleep(300);
+ ref.current?.classList?.remove(
+ "translate-x-full",
+ "scale-50",
+ "opacity-0"
+ );
+ }
+ }, 2000);
+
+ return (
+
+ {/*
*/}
+
+
+ {/* */}
+
+
+
+
+
+
+
+
+
+
+ //
+ //

+ //
+ //
+ );
+};
diff --git a/components/editor-badge/comps/header/form/advanced.tsx b/components/editor-badge/comps/header/form/advanced.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..856a085c9e25143f88d770e317256354e0678b88
--- /dev/null
+++ b/components/editor-badge/comps/header/form/advanced.tsx
@@ -0,0 +1,100 @@
+import { useState } from "react";
+import { FormattedMessage, useIntl } from "react-intl";
+
+import { FontFamilySelector } from "@/components/font-family";
+import { FontWeight } from "@/components/font-weight";
+
+import { Switch } from "@/components/switch";
+import { BadgeType } from "@/types/badge";
+import { PremiumOverlay } from "@/components/premium/overlay";
+
+import { Label } from "@/components/label";
+import { Input } from "@/components/input";
+
+export const AdvancedForm = ({
+ badge,
+ setBadge,
+}: {
+ badge: BadgeType;
+ setBadge: (b: BadgeType) => void;
+}) => {
+ const intl = useIntl();
+
+ return (
+
+ {badge?.type === "circle" && (
+
+
+ {
+ let radius: number = Number(newRadius);
+ if (radius > 100) radius = 100;
+ setBadge({ ...badge, radius: Number(radius) });
+ }}
+ />
+
+ )}
+
+
+
+
+
+
+
+
+
setBadge({ ...badge, fontFamily })}
+ />
+
+
+
+
+
+
setBadge({ ...badge, fontWeight })}
+ />
+
+
+
+
+
+
{
+ let letterSpacing: number = Number(newLetterSpacing);
+ if (letterSpacing > 100) letterSpacing = 100;
+ setBadge({
+ ...badge,
+ letterSpacing: Number(letterSpacing),
+ });
+ }}
+ />
+
+
+
+
+
+
+
+ setBadge({
+ ...badge,
+ shinyEffect: enabled,
+ })
+ }
+ />
+
+
+
+ );
+};
diff --git a/components/editor-badge/comps/header/form/main.tsx b/components/editor-badge/comps/header/form/main.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..d993cf465631563fde41f57c846d2fff51c2f51a
--- /dev/null
+++ b/components/editor-badge/comps/header/form/main.tsx
@@ -0,0 +1,267 @@
+import { useState } from "react";
+import { FormattedMessage, useIntl } from "react-intl";
+
+import { FontFamilySelector } from "@/components/font-family";
+import { FontWeight } from "@/components/font-weight";
+
+import { Switch } from "@/components/switch";
+import { BadgeType } from "@/types/badge";
+import { ColorPicker } from "components/color-picker";
+import { Label } from "@/components/label";
+import { Input } from "@/components/input";
+import classNames from "classnames";
+import { SelectShapes } from "../../select-shapes";
+import { IconPicker } from "@/components/icon-picker";
+
+export const MainForm = ({
+ badge,
+ setBadge,
+}: {
+ badge: BadgeType;
+ setBadge: (b: BadgeType) => void;
+}) => {
+ const intl = useIntl();
+
+ return (
+
+
+
+
+ {
+ setBadge({
+ ...badge,
+ text: { ...badge.text, value: value as string },
+ });
+ }}
+ />
+
+
+
+
+
+
+
+
+
+
{
+ let newBadge = { ...badge, stringColor: c };
+ if (c.includes("gradient")) {
+ const { colors, degrees } = datas;
+ const gradientType = c?.startsWith("linear")
+ ? "linearGradient"
+ : "radialGradient";
+ const anglePI =
+ Number(degrees ? degrees?.replace("deg", "") : 90) *
+ (Math.PI / 180);
+ const angleCoords = {
+ x1: Math.round(50 + Math.sin(anglePI) * 50) + "%",
+ y1: Math.round(50 + Math.cos(anglePI) * 50) + "%",
+ x2: Math.round(50 + Math.sin(anglePI + Math.PI) * 50) + "%",
+ y2: Math.round(50 + Math.cos(anglePI + Math.PI) * 50) + "%",
+ };
+
+ newBadge["gradient"] = {
+ ...newBadge.gradient,
+ enabled: true,
+ colours: colors,
+ angle: angleCoords,
+ type: gradientType,
+ };
+ } else {
+ newBadge = {
+ ...newBadge,
+ colour: c,
+ gradient: {
+ ...newBadge?.gradient,
+ enabled: false,
+ },
+ };
+ }
+ setBadge(newBadge);
+ }}
+ />
+
+
+
+
+
+
{
+ let newBadge = {
+ ...badge,
+ text: { ...badge.text, stringColor: c },
+ };
+ if (c.includes("gradient")) {
+ const { colors, degrees } = datas;
+ const gradientType = c?.startsWith("linear")
+ ? "linearGradient"
+ : "radialGradient";
+ const anglePI =
+ Number(degrees ? degrees?.replace("deg", "") : 90) *
+ (Math.PI / 180);
+ const angleCoords = {
+ x1: Math.round(50 + Math.sin(anglePI) * 50) + "%",
+ y1: Math.round(50 + Math.cos(anglePI) * 50) + "%",
+ x2: Math.round(50 + Math.sin(anglePI + Math.PI) * 50) + "%",
+ y2: Math.round(50 + Math.cos(anglePI + Math.PI) * 50) + "%",
+ };
+ newBadge["text"]["gradient"] = {
+ ...newBadge?.text?.gradient,
+ enabled: true,
+ colours: colors,
+ angle: angleCoords,
+ type: gradientType,
+ };
+ } else {
+ newBadge = {
+ ...newBadge,
+ text: {
+ ...newBadge?.text,
+ colour: c,
+ gradient: {
+ ...newBadge?.text?.gradient,
+ enabled: false,
+ },
+ },
+ };
+ }
+ setBadge(newBadge);
+ }}
+ />
+
+
+
+
+
+
+ setBadge({
+ ...badge,
+ icon: {
+ ...badge.icon,
+ position: badge?.icon?.position ?? "left",
+ enabled,
+ },
+ })
+ }
+ />
+
+
+
+
+
+
{
+ setBadge({
+ ...badge,
+ icon: {
+ ...badge?.icon,
+ position: badge?.icon?.position ?? "left",
+ colour: c,
+ stringColor: c,
+ },
+ });
+ }}
+ />
+
+
+
+
+
+
+
{
+ setBadge({
+ ...badge,
+ icon: {
+ ...badge?.icon,
+ position: badge?.icon?.position ?? "left",
+ enabled: true,
+ component,
+ },
+ });
+ }}
+ />
+
+
+
+
+
+
+
+ setBadge({
+ ...badge,
+ icon: { ...badge?.icon, position: "left" },
+ })
+ }
+ >
+ Left
+
+
+ setBadge({
+ ...badge,
+ icon: { ...badge?.icon, position: "right" },
+ })
+ }
+ >
+ Right
+
+
+ setBadge({
+ ...badge,
+ icon: { ...badge?.icon, position: "left-right" },
+ })
+ }
+ >
+ Left & Right
+
+
+
+
+
+
+ );
+};
diff --git a/components/editor-badge/comps/header/index.tsx b/components/editor-badge/comps/header/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..a357ce9be6e99baf7de765cec2140e9aba223584
--- /dev/null
+++ b/components/editor-badge/comps/header/index.tsx
@@ -0,0 +1,84 @@
+import Image from "next/image";
+
+import { Tag } from "components/tag";
+import { IconsSlider } from "./comps/icons-slider";
+
+// import { Editor } from "components/editor";
+// import { ArrayOfIcons } from "components/svg/icons";
+
+// import { Navigation } from "components/navigation";
+
+import Background from "assets/images/header/background.svg";
+import Stars from "assets/images/header/stars.svg";
+import Ananas from "assets/images/header/ananas.svg";
+import Pizza from "assets/images/header/pizza.svg";
+import { FormattedMessage, useIntl } from "react-intl";
+
+export const Header = ({ children }: any) => {
+ const intl = useIntl();
+ return (
+
+
+
+
+
+ NEW FEATURE AVAILABLE
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+
+
+ {/*
+
*/}
+
+ );
+};
diff --git a/components/editor-badge/comps/preview.tsx b/components/editor-badge/comps/preview.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..d2307a756dcaf080ca2e5e25b1622114ad9c2866
--- /dev/null
+++ b/components/editor-badge/comps/preview.tsx
@@ -0,0 +1,39 @@
+import { useMount, useUpdateEffect } from "react-use";
+import { domToPng } from "modern-screenshot";
+
+import { BadgeType } from "@/types/badge";
+import { useState } from "react";
+
+export const Preview = ({ badge }: { badge: BadgeType }) => {
+ const [badgeUrl, setBadgeUrl] = useState("");
+
+ useUpdateEffect(() => {
+ setTimeout(() => {
+ domToPng(
+ document.getElementById("discotools-selected-badge") as HTMLElement,
+ {
+ scale: 2,
+ }
+ ).then(async (dataUrl) => {
+ setBadgeUrl(dataUrl);
+ });
+ }, 190);
+ }, [badge]);
+
+ useMount(() => {
+ setTimeout(() => {
+ domToPng(
+ document.getElementById("discotools-selected-badge") as HTMLElement,
+ {
+ scale: 2,
+ }
+ ).then(async (dataUrl) => {
+ setBadgeUrl(dataUrl);
+ });
+ }, 100);
+ });
+
+ return (
+ {badgeUrl &&

}
+ );
+};
diff --git a/components/editor-badge/comps/select-shapes.tsx b/components/editor-badge/comps/select-shapes.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..26288c43f2d723500295a591ff7a311beb61c2e6
--- /dev/null
+++ b/components/editor-badge/comps/select-shapes.tsx
@@ -0,0 +1,74 @@
+import classNames from "classnames";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faCheck } from "@fortawesome/free-solid-svg-icons";
+
+import { Label } from "@/components/label";
+import { BADGE_COMPONENTS } from "@/components/svg/badges";
+import { BadgeType } from "@/types/badge";
+import { useUser } from "@/utils/auth";
+import { PremiumContext } from "@/components/premium/premium";
+import { useContext } from "react";
+import { Premium } from "@/components/premium";
+
+export const SelectShapes = ({
+ badge,
+ onChange,
+}: {
+ badge: BadgeType;
+ onChange: (b: BadgeType) => void;
+}) => {
+ const { user } = useUser();
+ const { setOpen } = useContext(PremiumContext);
+ return (
+
+
+
+ {BADGE_COMPONENTS.map((component, i) => {
+ const Component = component.mini as any;
+ return (
+
setOpen(true)
+ : () =>
+ onChange({
+ ...badge,
+ type:
+ component?.name === badge?.type
+ ? "circle"
+ : component.name,
+ })
+ }
+ >
+
+ {badge?.type === component.name && (
+
+ )}
+
+
+
+ );
+ })}
+
+
+ );
+};
diff --git a/components/editor-badge/comps/shine_effect.tsx b/components/editor-badge/comps/shine_effect.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..4ba908025004d0f991fc2a57ce9df3e0151094e2
--- /dev/null
+++ b/components/editor-badge/comps/shine_effect.tsx
@@ -0,0 +1,38 @@
+export const ShineEffect = ({
+ size = 8,
+ className,
+}: {
+ size?: number;
+ className?: string;
+}) => {
+ return (
+
+ );
+};
diff --git a/components/editor-badge/comps/tabs.tsx b/components/editor-badge/comps/tabs.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..e50bef7ae7f321233d4589d436fdd60c650f141a
--- /dev/null
+++ b/components/editor-badge/comps/tabs.tsx
@@ -0,0 +1,53 @@
+import { useState } from "react";
+import classNames from "classnames";
+
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faShapes, faAnglesRight } from "@fortawesome/free-solid-svg-icons";
+import { FormattedMessage } from "react-intl";
+
+const TABS_ELEMENTS = [
+ {
+ icon: faShapes,
+ name: "badgeEditor.tabs.main",
+ },
+ {
+ icon: faAnglesRight,
+ name: "badgeEditor.tabs.advanced",
+ },
+];
+
+export const EditorTabs = ({
+ current,
+ onChange,
+}: {
+ current: number;
+ onChange: (e: number) => void;
+}) => {
+ return (
+ // p-2 lg:p-4
+
+
+ {TABS_ELEMENTS.map((tab, index) => (
+ - onChange(index)}
+ >
+
+
+
+
+
+ ))}
+
+
+ );
+};
diff --git a/components/editor-badge/comps/user_card.tsx b/components/editor-badge/comps/user_card.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..217056abb4e9ff7007ccb11b465b01ae025d0a7c
--- /dev/null
+++ b/components/editor-badge/comps/user_card.tsx
@@ -0,0 +1,114 @@
+import { useMemo } from "react";
+
+import { BadgeType } from "@/types/badge";
+import { useUser } from "@/utils/auth";
+import DefaultAvatar from "assets/images/avatars/default-avatar.svg";
+import { Moderator } from "@/components/svg/icons/discord";
+import { Boost2 } from "@/components/svg/icons/discord";
+import { Preview } from "./preview";
+import { DownloadButton } from "./download_button";
+
+export const UserCard = ({
+ badge,
+ onSave,
+}: {
+ badge: BadgeType;
+ onSave: (e?: boolean) => void;
+}) => {
+ const { user, loading } = useUser();
+
+ const convertNumberToStringColor = (color: number) => {
+ if (!color) return "#121212";
+ return "#" + color?.toString(16);
+ };
+
+ const bannerStyle = useMemo(() => {
+ let style: any = { backgroundColor: "#121212", height: 60 };
+
+ if (user?.id) {
+ if (user?.banner) {
+ style.backgroundImage = `url(https://cdn.discordapp.com/banners/${user.id}/${user.banner}.gif?size=1024)`;
+ style.height = 120;
+ style.borderLeft = "5px solid #232429";
+ style.borderTop = "5px solid #232429";
+ style.borderRight = "5px solid #232429";
+ } else {
+ // style.backgroundColor = convertNumberToStringColor(user?.accent_color);
+ style.backgroundColor = user?.accent_color;
+ }
+ }
+
+ return style;
+ }, [user]);
+
+ return (
+
+
+
+
+
+
+
+
+
+ {user ? user?.username : "Captain Astro"}
+
+
+ {user ? `${user?.username}` : "captainastro"}
+
+
+
+ About me
+
+
+ {/*
+ Member since
+
*/}
+
+
+ Image will be split into multiple parts. You will upload them as
+ server emojis on Discord to use them on your discord Profile.
+
+
+
+
+ );
+};
diff --git a/components/editor-badge/index.tsx b/components/editor-badge/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..25b02ad592aee5954404ecaffde52394cdb896f3
--- /dev/null
+++ b/components/editor-badge/index.tsx
@@ -0,0 +1,94 @@
+import { useState, useRef } from "react";
+import { domToPng } from "modern-screenshot";
+import ImageToPieces from "image-to-pieces";
+import JSZip from "jszip";
+import download from "downloadjs";
+
+import {
+ DEFAULT_VALUE,
+ RATIONAL_BADGE_WIDTH,
+} from "components/editor-badge/badge-editor.constants";
+import BackgroundTransparent from "assets/images/editor/transparent_bg.svg";
+import { API } from "@/utils/api";
+import { useUser } from "@/utils/auth";
+
+import { MainForm } from "./comps/header/form/main";
+import { AdvancedForm } from "./comps/header/form/advanced";
+import { Badge } from "./comps/badge";
+import { EditorTabs } from "./comps/tabs";
+import { UserCard } from "./comps/user_card";
+
+export default function Editor() {
+ const { user } = useUser();
+ const [badge, setBadge] = useState({
+ ...DEFAULT_VALUE,
+ });
+
+ const badgeRef = useRef(null);
+ const [current, setCurrent] = useState(0);
+
+ const handleSave = (hasImage = false) => {
+ domToPng(
+ document.getElementById("discotools-selected-badge") as HTMLElement,
+ {
+ scale: 2,
+ }
+ ).then(async (dataUrl) => {
+ if (hasImage) {
+ download(dataUrl, "badge.png", "image/png");
+ // API.downloadBadge();
+ return;
+ }
+
+ if (!badgeRef?.current) return;
+ const badgeWidth = badgeRef.current.getBoundingClientRect().width;
+ const image = new ImageToPieces(
+ null,
+ badgeWidth / RATIONAL_BADGE_WIDTH,
+ 1
+ );
+ await image.loadImageByUrl(dataUrl);
+ const tiles = image
+ .getTiles()
+ ?.map((tile: any) => tile.data?.replace("data:image/png;base64,", ""));
+ const zip_folder = new JSZip();
+ tiles.forEach((tile: any, i: number) =>
+ zip_folder.file(`${i}.png`, tile, { base64: true })
+ );
+
+ zip_folder.generateAsync({ type: "blob" }).then(function (content) {
+ download(content, "badges.zip");
+ // API.downloadBadge();
+ });
+ });
+ };
+
+ const renderTabs = () => {
+ switch (current) {
+ case 0:
+ return ;
+ case 1:
+ return ;
+ }
+ };
+
+ return (
+
+
+
+ {renderTabs()}
+
+
+
+
+
+
+ );
+}
diff --git a/components/editor-icons/comps/header/comps/icons-slider.tsx b/components/editor-icons/comps/header/comps/icons-slider.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..ea495708aeac5e443c2583db6e0de3e44a65dfd9
--- /dev/null
+++ b/components/editor-icons/comps/header/comps/icons-slider.tsx
@@ -0,0 +1,108 @@
+import { useRef, useState } from "react";
+import { useInterval } from "react-use";
+import { motion } from "framer-motion";
+import Image from "next/image";
+
+import { sleep } from "utils";
+import HammerIcon from "assets/images/header/icons-slider/hammer.svg";
+import ShieldIcon from "assets/images/header/icons-slider/shield.svg";
+import CrownIcon from "assets/images/header/icons-slider/crown.svg";
+import PartnerIcon from "assets/images/header/icons-slider/partner.svg";
+import StarIcon from "assets/images/header/icons-slider/star.svg";
+import LeafIcon from "assets/images/header/icons-slider/leaf.svg";
+
+const ICONS = [
+ HammerIcon,
+ ShieldIcon,
+ CrownIcon,
+ PartnerIcon,
+ StarIcon,
+ LeafIcon,
+];
+
+const icon = {
+ hidden: {
+ opacity: 0,
+ pathLength: 0,
+ fill: "rgba(255, 255, 255, 0)",
+ },
+ visible: {
+ opacity: 1,
+ pathLength: 1,
+ fill: "rgba(255, 255, 255, 1)",
+ },
+};
+
+export const IconsSlider = () => {
+ const [selected, setSelected] = useState(0);
+ const ref = useRef(null);
+
+ useInterval(async () => {
+ if (ref?.current) {
+ ref.current?.classList?.add("opacity-0", "-translate-x-full", "scale-50");
+ await sleep(300);
+ ref.current?.classList?.remove("-translate-x-full");
+ ref.current?.classList?.add("translate-x-full");
+
+ setSelected((prev) => (prev + 1) % ICONS.length);
+ // if (this.selected >= this.avatars.length - 1) {
+ // this.selected = 0;
+ // } else {
+ // this.selected += 1;
+ // }
+ await sleep(300);
+ ref.current?.classList?.remove(
+ "translate-x-full",
+ "scale-50",
+ "opacity-0"
+ );
+ }
+ }, 2000);
+
+ return (
+
+ {/*
*/}
+
+
+
+
+
+
+
+
+
+ //
+ //

+ //
+ //
+ );
+};
diff --git a/components/editor-icons/comps/header/index.tsx b/components/editor-icons/comps/header/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..3effd4e189f3e51ca6d96433f0dbc877bdacd891
--- /dev/null
+++ b/components/editor-icons/comps/header/index.tsx
@@ -0,0 +1,84 @@
+import Image from "next/image";
+
+import { Tag } from "components/tag";
+import { IconsSlider } from "./comps/icons-slider";
+
+// import { Editor } from "components/editor";
+// import { ArrayOfIcons } from "components/svg/icons";
+
+// import { Navigation } from "components/navigation";
+
+import Background from "assets/images/header/background.svg";
+import Stars from "assets/images/header/stars.svg";
+import Ananas from "assets/images/header/ananas.svg";
+import Pizza from "assets/images/header/pizza.svg";
+import { FormattedMessage, useIntl } from "react-intl";
+
+export const Header = ({ children }: any) => {
+ const intl = useIntl();
+ return (
+
+
+
+
+
+ NEW VERSION RELEASED!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+
+
+ {/*
+
*/}
+
+ );
+};
diff --git a/components/editor-icons/comps/icons/icon-selected.tsx b/components/editor-icons/comps/icons/icon-selected.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..6c9310dd79c4161f3aff882ddebf05c2adf7429b
--- /dev/null
+++ b/components/editor-icons/comps/icons/icon-selected.tsx
@@ -0,0 +1,603 @@
+import classNames from "classnames";
+import {
+ faChevronDown,
+ faChevronRight,
+ faTrash,
+ faCaretDown,
+ faCaretUp,
+} from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+
+import { ListItem } from "components/editor-icons/comps/list/list-item";
+import {
+ Icons as ICONS,
+ IconCustomIcon,
+ IconCustomText,
+} from "components/svg/icons";
+import { IconItem, IconType } from "@/types/editor";
+import { ColorPicker } from "components/color-picker";
+import { Range } from "components/range";
+import { Input } from "components/input";
+import { Switch } from "@/components/switch";
+import { PremiumOverlay } from "@/components/premium/overlay";
+import { FormattedMessage, useIntl } from "react-intl";
+import { Label } from "@/components/label";
+
+export const IconSelected = ({
+ index,
+ totalIcons,
+ icon,
+ current,
+ setCurrent,
+ onDelete,
+ onChange,
+ onChangeOrder,
+}: {
+ index: number;
+ totalIcons: number;
+ icon: IconType;
+ current?: number | null;
+ setCurrent: (index: number | null) => void;
+ onDelete: (index: number) => void;
+ onChange: (idnex: number, icon: IconType) => void;
+ onChangeOrder: (index: number, value: number) => void;
+}) => {
+ const findIcon: any = icon?.custom_text?.enabled
+ ? IconCustomText
+ : icon?.image
+ ? IconCustomIcon
+ : ICONS?.find((i: IconItem) => icon.component === i.name);
+
+ const handleChange = (index: number, icon: any) => {
+ onChange(index, icon);
+ };
+
+ const intl = useIntl();
+
+ return (
+
+
+
setCurrent(current === index ? null : index)}
+ >
+
+
+ {icon?.image ? (
+
+

+
+ ) : (
+
{}}
+ />
+ )}
+
+
+
+
+
+
+ {findIcon?.tags?.join(", ")}
+
+
+
+
+
{
+ e.preventDefault();
+ e.stopPropagation();
+ onDelete(index);
+ }}
+ >
+
+
+
+
+
+
+
+ {current === index && (
+
+
+
+
+
+
+
+ X
+
+
{
+ const newIcon = {
+ ...icon,
+ position: { ...icon.position, x: Number(value) },
+ };
+ handleChange(index, newIcon);
+ }}
+ />
+
+
{
+ let x: number = Number(newX);
+ if (x > 100) x = 100;
+ const newIcon = {
+ ...icon,
+ position: { ...icon.position, x: x },
+ };
+ handleChange(index, newIcon);
+ }}
+ />
+
+
+
+
+ Y
+
+
{
+ const newIcon = {
+ ...icon,
+ position: { ...icon.position, y: Number(value) },
+ };
+ handleChange(index, newIcon);
+ }}
+ />
+
+
{
+ let y = Number(newY);
+ if (y > 100) y = 100;
+ const newIcon = {
+ ...icon,
+ position: { ...icon.position, y: y },
+ };
+ handleChange(index, newIcon);
+ }}
+ />
+
+
+
+
+ Z
+
+
{
+ const newIcon = {
+ ...icon,
+ position: { ...icon.position, angle: Number(value) },
+ };
+ handleChange(index, newIcon);
+ }}
+ />
+
+
{
+ let angle = Number(newAngle);
+ if (angle > 360) angle = 360;
+ const newIcon = {
+ ...icon,
+ position: { ...icon.position, angle: angle },
+ };
+ handleChange(index, newIcon);
+ }}
+ />
+
+ {!icon?.custom_text?.enabled && (
+
+
+
+
+
+
{
+ const newIcon = {
+ ...icon,
+ position: {
+ ...icon.position,
+ scale: Number(value),
+ },
+ };
+ handleChange(index, newIcon);
+ }}
+ />
+
+
{
+ let scale = Number(newScale);
+ if (scale > 100) scale = 100;
+ const newIcon = {
+ ...icon,
+ position: {
+ ...icon.position,
+ scale: Math.trunc(scale) / 100,
+ },
+ };
+ handleChange(index, newIcon);
+ }}
+ />
+
+ )}
+
+
+ {!icon?.image && (
+
+
+ {
+ let newIcon = { ...icon, stringColor: c };
+ if (c.includes("gradient")) {
+ const { colors, degrees } = datas;
+ const gradientType = c?.startsWith("linear")
+ ? "linearGradient"
+ : "radialGradient";
+
+ const angle = c?.startsWith("linear")
+ ? c?.replace("linear-gradient(", "")?.split("deg")?.[0]
+ : 90;
+
+ newIcon["gradient"] = {
+ ...newIcon.gradient,
+ enabled: true,
+ colours: colors,
+ angle,
+ type: gradientType,
+ };
+ } else {
+ newIcon = {
+ ...newIcon,
+ colour: c,
+ gradient: {
+ ...icon?.gradient,
+ enabled: false,
+ },
+ };
+ }
+ handleChange(index, newIcon);
+ }}
+ />
+
+ )}
+ {icon?.custom_text?.enabled && (
+ <>
+
+
+
+
+
+
+
+
{
+ const newIcon = {
+ ...icon,
+ custom_text: {
+ ...icon.custom_text,
+ text: target.value,
+ },
+ };
+ handleChange(index, newIcon);
+ }}
+ />
+
+
+
+
+
+
{
+ const newIcon = {
+ ...icon,
+ custom_text: {
+ ...icon.custom_text,
+ size: Number(target.value),
+ },
+ };
+ handleChange(index, newIcon);
+ }}
+ />
+
+
+
+ >
+ )}
+
+ {!icon?.image && (
+
+
+
+
+
+
+
+
{
+ const newIcon = {
+ ...icon,
+ border: {
+ ...icon.border,
+ width: target?.value
+ ? Number(target.value)
+ : undefined,
+ },
+ };
+ handleChange(index, newIcon);
+ }}
+ />
+
+
+
+
+
+
{
+ const newIcon = {
+ ...icon,
+ border: {
+ ...icon.border,
+ colour: c,
+ },
+ };
+ handleChange(index, newIcon);
+ }}
+ />
+
+
+
+ )}
+
+
+
+ {
+ const newIcon = {
+ ...icon,
+ shadow: {
+ ...icon.shadow,
+ enabled,
+ },
+ };
+ handleChange(index, newIcon);
+ }}
+ />
+
+ {icon?.shadow?.enabled && (
+
+
+
+ X
+
+
{
+ const newIcon = {
+ ...icon,
+ shadow: {
+ ...icon.shadow,
+ position: {
+ ...icon?.shadow?.position,
+ x: target?.value
+ ? Number(target.value)
+ : undefined,
+ },
+ },
+ };
+ handleChange(index, newIcon);
+ }}
+ />
+
+
+
+ Y
+
+
{
+ const newIcon = {
+ ...icon,
+ shadow: {
+ ...icon.shadow,
+ position: {
+ ...icon?.shadow?.position,
+ y: target?.value
+ ? Number(target.value)
+ : undefined,
+ },
+ },
+ };
+ handleChange(index, newIcon);
+ }}
+ />
+
+
+
+
+
+
{
+ const newIcon = {
+ ...icon,
+ shadow: {
+ ...icon.shadow,
+ position: {
+ ...icon?.shadow?.position,
+ blur: target?.value
+ ? Number(target.value)
+ : undefined,
+ },
+ },
+ };
+ handleChange(index, newIcon);
+ }}
+ />
+
+
+
+
+
+
{
+ const newIcon = {
+ ...icon,
+ shadow: {
+ ...icon.shadow,
+ colour: c,
+ },
+ };
+ handleChange(index, newIcon);
+ }}
+ />
+
+
+ )}
+
+
+
+
+ )}
+
+ {totalIcons > 1 && (
+
+ {index !== 0 && (
+ onChangeOrder(index, index - 1)}
+ />
+ )}
+ {index !== totalIcons - 1 && (
+ onChangeOrder(index, index + 1)}
+ />
+ )}
+
+ )}
+
+ );
+};
diff --git a/components/editor-icons/comps/icons/index.tsx b/components/editor-icons/comps/icons/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..8411ff6a36809c4f2b95b7f53054d719939fc906
--- /dev/null
+++ b/components/editor-icons/comps/icons/index.tsx
@@ -0,0 +1,107 @@
+import { useState } from "react";
+import { PlusIcon } from "@heroicons/react/solid";
+
+import { EditorType, IconType } from "@/types/editor";
+import { Empty } from "@/components/empty";
+
+import { IconSelected } from "./icon-selected";
+import { FormattedMessage, useIntl } from "react-intl";
+
+export const Icons = ({
+ editor,
+ onChange,
+ onStep,
+}: {
+ editor: EditorType;
+ onChange: (e: EditorType) => void;
+ onStep: (step: number, multiple?: boolean) => void;
+}) => {
+ const [opened, setOpened] = useState(0);
+ const intl = useIntl();
+
+ const handleChange = (index: number, icon: any) => {
+ const newIcons = [...editor.icons];
+ newIcons[index] = icon;
+ onChange({
+ ...editor,
+ icons: newIcons,
+ });
+ };
+
+ const handleDeleteIcon = (index: number) => {
+ const newIcons = [...editor.icons];
+ newIcons.splice(index, 1);
+ onChange({
+ ...editor,
+ icons: newIcons,
+ });
+ };
+
+ const handleChangeOrder = (index: number, value: number) => {
+ const newIcons = [...editor.icons];
+ const [removed] = newIcons.splice(index, 1);
+ newIcons.splice(value, 0, removed);
+ onChange({
+ ...editor,
+ icons: newIcons,
+ });
+ };
+
+ return (
+
+
+
+
+ {editor?.icons?.length > 0 ? (
+ <>
+
+ {editor?.icons?.map((icon: IconType, k) => {
+ return (
+
+ );
+ })}
+ >
+ ) : (
+
onStep(0)}
+ />
+ )}
+
+ );
+};
diff --git a/components/editor-icons/comps/list/index.tsx b/components/editor-icons/comps/list/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..16300b14fe62a8709c6d1c93134357ac69b7b833
--- /dev/null
+++ b/components/editor-icons/comps/list/index.tsx
@@ -0,0 +1,151 @@
+import { ArrowRightIcon, ArrowLeftIcon } from "@heroicons/react/solid";
+import { useContext, useRef } from "react";
+
+import { useResults } from "components/search/hooks/useSearch";
+import { ListItem } from "components/editor-icons/comps/list/list-item";
+import { IconItem } from "types/editor";
+import { Empty } from "components/empty";
+import { login, useUser } from "utils/auth";
+import { PremiumContext } from "components/premium/premium";
+import { Premium } from "@/components/premium";
+import classNames from "classnames";
+import { toBase64 } from "@/components/upload";
+import { FormattedMessage } from "react-intl";
+
+export const ListIcons = ({
+ onSelect,
+ onCustomText,
+ onCustomUpload,
+}: {
+ onSelect: (e: IconItem) => void;
+ onCustomText: () => void;
+ onCustomUpload: (s: string) => void;
+}) => {
+ const { user } = useUser();
+ const { setOpen } = useContext(PremiumContext);
+ const inputRef = useRef(null);
+
+ const { results, page, maxPage, totalItems, setPage } = useResults();
+
+ const handleCustomUpload = (e: any) => {
+ const file = e.target.files[0];
+ if (file) {
+ toBase64(file).then((res: any) => {
+ onCustomUpload(res);
+ });
+ }
+ };
+
+ return (
+
+
+
+ totalItems }}
+ />
+
+
+
+
+
+
+
+
+ {results?.map((result: any, key: number) => (
+ {
+ // if (icon?.isPremium && !user?.id) return setOpen(true);
+ onSelect(icon);
+ }}
+ />
+ ))}
+ {results?.length === 0 && (
+ {
+ if (!user?.id) return login();
+ // send to api a request;
+ console.log("display Modal and call API");
+ }}
+ button="Ask for a new Icon"
+ />
+ )}
+
+ {results?.length > 0 && (
+
+
+
+
+
+ )}
+
+ );
+};
diff --git a/components/editor-icons/comps/list/list-item.tsx b/components/editor-icons/comps/list/list-item.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..2f0379ec6a705ef33d94b986330179c6143ff01f
--- /dev/null
+++ b/components/editor-icons/comps/list/list-item.tsx
@@ -0,0 +1,86 @@
+import classNames from "classnames";
+import React, { useEffect, useRef } from "react";
+
+export const ListItem = ({
+ icon,
+ tooltip = true,
+ hoverable = true,
+ fill = "#b0b0b0",
+ onSelect,
+}: {
+ icon: any;
+ tooltip?: boolean;
+ hoverable?: boolean;
+ fill?: string;
+ onSelect: (e: any) => void;
+}) => {
+ const ref = useRef(null);
+ const svgRef = useRef(null);
+ const [viewBox, setViewBox] = React.useState("0 0 24 24");
+ const renderComponent = () => {
+ const Component = icon?.component as any;
+ return ;
+ };
+
+ useEffect(() => {
+ return setViewBox(ref?.current?.getAttribute("viewBox") ?? "0 0 200 200");
+ });
+
+ return (
+ onSelect(icon)}
+ >
+ {/* {icon?.isPremium && (
+
+ )} */}
+
+ {hoverable && (
+
+ )}
+ {/* {tooltip && (
+ <>
+
+
+
+
+ {icon?.tags?.join(", ")}
+
+
+ >
+ )} */}
+
+ );
+};
diff --git a/components/editor-icons/comps/shapes/index.tsx b/components/editor-icons/comps/shapes/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..bf7ce3b83706653836fd05b387a627116c84ca34
--- /dev/null
+++ b/components/editor-icons/comps/shapes/index.tsx
@@ -0,0 +1,98 @@
+import { useContext } from "react";
+
+import { EditorType } from "types/editor";
+import { SHAPES } from "components/svg/shapes";
+import classNames from "classnames";
+import { Empty } from "@/components/empty";
+import { useUser } from "@/utils/auth";
+
+import { ShapeSelected } from "./shape-selected";
+import { PremiumContext } from "@/components/premium/premium";
+import { FormattedMessage, useIntl } from "react-intl";
+
+export const Shapes = ({
+ editor,
+ onChange,
+}: {
+ editor: EditorType;
+ onChange: (s: any) => void;
+}) => {
+ const { user } = useUser();
+ const intl = useIntl();
+
+ const { setOpen } = useContext(PremiumContext);
+
+ return (
+ <>
+
+
+
+
+ {SHAPES.map((shape, k) => (
+
{
+ // if (!user && shape?.isPremium) {
+ // return setOpen(true);
+ // }
+ onChange({ ...editor.shape, component: shape?.name });
+ }}
+ >
+ {/* {shape?.isPremium && (
+
+ )} */}
+ <_ShapeComponent
+ component={shape.component}
+ width={28}
+ height={28}
+ shape={shape}
+ />
+
+
+ ))}
+
+ {editor.shape?.component ? (
+ onChange({ ...editor.shape, ...shape })}
+ onDelete={() => onChange({ ...editor.shape, component: null })}
+ />
+ ) : (
+ onStep(0)}>Add my first Icon}
+ />
+ )}
+ >
+ );
+};
+
+const _ShapeComponent = ({ component, shape, onClick, ...props }: any) => {
+ const ShapeComponent = component as any;
+ const newShape = {
+ ...shape,
+ component: `${shape.name}-small`,
+ };
+ return ;
+};
diff --git a/components/editor-icons/comps/shapes/shape-selected.tsx b/components/editor-icons/comps/shapes/shape-selected.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..7b76940b297c9e63e1f549c6a34cee1260f169b8
--- /dev/null
+++ b/components/editor-icons/comps/shapes/shape-selected.tsx
@@ -0,0 +1,307 @@
+import classNames from "classnames";
+import { useState } from "react";
+import {
+ faChevronDown,
+ faChevronRight,
+ faTrash,
+ faCaretDown,
+ faCaretUp,
+} from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+
+import { ColorPicker } from "components/color-picker";
+import { Range } from "components/range";
+import { UploadImage } from "@/components/upload";
+import { Switch } from "@/components/switch";
+import { login, useUser } from "@/utils/auth";
+import { Premium } from "@/components/premium";
+import { PremiumOverlay } from "@/components/premium/overlay";
+import { FormattedMessage, useIntl } from "react-intl";
+import { Label } from "@/components/label";
+
+export const ShapeSelected = ({
+ shape,
+ handleChange,
+ onDelete,
+}: {
+ shape: any;
+ handleChange: (s: any) => void;
+ onDelete: () => void;
+}) => {
+ const { user } = useUser();
+ const intl = useIntl();
+ const [open, setOpen] = useState(true);
+
+ return (
+
+
setOpen(!open)}
+ >
+
+
+
+
+
{
+ e.preventDefault();
+ e.stopPropagation();
+ onDelete();
+ }}
+ >
+
+
+
+
+
+
+
+ {open && (
+
+
+
+
+
Z
+
{
+ const newShape = {
+ ...shape,
+ position: { ...shape.position, angle: Number(value) },
+ };
+ handleChange(newShape);
+ }}
+ />
+ {
+ const newShape = {
+ ...shape,
+ position: {
+ ...shape.position,
+ angle: target.value
+ ? Number(target.value) > 360
+ ? 360
+ : Number(target.value)
+ : undefined,
+ },
+ };
+ handleChange(newShape);
+ }}
+ />
+
+
+ {shape?.component === "Square" && (
+
+
+
+
+ Z
+
+
{
+ const newShape = {
+ ...shape,
+ radius: Number(value),
+ };
+ handleChange(newShape);
+ }}
+ />
+ {
+ const newShape = {
+ ...shape,
+ radius: Number(target.value),
+ };
+ handleChange(newShape);
+ }}
+ />
+
+
+ )}
+
+
+
+ {
+ const newShape = {
+ ...shape,
+ transparency: !transparency,
+ };
+ handleChange(newShape);
+ }}
+ />
+
+
{
+ let newShape = { ...shape, stringColor: c };
+ if (c.includes("gradient")) {
+ const { colors, degrees } = datas;
+ const gradientType = c?.startsWith("linear")
+ ? "linearGradient"
+ : "radialGradient";
+
+ const angle = c?.startsWith("linear")
+ ? c?.replace("linear-gradient(", "")?.split("deg")?.[0]
+ : 90;
+
+ newShape["gradient"] = {
+ ...newShape.gradient,
+ enabled: true,
+ colours: colors,
+ angle,
+ type: gradientType,
+ };
+ } else {
+ newShape = {
+ ...newShape,
+ colour: c,
+ gradient: {
+ ...shape?.gradient,
+ enabled: false,
+ },
+ };
+ }
+
+ handleChange(newShape);
+ }}
+ />
+
+
+
+
+
+ {
+ const newShape = {
+ ...shape,
+ image: {
+ ...shape.image,
+ enabled,
+ },
+ };
+ handleChange(newShape);
+ }}
+ />
+
+ {shape?.image?.enabled && (
+
{
+ const newShape = {
+ ...shape,
+ image: {
+ ...shape.image,
+ urls,
+ },
+ };
+ handleChange(newShape);
+ }}
+ />
+ )}
+
+
+
+
+
+
+
+
+
{
+ const newShape = {
+ ...shape,
+ border: {
+ ...shape.border,
+ width: target?.value
+ ? Number(target.value)
+ : undefined,
+ },
+ };
+ handleChange(newShape);
+ }}
+ />
+
+
+
+
+
+
{
+ const newShape = {
+ ...shape,
+ border: {
+ ...shape.border,
+ colour: c,
+ },
+ };
+ handleChange(newShape);
+ }}
+ />
+
+
+
+
+
+
+ )}
+
+ );
+};
diff --git a/components/editor-icons/comps/tabs.tsx b/components/editor-icons/comps/tabs.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..64da7465db2392f05bdf7e9412d6276ac913b70a
--- /dev/null
+++ b/components/editor-icons/comps/tabs.tsx
@@ -0,0 +1,65 @@
+import { useState } from "react";
+import classNames from "classnames";
+
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import {
+ faSearch,
+ faArrowsUpDownLeftRight,
+ faPalette,
+ faShapes,
+ faIcons,
+} from "@fortawesome/free-solid-svg-icons";
+import { FormattedMessage } from "react-intl";
+
+const TABS_ELEMENTS = [
+ {
+ icon: faSearch,
+ name: "iconsEditor.editor.tabs.listIcons",
+ },
+ {
+ icon: faShapes,
+ name: "iconsEditor.editor.tabs.shape",
+ },
+ {
+ icon: faIcons,
+ name: "iconsEditor.editor.tabs.icons",
+ },
+];
+
+export const EditorTabs = ({
+ current,
+ onChange,
+}: {
+ current: number;
+ onChange: (e: number) => void;
+}) => {
+ return (
+ // p-2 lg:p-4
+
+
+ {TABS_ELEMENTS.map((tab, index) => (
+ - onChange(index)}
+ >
+
+
+
+
+
+ ))}
+
+
+ );
+};
diff --git a/components/editor-icons/editor.constants.ts b/components/editor-icons/editor.constants.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a204ed30c3f6424e4b5584e1e4d8f09265dc3517
--- /dev/null
+++ b/components/editor-icons/editor.constants.ts
@@ -0,0 +1,50 @@
+import { EditorType } from "types/editor";
+
+export const DEFAULT_VALUE: EditorType = {
+ icons: [
+ {
+ component: "King",
+ colour: "#FFF",
+ gradient: {
+ enabled: false,
+ colours: [],
+ angle: 0,
+ type: "linear-gradient",
+ },
+ border: {
+ width: 0,
+ colour: "#FFFFFF",
+ },
+ shadow: {
+ enabled: false,
+ colour: "#FEE75C",
+ position: {
+ x: 0,
+ y: 0,
+ blur: 16,
+ },
+ },
+ transparency: 100,
+ },
+ ],
+ shape: {
+ component: "Badge",
+ colour: "#5865F1",
+ gradient: {
+ enabled: false,
+ colours: [],
+ angle: 90,
+ type: "linear-gradient",
+ },
+ position: {
+ angle: 0,
+ scale: 1,
+ },
+ radius: 40,
+ border: {
+ width: 0,
+ colour: "#ffffff",
+ },
+ transparency: false,
+ },
+};
diff --git a/components/editor-icons/icon.tsx b/components/editor-icons/icon.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..4d126c6ffaa05188d3ecb10b05c9d1f1b89cddcf
--- /dev/null
+++ b/components/editor-icons/icon.tsx
@@ -0,0 +1,200 @@
+import { Fragment, useRef, useState } from "react";
+import { useMount, useUpdateEffect } from "react-use";
+import { motion } from "framer-motion";
+import saveAsPng from "save-svg-as-png";
+
+import { EditorType, IconItem } from "types/editor";
+import Shapes from "@/components/svg/shapes";
+import { Icons } from "components/svg/icons";
+import { Null } from "components/svg/shapes/null";
+
+export const Icon = ({
+ editor,
+ size = 200,
+ id,
+ onChange,
+ onChangeTab,
+}: {
+ editor: EditorType;
+ id?: string;
+ size?: number;
+ onChange?: (e: EditorType) => void;
+ onChangeTab?: (e: number) => void;
+}) => {
+ const shapeRef = useRef(null);
+ const ComponentShape =
+ Shapes[editor.shape?.component as keyof typeof Shapes] ?? Null;
+
+ return (
+
+ {editor?.icons?.map((icon, k) => {
+ const findIcon = Icons?.find(
+ (i: IconItem) => icon.component === i.name
+ );
+ const customText = icon?.custom_text?.enabled;
+ const customImage = icon?.image;
+ const GradientType =
+ icon?.gradient?.type === "radialGradient"
+ ? "radialGradient"
+ : "linearGradient";
+ if (!findIcon && !customText && !customImage) return null;
+ return (
+
+
+
+ {icon?.gradient?.enabled ? (
+ <>
+ {icon?.gradient?.colours?.map((color, c) => (
+
+ ))}
+ >
+ ) : (
+
+ )}
+
+
+ <_IconComponent
+ component={findIcon?.component}
+ shapeRef={shapeRef}
+ shape={editor?.shape?.component}
+ key={k}
+ index={k}
+ icon={icon}
+ onChange={(e: any) =>
+ onChange &&
+ onChange({
+ ...editor,
+ icons: editor.icons.map((i: any, index: number) =>
+ index === k ? e : i
+ ),
+ })
+ }
+ onClick={onChangeTab ? () => onChangeTab(2) : undefined}
+ />
+
+ );
+ })}
+
+ );
+};
+
+const _IconComponent = ({
+ shapeRef,
+ component,
+ icon,
+ shape,
+ index,
+ onChange,
+ ...props
+}: any) => {
+ const iconRef = useRef(null);
+ const [position, setPosition] = useState(null);
+
+ useUpdateEffect(() => {
+ const position = setIconPosition(iconRef, shapeRef);
+ setPosition(position);
+ const svg = document.getElementById("discotools-selected-svg");
+ saveAsPng.svgAsPngUri(svg).then(() => {});
+ }, [shapeRef.current, icon.component, icon?.image]);
+
+ useMount(() => {
+ const position = setIconPosition(iconRef, shapeRef);
+ setPosition(position);
+ });
+
+ useUpdateEffect(() => {
+ onChange({
+ ...icon,
+ position,
+ });
+ }, [position]);
+
+ const IconComponent = component as any;
+
+ return (
+
+ {icon?.custom_text?.enabled ? (
+
+ {icon?.custom_text?.text}
+
+ ) : icon?.image ? (
+
+ ) : (
+
+ )}
+
+ );
+};
+
+const setIconPosition = (iconRef: any, shapeRef: any) => {
+ if (iconRef?.current) {
+ const shapeBoxW = shapeRef?.current?.getAttribute("viewBox")?.split(" ")[2];
+ const shapeBoxH = shapeRef?.current?.getAttribute("viewBox")?.split(" ")[3];
+ const iconBoxW = iconRef?.current?.getAttribute("viewBox")?.split(" ")[2];
+ const iconBoxH = iconRef?.current?.getAttribute("viewBox")?.split(" ")[3];
+
+ const position = {
+ x: shapeBoxW / 2 - iconBoxW / 2,
+ y: shapeBoxH / 2 - iconBoxH / 2,
+ xPath: Number(shapeBoxW - iconBoxW),
+ yPath: Number(shapeBoxH - iconBoxH),
+ };
+ return position;
+ }
+};
diff --git a/components/editor-icons/index.tsx b/components/editor-icons/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..a99b1a6ea19c9fa6f48f0774b41ea4ec824add79
--- /dev/null
+++ b/components/editor-icons/index.tsx
@@ -0,0 +1,317 @@
+import { useContext, useState } from "react";
+import Image from "next/image";
+import { useTour } from "@reactour/tour";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faDownload } from "@fortawesome/free-solid-svg-icons";
+import saveAsPng from "save-svg-as-png";
+
+import { EditorType, IconItem } from "types/editor";
+import { Icon } from "./icon";
+import { EditorTabs } from "components/editor-icons/comps/tabs";
+import { ListIcons } from "components/editor-icons/comps/list";
+import { Search } from "components/search";
+import { ClydeMessage } from "components/clyde/message";
+import { Switch } from "components/switch";
+import { Shapes } from "@/components/editor-icons/comps/shapes";
+import { Icons } from "components/editor-icons/comps/icons";
+import { useUser } from "utils/auth";
+import { PremiumContext } from "components/premium/premium";
+
+import DefaultAvatar from "assets/images/avatars/default-avatar.svg";
+import DefaultAvatar2 from "assets/images/avatars/default-avatar-2.svg";
+import BackgroundTransparent from "assets/images/editor/transparent_bg.svg";
+import { API } from "@/utils/api";
+import { Premium } from "../premium";
+import { FormattedMessage, useIntl } from "react-intl";
+
+export default function Editor({
+ editor,
+ onChange,
+}: {
+ editor: EditorType;
+ onChange: (e: EditorType) => void;
+}) {
+ const intl = useIntl();
+ const { user } = useUser();
+ const { setOpen } = useContext(PremiumContext);
+ const { setIsOpen } = useTour();
+
+ const [currentTab, setCurrentTab] = useState(0);
+ const [multiIcons, setMultiIcons] = useState(false);
+ const [search, setSearch] = useState("");
+
+ const renderTabs = (currentTab: number) => {
+ switch (currentTab) {
+ case 0:
+ return (
+ <>
+
+
+
setOpen(true) : setMultiIcons}
+ />
+
+
+ {/* {!user?.id ? : ""} */}
+
+
+ {
+ // if (!user?.id) return setOpen(true);
+ onChange({
+ ...editor,
+ icons: multiIcons
+ ? [
+ ...editor.icons,
+ {
+ colour: "#ffffff",
+ custom_text: {
+ enabled: true,
+ text: "10",
+ size: 160,
+ },
+ },
+ ]
+ : [
+ {
+ ...editor?.icons[0],
+ component: undefined,
+ position: undefined,
+ colour: "#ffffff",
+ custom_text: {
+ enabled: true,
+ text: "10",
+ size: 160,
+ },
+ },
+ ],
+ });
+ if (!multiIcons) setCurrentTab(2);
+ }}
+ onCustomUpload={(image) => {
+ onChange({
+ ...editor,
+ icons: multiIcons
+ ? [
+ ...editor.icons,
+ {
+ colour: "#ffffff",
+ image,
+ },
+ ]
+ : [
+ {
+ ...editor?.icons[0],
+ custom_text: undefined,
+ component: undefined,
+ position: undefined,
+ colour: "#ffffff",
+ image,
+ },
+ ],
+ });
+ if (!multiIcons) setCurrentTab(2);
+ }}
+ onSelect={(icon: IconItem) => {
+ onChange({
+ ...editor,
+ icons: multiIcons
+ ? [
+ ...editor.icons,
+ {
+ component: icon.name,
+ colour: icon?.defaultColor ?? "#fff",
+ },
+ ]
+ : [
+ {
+ ...editor?.icons[0],
+ colour: icon?.defaultColor ?? "#fff",
+ component: icon.name,
+ position: undefined,
+ custom_text: undefined,
+ },
+ ],
+ });
+ }}
+ />
+ >
+ );
+ case 1:
+ return (
+ <>
+
+ onChange({ ...editor, shape: { ...editor.shape, ...newShape } })
+ }
+ />
+ >
+ );
+ case 2:
+ return (
+ <>
+ {
+ setCurrentTab(tabIndex);
+ setMultiIcons(user?.id ? allowMultiple || false : false);
+ }}
+ />
+ >
+ );
+ default:
+ return null;
+ }
+ };
+
+ const handleSaveIcon = () => {
+ const svg = document.getElementById("discotools-selected-svg");
+
+ saveAsPng.saveSvgAsPng(svg, "discotools-xyz-icon.png", {
+ scale: 1.25,
+ encoderOptions: 1,
+ });
+ // API.download();
+ };
+
+ const previewAvatar = () => {
+ if (user?.id) {
+ if (user?.avatar)
+ return `https://cdn.discordapp.com/avatars/${user?.id}/${user?.avatar}.png`;
+ else
+ return `https://cdn.discordapp.com/embed/avatars/${
+ user.discriminator % 5
+ }.png`;
+ }
+ return DefaultAvatar.src;
+ };
+
+ const renderColor = () => {
+ if (editor?.shape?.gradient?.enabled) {
+ const color = editor?.shape?.gradient?.colours?.[0]?.value;
+ if (color.includes("rgba")) {
+ const rgba = color?.split(",");
+ return `rgb(${rgba[0].split("(")[1]}, ${rgba[1]}, ${rgba[2]})`;
+ }
+ return color;
+ }
+ if (editor?.shape?.colour?.includes("rgba")) {
+ const rgba = editor?.shape?.colour?.split(",");
+ return `rgb(${rgba[0].split("(")[1]}, ${rgba[1]}, ${rgba[2]})`;
+ }
+ if (editor?.shape?.colour) {
+ return editor.shape.colour;
+ }
+ return "#fff";
+ };
+
+ return (
+
+
setIsOpen(true)}
+ />
+ {
+ // e.preventDefault();
+ // e.stopPropagation();
+ }}
+ >
+
+
+
+ {renderTabs(currentTab)}
+
+
+
+
+ setCurrentTab(i)}
+ />
+
+
+
+
+
+
})
+
+
+
+ {user?.username ?? "Captain Astro"}
+
+
+
+
+ Today at 11:23 pm
+
+
+
+
+
+
+
+
+
+
+
+
+ Rookie
+
+
+ Today at 11:23 pm
+
+
+
+ user?.username ?? "Captain Astro",
+ }}
+ />
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/components/empty/index.tsx b/components/empty/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..eb53185133c089946354ada898ac8f57e3a03d4d
--- /dev/null
+++ b/components/empty/index.tsx
@@ -0,0 +1,46 @@
+import Image from "next/image";
+import ClydeAffraid from "assets/images/clyde/affraid.svg";
+
+export const Empty = ({
+ title,
+ description,
+ button,
+ className,
+ action,
+}: {
+ title?: string;
+ description?: string;
+ button?: any;
+ className?: string;
+ action?: () => void;
+}) => {
+ return (
+
+ {/*
*/}
+ {title && (
+
+ {title}
+
+ )}
+ {title && (
+
+ {description}
+
+ )}
+ {button && (
+
null}
+ >
+ {button}
+
+ )}
+
+ );
+};
diff --git a/components/font-family/font-family.constants.ts b/components/font-family/font-family.constants.ts
new file mode 100644
index 0000000000000000000000000000000000000000..80878c1df9aad1d5ad638ccc1c2a98c6bcb3fb5f
--- /dev/null
+++ b/components/font-family/font-family.constants.ts
@@ -0,0 +1,145 @@
+import {
+ Montserrat,
+ Cousine,
+ Roboto,
+ Lato,
+ Oswald,
+ Kanit,
+ Fugaz_One,
+ Roboto_Slab,
+ Libre_Baskerville,
+ Bree_Serif,
+ Pacifico,
+ Permanent_Marker,
+ Yellowtail,
+ Scheherazade_New,
+} from "next/font/google";
+
+const montserrat = Montserrat({
+ subsets: ["latin"],
+ weight: ["400", "700"],
+});
+
+const cousine = Cousine({
+ subsets: ["latin"],
+ weight: ["400", "700"],
+});
+
+const roboto = Roboto({
+ subsets: ["latin"],
+ weight: ["400", "700", "100", "300", "500", "900"],
+});
+
+const lato = Lato({
+ subsets: ["latin"],
+ weight: ["100", "300", "400", "700", "900"],
+});
+
+const oswald = Oswald({
+ subsets: ["latin"],
+ weight: ["200", "300", "400", "500", "600", "700"],
+});
+
+const kanit = Kanit({
+ subsets: ["latin"],
+ weight: ["100", "200", "300", "400", "500", "600", "700", "800", "900"],
+});
+
+const fugaz_one = Fugaz_One({
+ subsets: ["latin"],
+ weight: ["400"],
+});
+
+const roboto_slab = Roboto_Slab({
+ subsets: ["latin"],
+ weight: ["100", "200", "300", "400", "500", "600", "700", "800", "900"],
+});
+
+const libre_baskerville = Libre_Baskerville({
+ subsets: ["latin"],
+ weight: ["400", "700"],
+});
+
+const bree_serif = Bree_Serif({
+ subsets: ["latin"],
+ weight: ["400"],
+});
+
+const pacifico = Pacifico({
+ subsets: ["latin"],
+ weight: ["400"],
+});
+
+const permanent_marker = Permanent_Marker({
+ subsets: ["latin"],
+ weight: ["400"],
+});
+
+const yellowtail = Yellowtail({
+ subsets: ["latin"],
+ weight: ["400"],
+});
+
+const scheherazade_new = Scheherazade_New({
+ subsets: ["latin"],
+ weight: ["400", "700"],
+});
+
+export const FONT_FAMILY = [
+ {
+ label: "Montserrat",
+ value: montserrat.className,
+ },
+ {
+ label: "Cousine",
+ value: cousine.className,
+ },
+ {
+ label: "Roboto",
+ value: roboto.className,
+ },
+ {
+ label: "Lato",
+ value: lato.className,
+ },
+ {
+ label: "Oswald",
+ value: oswald.className,
+ },
+ {
+ label: "Kanit",
+ value: kanit.className,
+ },
+ {
+ label: "Fugaz One",
+ value: fugaz_one.className,
+ },
+ {
+ label: "Roboto Slab",
+ value: roboto_slab.className,
+ },
+ {
+ label: "Libre Baskerville",
+ value: libre_baskerville.className,
+ },
+ {
+ label: "Bree Serif",
+ value: bree_serif.className,
+ },
+ {
+ label: "Pacifico",
+ value: pacifico.className,
+ },
+ {
+ label: "Permanent Marker",
+ value: permanent_marker.className,
+ },
+ {
+ label: "Yellowtail",
+ value: yellowtail.className,
+ },
+ {
+ label: "Scheherazade New",
+ value: scheherazade_new.className,
+ },
+];
diff --git a/components/font-family/index.tsx b/components/font-family/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..4f638b4eb45d6b3531ae7b782f759978cef2ea51
--- /dev/null
+++ b/components/font-family/index.tsx
@@ -0,0 +1,57 @@
+import { useRef, useState } from "react";
+import { useClickAway } from "react-use";
+import classNames from "classnames";
+import { ChevronDownIcon } from "@heroicons/react/solid";
+
+import { FONT_FAMILY } from "./font-family.constants";
+
+export const FontFamilySelector = ({
+ value,
+ onSelect,
+}: {
+ value?: any;
+ onSelect: (t: string) => void;
+}) => {
+ const [open, setOpen] = useState(false);
+ const ref = useRef(null);
+ const selected = FONT_FAMILY.find((f) => f.label === value);
+
+ useClickAway(ref, () => setOpen(false));
+
+ return (
+
+
setOpen(!open)}
+ >
+ {selected?.label}
+
+
+
+
+ {FONT_FAMILY.map((font, f) => (
+
onSelect(font.label)}
+ >
+ {font.label}
+
+ ))}
+
+
+
+ );
+};
diff --git a/components/font-weight/font-weight.constants.ts b/components/font-weight/font-weight.constants.ts
new file mode 100644
index 0000000000000000000000000000000000000000..de5ce8e16e1e5fd04c9065371239bee864a372ef
--- /dev/null
+++ b/components/font-weight/font-weight.constants.ts
@@ -0,0 +1,38 @@
+export const FONT_WEIGHT = [
+ {
+ label: "Thin",
+ value: "100",
+ },
+ {
+ label: "Extra Light",
+ value: "200",
+ },
+ {
+ label: "Light",
+ value: "300",
+ },
+ {
+ label: "Regular",
+ value: "400",
+ },
+ {
+ label: "Medium",
+ value: "500",
+ },
+ {
+ label: "Semi Bold",
+ value: "600",
+ },
+ {
+ label: "Bold",
+ value: "700",
+ },
+ {
+ label: "Extra Bold",
+ value: "800",
+ },
+ {
+ label: "Black",
+ value: "900",
+ },
+];
diff --git a/components/font-weight/index.tsx b/components/font-weight/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..d75a09c217175a2c51d493a3bc433b6bd57ebc25
--- /dev/null
+++ b/components/font-weight/index.tsx
@@ -0,0 +1,57 @@
+import { useRef, useState } from "react";
+import { useClickAway } from "react-use";
+import classNames from "classnames";
+import { ChevronDownIcon } from "@heroicons/react/solid";
+
+import { FONT_WEIGHT } from "./font-weight.constants";
+
+export const FontWeight = ({
+ value,
+ onSelect,
+}: {
+ value?: any;
+ onSelect: (t: string) => void;
+}) => {
+ const [open, setOpen] = useState(false);
+ const ref = useRef(null);
+ const selected = FONT_WEIGHT.find((f) => f.value === value);
+
+ useClickAway(ref, () => setOpen(false));
+
+ return (
+
+
setOpen(!open)}
+ >
+ {selected?.label}
+
+
+
+
+ {FONT_WEIGHT.map((font, f) => (
+
onSelect(font.value)}
+ >
+ {font.label}
+
+ ))}
+
+
+
+ );
+};
diff --git a/components/footer/index.tsx b/components/footer/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..1192e89e8a5988677cfad0b9885494d1f810af06
--- /dev/null
+++ b/components/footer/index.tsx
@@ -0,0 +1,128 @@
+import Image from "next/image";
+import LinkComp from "next/link";
+import { FormattedMessage } from "react-intl";
+import moment from "moment";
+
+import BannerFooter from "assets/images/banner_footer.svg";
+import { SelectLanguages } from "./select-languages";
+
+export const Footer = () => {
+ return (
+
+ );
+};
diff --git a/components/footer/select-languages.tsx b/components/footer/select-languages.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..59ae711eaf5ba71d2230a3e834bdd3c07f1e9e9a
--- /dev/null
+++ b/components/footer/select-languages.tsx
@@ -0,0 +1,70 @@
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faCaretDown } from "@fortawesome/free-solid-svg-icons";
+import { useRef, useState } from "react";
+import { useClickAway } from "react-use";
+import router, { useRouter } from "next/router";
+
+import FLAGS from "components/langs";
+import classNames from "classnames";
+
+export const SelectLanguages = () => {
+ const { locale } = useRouter();
+
+ const ref = useRef(null);
+ const [open, setOpen] = useState(false);
+
+ useClickAway(ref, () => setOpen(false));
+
+ const country = FLAGS.find((flag) => flag.code === locale) ?? FLAGS[0];
+ const SelectedCountry = country?.icon || (() => null);
+
+ return (
+
+
setOpen(!open)}
+ >
+
+ {country.name}
+
+
+
+ {FLAGS.map((lang) => {
+ const Component = lang.icon;
+ return (
+
{
+ // change locale of the app
+ router.push(router.asPath, router.asPath, {
+ locale: lang.code,
+ });
+ setOpen(false);
+ }}
+ >
+
+ {lang.name}
+
+ );
+ })}
+
+
+ );
+};
diff --git a/components/icon-picker/index.tsx b/components/icon-picker/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..dc7358a3b678df8b9af4fc822ef7e752c16ed9a9
--- /dev/null
+++ b/components/icon-picker/index.tsx
@@ -0,0 +1,137 @@
+import { useEffect, useRef, useState } from "react";
+import { FormattedMessage, useIntl } from "react-intl";
+
+import { Icons } from "@/components/svg/icons";
+import { IconItem } from "@/types/editor";
+import { Input } from "@/components/input";
+import {
+ useResults,
+ useSingleSearch,
+} from "@/components/search/hooks/useSearch";
+import { ListItem } from "@/components/editor-icons/comps/list/list-item";
+import { Empty } from "@/components/empty";
+import { ArrowLeftIcon } from "@heroicons/react/solid";
+import { ArrowRightIcon } from "@heroicons/react/solid";
+import { useClickAway } from "react-use";
+import classNames from "classnames";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faTrash } from "@fortawesome/free-solid-svg-icons";
+
+export const IconPicker = ({
+ icon,
+ onSelect,
+ children,
+}: {
+ onSelect: (i?: string) => void;
+ icon?: string;
+ children?: React.ReactNode;
+}) => {
+ const [open, setOpen] = useState(false);
+ const svgRef = useRef(null);
+ const container = useRef(null);
+ const intl = useIntl();
+ const [viewBox, setViewBox] = useState("0 0 24 24");
+ const [search, setSearch] = useState("");
+ useSingleSearch(search);
+ const { results, page, maxPage, totalItems, setPage } = useResults(21);
+
+ const findIcon = Icons?.find((i: IconItem) => icon === i.name);
+ const IconComponent = findIcon?.component as any;
+
+ useEffect(() => {
+ return setViewBox(
+ svgRef?.current?.getAttribute("viewBox") ?? "0 0 200 200"
+ );
+ }, [icon]);
+
+ useClickAway(container, () => setOpen(false));
+
+ return (
+
+
setOpen(!open)}
+ >
+
+
+
+
+
+ {results?.map((result: any, key: number) => (
+ {
+ // if (icon?.isPremium && !user?.id) return setOpen(true);
+ // onSelect(icon);
+ onSelect(icon.name);
+ }}
+ />
+ ))}
+ {results?.length === 0 && (
+ {
+ // if (!user?.id) return login();
+ // send to api a request;
+ }}
+ button="Ask for a new Icon"
+ />
+ )}
+
+ {results?.length > 0 && (
+
+
+
+
+
+ )}
+
+
+ );
+};
diff --git a/components/input/index.tsx b/components/input/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..d6904e4af6f23a756e27a9ed6c334f994bc7105a
--- /dev/null
+++ b/components/input/index.tsx
@@ -0,0 +1,34 @@
+export const Input = ({
+ type = "string",
+ value,
+ className,
+ prefix,
+ placeholder,
+ onChange,
+}: {
+ type?: string;
+ placeholder?: string;
+ prefix?: string;
+ className?: string;
+ value?: string | number;
+ onChange: (e: string | number) => void;
+}) => {
+ return type === "textarea" ? (
+