diff --git a/.env.example.local b/.env.example.local index 2ed5dfd..9e1a313 100644 --- a/.env.example.local +++ b/.env.example.local @@ -1,2 +1,6 @@ -NODE_MODE=development -EXPRESS_API_URL=http://127.0.0.1:3100 \ No newline at end of file +NODE_ENV=development +NEXT_PUBLIC_EXPRESS_API_URL=http://127.0.0.1:3100 +NEXT_PUBLIC_RECAPTCHA_KEY="" +NEXT_PUBLIC_GOOGLE_ANALYTICS_HOME="" +NEXT_PUBLIC_GOOGLE_ANALYTICS_PAGES="" +NEXT_PUBLIC_MICROSOFT_CLARITY="" \ No newline at end of file diff --git a/.prettierrc b/.prettierrc index 6894005..b303599 100644 --- a/.prettierrc +++ b/.prettierrc @@ -7,8 +7,6 @@ "useTabs": false, "arrowParens": "avoid", "tabWidth": 2, - "plugins": [ - "prettier-plugin-tailwindcss" - ], - "pluginSearchDirs": false + "pluginSearchDirs": false, + "plugins": ["prettier-plugin-tailwindcss"] } \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index a6af7e2..dfe4799 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,7 +30,4 @@ RUN adduser --system --uid 1001 nextjs RUN chown -R nextjs:nodejs /app/.next USER nextjs EXPOSE 3000 -CMD ["yarn", "start"] - -# create image with this command: sudo docker build . -t zoz.bio-image -# run container with this command: sudo docker run -d --name zoz.bio --network npm zoz.bio-image \ No newline at end of file +CMD ["yarn", "start"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..447cd35 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,21 @@ +version: "3" +services: + zoz.bio: + container_name: zoz.bio + build: + context: . + dockerfile: ./Dockerfile + restart: unless-stopped + environment: + NODE_ENV: production + networks: + - npm + deploy: + resources: + limits: + cpus: '2' + memory: '4GB' +networks: + npm: + external: true + name: npm \ No newline at end of file diff --git a/next.config.js b/next.config.js index a145331..b0c6800 100644 --- a/next.config.js +++ b/next.config.js @@ -2,7 +2,7 @@ const nextConfig = { reactStrictMode: false, images: { - domains: ["cdn.discordapp.com", "i.scdn.co", "live.staticflickr.com", "api.mapbox.com", "flowbite.s3.amazonaws.com"], + domains: ["api.zoz.bio", "127.0.0.1"], }, }; diff --git a/package.json b/package.json index 65f4bc7..678cdcc 100644 --- a/package.json +++ b/package.json @@ -22,13 +22,16 @@ "axios": "^1.4.0", "clsx": "^1.2.1", "cookies-next": "^2.1.1", + "css-doodle": "^0.34.9", "next": "^13.4.4", "next-auth": "^4.22.1", "nextjs-toploader": "^1.4.2", "react": "^18.2.0", "react-colorful": "^5.6.1", "react-dom": "^18.2.0", + "react-google-recaptcha": "^3.1.0", "react-hook-form": "^7.44.3", + "react-toastify": "^9.1.3", "tailwind-merge": "^1.13.0", "zod": "^3.21.4" }, @@ -36,15 +39,20 @@ "@types/node": "20.2.5", "@types/react": "18.2.7", "@types/react-dom": "18.2.4", + "@types/react-google-recaptcha": "^2.1.5", "@typescript-eslint/eslint-plugin": "^5.50.0", "@typescript-eslint/parser": "^5.50.0", "color": "^4.2.3", "eslint": "8.41.0", "eslint-config-next": "13.4.4", "postcss": "8.4.24", - "prettier": "^2.8.8", - "prettier-plugin-tailwindcss": "^0.3.0", + "prettier": "^3.0.3", + "prettier-plugin-tailwindcss": "^0.5.4", "tailwindcss": "3.3.2", "typescript": "5.0.4" + }, + "resolutions": { + "@types/react": "18.2.7", + "@types/react-dom": "18.2.4" } } diff --git a/public/banners/link-zelda.jpg b/public/banners/link-zelda.jpg new file mode 100644 index 0000000..95278c4 Binary files /dev/null and b/public/banners/link-zelda.jpg differ diff --git a/public/bg.svg b/public/bg.svg new file mode 100644 index 0000000..7aa12c6 --- /dev/null +++ b/public/bg.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/confirm.gif b/public/confirm.gif new file mode 100644 index 0000000..9858cba Binary files /dev/null and b/public/confirm.gif differ diff --git a/public/home.jpg b/public/home.jpg new file mode 100644 index 0000000..84d20a6 Binary files /dev/null and b/public/home.jpg differ diff --git a/public/home2.jpg b/public/home2.jpg new file mode 100644 index 0000000..0b8c4d0 Binary files /dev/null and b/public/home2.jpg differ diff --git a/public/home3.jpg b/public/home3.jpg new file mode 100644 index 0000000..764036c Binary files /dev/null and b/public/home3.jpg differ diff --git a/public/icons/social/flickr.png b/public/icons/social/flickr.png new file mode 100644 index 0000000..1b0e1db Binary files /dev/null and b/public/icons/social/flickr.png differ diff --git a/public/login.gif b/public/login.gif new file mode 100644 index 0000000..28f8077 Binary files /dev/null and b/public/login.gif differ diff --git a/public/metabg.png b/public/metabg.png index 89bdf6a..ebf5f0e 100644 Binary files a/public/metabg.png and b/public/metabg.png differ diff --git a/public/pfp.jpg b/public/pfp.jpg new file mode 100644 index 0000000..9e0b553 Binary files /dev/null and b/public/pfp.jpg differ diff --git a/public/register.png b/public/register.png deleted file mode 100644 index 553c919..0000000 Binary files a/public/register.png and /dev/null differ diff --git a/public/reset.gif b/public/reset.gif new file mode 100644 index 0000000..753bfd9 Binary files /dev/null and b/public/reset.gif differ diff --git a/public/teste.jpg b/public/teste.jpg new file mode 100644 index 0000000..70f0af3 Binary files /dev/null and b/public/teste.jpg differ diff --git a/public/zoz.png b/public/zoz.png index 481c1e6..a123edd 100644 Binary files a/public/zoz.png and b/public/zoz.png differ diff --git a/src/app/(BioLayout)/[username]/Bio.tsx b/src/app/(BioLayout)/[username]/Bio.tsx index df25e5d..f95ec62 100644 --- a/src/app/(BioLayout)/[username]/Bio.tsx +++ b/src/app/(BioLayout)/[username]/Bio.tsx @@ -1,43 +1,67 @@ -"use client"; +import { PageProps } from "@/types/PageProps"; +import { defaultPage } from "@/utils/BioVariables"; +import { cookies } from "next/headers"; +import Image from "next/image"; import React from "react"; -import { useQuery } from "@tanstack/react-query"; -import { getPage } from "@/services/PageService"; -import BioMain from "./BioMain"; +import BioAvatar from "./BioAvatar"; +import BioBadges from "./BioBadges"; +import BioInfos from "./BioInfos"; +import BioLinks from "./BioLinks"; +import BioNavigation from "./BioNavigation"; +import BioSocials from "./BioSocials"; +import BioCard from "./BioCard"; +import CssDoodle from "@/components/CssDoodle/CssDoodle"; -export const BioComponent = ({ username }: { username: string }) => { - const queryPage = useQuery({ - queryKey: ["getPage"], - queryFn: () => getPage(username), - }); - - if (queryPage.isError) { - const error = queryPage.error as Error; - console.log(error); - //TODO TOAST - // errorToast(error.message); - } - - if (queryPage.isLoading) { - //TODO LOADING PAGE - return
LOADING ....
; - // return ; - } +export const BioComponent = ({ page }: { page: PageProps }) => { + const cookieStore = cookies(); + const userCookie = cookieStore.get("zoz_user"); + const user = userCookie ? JSON.parse(userCookie?.value) : undefined; + const backgroundUrl = page?.backgroundUrl || null; + const backGroundOpacity = page?.backGroundOpacity || defaultPage.bgOpacity; return ( <> - {queryPage.data?.page ? ( -
- +
+ {backgroundUrl ? ( +
+ {`${page.pagename} +
+ ) : ( + + )} + +
+
+ + +
+ + + +
+
+
+ +
+ +
- ) : ( - // TODO 404 page - <> -
404
- {/*
- -
*/} - - )} +
); }; diff --git a/src/app/(BioLayout)/[username]/BioAvatar.tsx b/src/app/(BioLayout)/[username]/BioAvatar.tsx new file mode 100644 index 0000000..3a17f9d --- /dev/null +++ b/src/app/(BioLayout)/[username]/BioAvatar.tsx @@ -0,0 +1,49 @@ +import { PageProps } from "@/types/PageProps"; +import { defaultPage } from "@/utils/BioVariables"; +import { twMerge } from "tailwind-merge"; +import { memo } from "react"; +import Image from "next/image"; + +type SectionCardProps = { + page?: PageProps; + className?: string; +}; + +const BioAvatar = ({ page }: SectionCardProps) => { + const pfpUrl = page?.pfpUrl || defaultPage.pfpUrl; + const primaryColor = page?.primaryColor || defaultPage.primaryColor; + + return ( +
+ bio page avatar + bio page avatar +
+ ); +}; + +export default memo(BioAvatar); diff --git a/src/app/(BioLayout)/[username]/BioBadges.tsx b/src/app/(BioLayout)/[username]/BioBadges.tsx new file mode 100644 index 0000000..49769bd --- /dev/null +++ b/src/app/(BioLayout)/[username]/BioBadges.tsx @@ -0,0 +1,32 @@ +import { PageProps } from "@/types/PageProps"; +import { defaultPage } from "@/utils/BioVariables"; +import { getBadge } from "@/utils/IconsList"; +import { memo } from "react"; + +const BioBadges = ({ page }: { page: PageProps }) => { + const badges = page.badges || defaultPage.pageBadges; + const secondaryColor = page?.secondaryColor || defaultPage.secondaryColor; + const fontColor = page?.fontColor || defaultPage.fontColor; + + if (badges && badges.length === 0) return null; + + return ( +
+ {badges?.map((badge, idx) => + getBadge(badge) ? ( + + {getBadge(badge)?.label} + + ) : null + )} +
+ ); +}; +export default memo(BioBadges); diff --git a/src/app/(BioLayout)/[username]/BioCard.tsx b/src/app/(BioLayout)/[username]/BioCard.tsx index fa0c592..a285138 100644 --- a/src/app/(BioLayout)/[username]/BioCard.tsx +++ b/src/app/(BioLayout)/[username]/BioCard.tsx @@ -1,17 +1,16 @@ import { PageProps } from "@/types/PageProps"; import { defaultPage } from "@/utils/BioVariables"; import { twMerge } from "tailwind-merge"; -import { memo } from "react"; +import { ReactNode, memo } from "react"; type SectionCardProps = { - children: JSX.Element; + children: ReactNode; page?: PageProps; className?: string; center?: boolean; - bioPage?: boolean; }; -const BioCard = ({ children, page, className, center = true, bioPage = true }: SectionCardProps) => { +const BioCard = ({ children, page, className, center = true }: SectionCardProps) => { const primaryColor = page?.primaryColor || defaultPage.primaryColor; const cardBlur = page?.cardBlur || defaultPage.cardBlur; const cardHueRotate = page?.cardHueRotate || defaultPage.cardHueRotate; @@ -20,17 +19,15 @@ const BioCard = ({ children, page, className, center = true, bioPage = true }: S className={twMerge( cardBlur, cardHueRotate, - "relative flex flex-col", - "mb-2 rounded-xl px-2 shadow-sm shadow-black sm:px-3", + "relative flex flex-row w-full rounded-xl shadow-sm shadow-black", center ? "justify-center" : "justify-start", - bioPage ? "w-full sm:w-5/6 md:w-3/4 lg:w-3/5 lg:max-w-2xl" : "", className )} style={{ backgroundColor: `rgb(${primaryColor.r},${primaryColor.g},${primaryColor.b},${primaryColor.a})`, }} > -
{children}
+ {children}
); }; diff --git a/src/app/(BioLayout)/[username]/BioIFrames.tsx b/src/app/(BioLayout)/[username]/BioIFrames.tsx new file mode 100644 index 0000000..c096153 --- /dev/null +++ b/src/app/(BioLayout)/[username]/BioIFrames.tsx @@ -0,0 +1,75 @@ +import { LinkProps } from "@/types/LinkProps"; +import { memo } from "react"; + +// TODO - its kinda of laggy when switch folders if you hjave an iframe +// seems like the iframe is reloading everytime, find a way to save the iframe, cache, etc +// TODO - encapsulate this better +// TODO - spotify albums +const BioIFrames = ({ link }: { link: LinkProps }) => { + return ( + <> + {link.embedded === "spotify" && link.isPlaylist ? ( + + ) : link.embedded === "spotify" ? ( + + ) : link.embedded === "soundcloud" && link.url.includes("/sets/") ? ( + + ) : link.embedded === "soundcloud" ? ( + + ) : link.embedded === "youtube" && link.url.length > 20 ? ( + + ) : link.embedded === "youtube" ? ( + + ) : null} + + ); +}; +export default memo(BioIFrames); diff --git a/src/app/(BioLayout)/[username]/BioIcon.tsx b/src/app/(BioLayout)/[username]/BioIcon.tsx index ec12bd4..363cabb 100644 --- a/src/app/(BioLayout)/[username]/BioIcon.tsx +++ b/src/app/(BioLayout)/[username]/BioIcon.tsx @@ -1,25 +1,20 @@ +"use client"; import { memo } from "react"; -// import ZozTooltip from "../../components/Tooltip"; -// import { useToasts } from "../../context/ToastProvider/useToasts"; import { getSocialIcon } from "@/utils/IconsList"; +import { successToast } from "@/utils/toaster"; +import { Tooltip } from "@/components/Tooltip"; type MediaProps = { username: string; key: string; }; -type PageIconProps = { - media: MediaProps; -}; - -const PageIcon = ({ media }: PageIconProps) => { +// TODO - next/image and try to ssr this +const BioIcon = ({ media }: { media: MediaProps }) => { const social = getSocialIcon(media.key); - // const { successToast } = useToasts(); if (!social) return null; return ( -
- {/* TODO TIPSY */} - {/* */} + {social.url ? ( { target="_blank" rel="noopener noreferrer" > - {`${media.key} + {`${media.key} ) : ( {`${media.key} { - // successToast(`Copied: ${media.username}`); + successToast(`Copied: ${media.username}`); if (navigator.clipboard) { navigator.clipboard.writeText(media.username); } @@ -43,8 +38,8 @@ const PageIcon = ({ media }: PageIconProps) => { loading="lazy" /> )} -
+ ); }; -export default memo(PageIcon); +export default memo(BioIcon); diff --git a/src/app/(BioLayout)/[username]/BioInfos.tsx b/src/app/(BioLayout)/[username]/BioInfos.tsx index 8463a5e..3b3c487 100644 --- a/src/app/(BioLayout)/[username]/BioInfos.tsx +++ b/src/app/(BioLayout)/[username]/BioInfos.tsx @@ -1,55 +1,57 @@ import { memo } from "react"; -// import { useToasts } from "../../context/ToastProvider/useToasts"; import { PageProps } from "@/types/PageProps"; import { getAdornmentIcon } from "@/utils/IconsList"; +import CopyLabel from "@/components/Labels/CopyLabel"; +import { defaultPage } from "@/utils/BioVariables"; -type PageInfosProps = { - page: PageProps; -}; +const BioInfos = ({ page }: { page: PageProps }) => { + const fontColor = page?.fontColor || defaultPage.fontColor; -const PageInfos = ({ page }: PageInfosProps) => { - // const { successToast } = useToasts(); return ( - <> -
-

+
+

+ {page?.uname || "No name~"} - {page?.adornment ? ( - {getAdornmentIcon(page.adornment)?.label} - ) : null} -

- { - // successToast(`Copied`); - if (navigator.clipboard) { - navigator.clipboard.writeText(`https://zoz.bio/${page?.pagename || ""}`); - } - }} - > - {`zoz.bio/${page?.pagename || ""}`} + + {page?.adornment ? ( + {getAdornmentIcon(page.adornment)?.label} + ) : null} + +

+
+ +
+ {page?.bio && (
{page?.bio}
-
- + )} + ); }; -export default memo(PageInfos); +export default memo(BioInfos); diff --git a/src/app/(BioLayout)/[username]/BioLink.tsx b/src/app/(BioLayout)/[username]/BioLink.tsx new file mode 100644 index 0000000..7b089de --- /dev/null +++ b/src/app/(BioLayout)/[username]/BioLink.tsx @@ -0,0 +1,122 @@ +import { LinkProps } from "@/types/LinkProps"; +import { PageProps } from "@/types/PageProps"; +import { defaultPage } from "@/utils/BioVariables"; +import { getIcon } from "@/utils/IconsList"; +import clsx from "clsx"; +import Image from "next/image"; +import Link from "next/link"; +import { memo } from "react"; + +type BioLinkProps = { + page: PageProps; + link: LinkProps; + setFolderOwner: (link: LinkProps) => void; +}; + +const BannerComponent = ({ link, fontColor }: { link: LinkProps; fontColor: string }) => { + const typeCover = link.bannerUrl || ""; + + return ( + <> + {typeCover && ( + link banner + )} +
+ {`folder + {link.isFolder ? "Folder" : "Link"} +
+ + ); +}; + +const LinkComponent = ({ page, link, setFolderOwner }: BioLinkProps) => { + const fontColor = page?.fontColor || defaultPage.fontColor; + const h2ClassName = + "sm:ml-7 flex-1 flex-shrink-0 truncate whitespace-pre-wrap text-center font-bold tracking-wide overflow-visible whitespace-nowrap text-lg sm:text-xl"; + if (link.isFolder) + return ( +
{ + link.isSelected = true; + setFolderOwner(link); + }} + > +

+ {/* TODO - UX upgrade */} + {link.isSelected ? "Click to go back" : link.label} +

+
+ ); + return ( + +

+ {link.label} +

+ + ); +}; + +const BioLink = ({ page, link, setFolderOwner }: BioLinkProps) => { + const primaryColor = page?.primaryColor || defaultPage.primaryColor; + const fontColor = page?.fontColor || defaultPage.fontColor; + const cardBlur = page?.cardBlur || defaultPage.cardBlur; + const cardHueRotate = page?.cardHueRotate || defaultPage.cardHueRotate; + + const cardStyle = { + backgroundColor: `rgb(${primaryColor.r},${primaryColor.g},${primaryColor.b},${primaryColor.a})`, + }; + + return ( + <> +
+
+ +
+
+ +
+
+ +
+
+ + ); +}; + +export default memo(BioLink); diff --git a/src/app/(BioLayout)/[username]/BioLinks.tsx b/src/app/(BioLayout)/[username]/BioLinks.tsx index ba131de..66df956 100644 --- a/src/app/(BioLayout)/[username]/BioLinks.tsx +++ b/src/app/(BioLayout)/[username]/BioLinks.tsx @@ -1,15 +1,16 @@ -import SectionCard from "./BioCard"; -import { memo, useState } from "react"; -import { getIcon } from "@/utils/IconsList"; -import { PageProps } from "@/types/PageProps"; +"use client"; import { LinkProps } from "@/types/LinkProps"; -import { ArrowUpRightIcon, ArrowRightIcon, ArrowLeftIcon } from "@heroicons/react/20/solid"; +import { PageProps } from "@/types/PageProps"; +import { memo, useState } from "react"; +import BioIFrames from "./BioIFrames"; +import BioLink from "./BioLink"; +import BioCard from "./BioCard"; -type PageLinksProps = { +type BioLinksProps = { page: PageProps; }; -const PageLinks = ({ page }: PageLinksProps) => { +const BioLinks = ({ page }: BioLinksProps) => { const [folderOwner, setFolderOwner] = useState(); const pageLinks = page?.pageLinks @@ -29,151 +30,33 @@ const PageLinks = ({ page }: PageLinksProps) => { return a.position - b.position; }) : []; - return ( <> {folderOwner ? ( - -
setFolderOwner(null)} - > - {`${folderOwner.icon} -

- {folderOwner.label} -

- Click to go back - -
-
+
+ { + link.isSelected = false; + setFolderOwner(null); + }} + /> +
) : null} {pageLinks.map((link, idx) => link.embedded === "none" ? ( - <> - {link.isFolder ? ( - -
setFolderOwner(link)} - > - {`${link.icon} -

- {link.label} -

- Click to open - -
-
- ) : ( - - - {`${link.icon} -

- {link.label} -

- {link.url} - -
-
- )} - +
+ +
) : ( - <> -
- {link.embedded === "spotify" && link.url.includes("playlist") ? ( - - ) : link.embedded === "spotify" ? ( - - ) : link.embedded === "soundcloud" && link.url.includes("/sets/") ? ( - - ) : link.embedded === "soundcloud" ? ( - - ) : link.embedded === "youtube" && link.url.length > 20 ? ( - - ) : link.embedded === "youtube" ? ( - - ) : null} -
- + + + ) )} ); }; -export default memo(PageLinks); +export default memo(BioLinks); diff --git a/src/app/(BioLayout)/[username]/BioMain.tsx b/src/app/(BioLayout)/[username]/BioMain.tsx deleted file mode 100644 index d369e8f..0000000 --- a/src/app/(BioLayout)/[username]/BioMain.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import { memo } from "react"; -import PageIcon from "./BioIcon"; -import SectionCard from "./BioCard"; -import { PageProps, PagePropsSocialMedia, PagePropsStatus } from "@/types/PageProps"; -import { getBadge, getStatusIcon } from "@/utils/IconsList"; -// import { useAuth } from "../../context/AuthProvider/useAuth"; -// import { Cog6ToothIcon } from "@heroicons/react/20/solid"; -// import { BigHead } from "@bigheads/core"; -import { defaultPage, setCssVariables } from "@/utils/BioVariables"; -import PageInfos from "./BioInfos"; -import "./Page.css"; -import PageLinks from "./BioLinks"; -import { LazyLoadImage } from "@/components/Loadings"; - -const mapSocials = (pageSocialMedias: PagePropsSocialMedia[]) => { - return ( -
- {pageSocialMedias.map((media, idx) => ( - - ))} -
- ); -}; - -const mapBadges = (pageBadges: string[]) => { - return ( -
- {pageBadges.map((badge, idx) => - getBadge(badge) ? ( - - {getBadge(badge)?.label} - - ) : null - )} -
- ); -}; - -const getPageStatus = (status: PagePropsStatus) => { - const statusIcon = getStatusIcon(status.key); - return statusIcon ? ( -
- {statusIcon.label} -
- ) : null; -}; - -const getAvatar = (pfpUrl: string | undefined) => { - return ( -
- {pfpUrl ? ( - pfp - ) : ( -
- {/* */} -
- )} -
- ); -}; - -// TODO -// blur effect on loading maybe? blur-sm -const BioMain = ({ page }: { page: PageProps }) => { - // const auth = useAuth(); - const primaryColor = page?.primaryColor || defaultPage.primaryColor; - const secondaryColor = page?.secondaryColor || defaultPage.secondaryColor; - const fontColor = page?.fontColor || defaultPage.fontColor; - const pfpUrl = page?.pfpUrl || undefined; - const backgroundUrl = page?.backgroundUrl || defaultPage.bgUrl; - const backgroundSize = page?.backgroundSize || defaultPage.bgSize; - const backGroundOpacity = page?.backGroundOpacity || defaultPage.bgOpacity; - const pageSocialMedias = page?.socialMedias?.length > 0 ? page.socialMedias : defaultPage.pageSocialMedias; - - const pageBadges = page?.badges?.length > 0 ? page.badges : defaultPage.pageBadges; - const pageStatus = page?.status || defaultPage.pageStatus; - setCssVariables(primaryColor, secondaryColor, fontColor); - - return ( - <> - {/* Link to Account settings */} - {/* TODO MELHORAR ISSO AQUI */} - {/* {auth && auth.email ? ( - - - ) : null} */} - {/* Page Background */} - {/* TODO VER ISSO AQUI EM */} - - -
- {/* Page Primary Card */} - - <> - {getPageStatus(pageStatus)} - {getAvatar(pfpUrl)} -
- - {mapBadges(pageBadges)} - {mapSocials(pageSocialMedias)} -
- -
- {/* Page Other Cards */} - - {/* Bottom home link */} - -
- - ); -}; - -export default memo(BioMain); diff --git a/src/app/(BioLayout)/[username]/BioNavigation.tsx b/src/app/(BioLayout)/[username]/BioNavigation.tsx new file mode 100644 index 0000000..2b94b91 --- /dev/null +++ b/src/app/(BioLayout)/[username]/BioNavigation.tsx @@ -0,0 +1,47 @@ +"use client"; +import { PageProps } from "@/types/PageProps"; +import { memo } from "react"; +import { HomeIcon, Cog6ToothIcon, ExclamationTriangleIcon } from "@heroicons/react/20/solid"; +import { Link } from "@/components/Buttons"; +import { UserProps } from "@/types/UserProps"; + +const BioNavigation = ({ page, user, editPage }: { page: PageProps; user: UserProps; editPage?: boolean }) => { + //TODO - make a better visual here, make report dialog + if (page && user) + return ( +
+ +
+
+ + {!editPage && ( + + )} +
+ ); + + return ( +
+ +
+
+ +
+ ); +}; +export default memo(BioNavigation); diff --git a/src/app/(BioLayout)/[username]/BioSocials.tsx b/src/app/(BioLayout)/[username]/BioSocials.tsx new file mode 100644 index 0000000..55bbf0b --- /dev/null +++ b/src/app/(BioLayout)/[username]/BioSocials.tsx @@ -0,0 +1,14 @@ +import { memo } from "react"; +import BioIcon from "./BioIcon"; +import { PageProps } from "@/types/PageProps"; +import { defaultPage } from "@/utils/BioVariables"; + +const BioSocials = ({ page }: { page: PageProps }) => { + const socialMedias = page?.socialMedias?.length > 0 ? page.socialMedias : defaultPage.pageSocialMedias; + return ( +
+ {socialMedias && socialMedias.map((media, idx) => )} +
+ ); +}; +export default memo(BioSocials); diff --git a/src/app/(BioLayout)/[username]/BioStatusIcon.tsx b/src/app/(BioLayout)/[username]/BioStatusIcon.tsx new file mode 100644 index 0000000..1636679 --- /dev/null +++ b/src/app/(BioLayout)/[username]/BioStatusIcon.tsx @@ -0,0 +1,14 @@ +import { memo } from "react"; +import { PageStatusProps } from "@/types/PageProps"; +import { getStatusIcon } from "@/utils/IconsList"; + +const BioStatusIcon = ({ status }: { status: PageStatusProps }) => { + const statusIcon = getStatusIcon(status.key); + return statusIcon ? ( +
+ {statusIcon.label} +
+ ) : null; +}; + +export default memo(BioStatusIcon); diff --git a/src/app/(BioLayout)/[username]/NotFound.tsx b/src/app/(BioLayout)/[username]/NotFound.tsx new file mode 100644 index 0000000..2a254d5 --- /dev/null +++ b/src/app/(BioLayout)/[username]/NotFound.tsx @@ -0,0 +1,4 @@ +// TODO +export const NotFound = ({ username }: { username: string }) => { + return
{username} - NotFound
; +}; diff --git a/src/app/(BioLayout)/[username]/layout.tsx b/src/app/(BioLayout)/[username]/layout.tsx index 7e65201..f455fce 100644 --- a/src/app/(BioLayout)/[username]/layout.tsx +++ b/src/app/(BioLayout)/[username]/layout.tsx @@ -1,34 +1,17 @@ // import Script from "next/script"; -import axios from "axios"; -import { PageProps } from "@/types/PageProps"; -import { ZOZ_META_DESCRIPTION, ZOZ_META_TITLE } from "@/utils/Constants"; -import "@/app/globals.css"; +import "@/app/(BioLayout)/biolayout.css"; +import "react-toastify/dist/ReactToastify.css"; +import "tippy.js/animations/perspective.css"; +import "tippy.js/dist/tippy.css"; +import "tippy.js/themes/translucent.css"; +import ToastProvider from "@/providers/ToastProvider"; +import { ReactNode } from "react"; +import Analytics from "@/components/Analytics"; -export async function generateMetadata({ params }: { params: { username: string; page: PageProps } }) { - const res = await axios.get("http://127.0.0.1:3100/page", { - params: { pagename: params.username }, - }); +// METADATA EXAMPLE - https://nextjs.org/docs/app/api-reference/functions/generate-metadata - if (res && res.data?.page) { - params.page = res.data.page; - return { - title: `Username: ${params.username}`, - description: res.data.page.bio || ZOZ_META_DESCRIPTION, - // openGraph: { - // images: ['/some-specific-page-image.jpg', ...previousImages], - // }, - }; - } - - return { - title: ZOZ_META_TITLE, - description: ZOZ_META_DESCRIPTION, - }; -} - -export default function BioLayout({ children }: { children: React.ReactNode }) { - //TODO OG GRAPH IMAGE TWITTER E FB - //TODO GOOGLE ANALYTICS +export default function BioLayout({ children }: { children: ReactNode }) { + //TODO - OG GRAPH IMAGE TWITTER E FB return ( @@ -39,7 +22,16 @@ export default function BioLayout({ children }: { children: React.ReactNode }) { rel="stylesheet" /> - {children} + + {process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS_PAGES && process.env.NEXT_PUBLIC_MICROSOFT_CLARITY && ( + + )} + {children} + + ); } diff --git a/src/app/(BioLayout)/[username]/page.css b/src/app/(BioLayout)/[username]/page.css deleted file mode 100644 index c81afd3..0000000 --- a/src/app/(BioLayout)/[username]/page.css +++ /dev/null @@ -1,28 +0,0 @@ -.icon-shadow { - -webkit-filter: drop-shadow(1px 1px 1px rgba(0, 0, 0, 1)); - filter: drop-shadow(1px 1px 1px rgba(0, 0, 0, 1)); -} - -.page-font-color { - color: var(--page-font-color); -} - -.hsecondary { - color: var(--page-font-color); - opacity: 0.5; -} - -.hsecondary:hover { - color: var(--page-secondary-color); - opacity: 1; -} - -.ring-avatar { - --tw-ring-color: var(--page-secondary-color); - border-color: var(--page-secondary-color); -} - -.ring-badges { - color: var(--page-font-color); - --tw-ring-color: var(--page-secondary-color); -} diff --git a/src/app/(BioLayout)/[username]/page.tsx b/src/app/(BioLayout)/[username]/page.tsx index c52a07a..9b0e08e 100644 --- a/src/app/(BioLayout)/[username]/page.tsx +++ b/src/app/(BioLayout)/[username]/page.tsx @@ -1,14 +1,39 @@ import { QueryClientProviderComponent } from "@/providers/QueryClientProvider"; +import { fetchBioPage } from "@/services/PageService"; +import { PageProps } from "@/types/PageProps"; +import { ZOZ_META_DESCRIPTION, ZOZ_META_TITLE } from "@/utils/Constants"; import { BioComponent } from "./Bio"; +import { NotFound } from "./NotFound"; + +let pageData: PageProps | undefined = undefined; + +export async function generateMetadata({ params }: { params: { username: string } }) { + if (pageData) { + return { + // TODO - generate profile img, change title + title: `zoz.bio - ${params.username}`, + description: pageData.bio || ZOZ_META_DESCRIPTION, + // openGraph: { + // images: ['/some-specific-page-image.jpg', ...previousImages], + // }, + }; + } + + return { + title: ZOZ_META_TITLE, + description: ZOZ_META_DESCRIPTION, + }; +} + +export default async function BioPage({ params }: { params: { username: string } }) { + const res = await fetchBioPage(params.username); + pageData = res?.page; -export default function BioPage({ params }: { params: { username: string } }) { return ( - <> -
- - - -
- +
+ + {pageData ? : } + +
); } diff --git a/src/app/(BioLayout)/biolayout.css b/src/app/(BioLayout)/biolayout.css new file mode 100644 index 0000000..26d79fd --- /dev/null +++ b/src/app/(BioLayout)/biolayout.css @@ -0,0 +1,81 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --foreground-rgb: 255, 255, 255; + --background-start-rgb: 0, 0, 0; + --background-end-rgb: 0, 0, 0; + + font-family: Inter, Avenir, Helvetica, sans-serif; + font-size: clamp(0.8rem, 1vw, 1rem); + font-weight: 500; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; +} + +/* Scrollbar */ + +::-webkit-scrollbar { + width: 0; + height: 0; + background: transparent; +} + +/* */ + +html { + overflow: scroll; + overflow-x: hidden; + overflow-y: scroll; +} + +body { + /* font-family: Roboto, sans-serif; */ + color: rgb(var(--foreground-rgb)); + background-color: #141618; +} + +button:focus, +a:focus, +button:focus-visible, +a:focus-visible { + outline: 0ch; + --tw-ring-offset-width: 0px !important; +} + +.icon-shadow { + -webkit-filter: drop-shadow(1px 1px 1px rgba(0, 0, 0, 1)); + filter: drop-shadow(1px 1px 1px rgba(0, 0, 0, 1)); +} + +.arrow-card-start { + -webkit-clip-path: polygon(0% 0%, 70% 0%, 100% 50%, 70% 100%, 0% 100%); + clip-path: polygon(0% 0%, 70% 0%, 100% 50%, 70% 100%, 0% 100%); +} + +.arrow-card-end { + -webkit-clip-path: polygon(100% 0, 100% 50%, 100% 100%, 0% 100%, 6% 50%, 0% 0%); + clip-path: polygon(100% 0, 100% 50%, 100% 100%, 0% 100%, 6% 50%, 0% 0%); +} + +.home-image-fade { + -webkit-mask-image:-webkit-gradient(linear, left top, right bottom, from(rgba(0,0,0,0)), to(rgba(0,0,0,1))); + mask-image: linear-gradient(to left, rgba(0,0,0,1), rgba(0,0,0,0)); +} + +@screen sm { + .arrow-card-start { + -webkit-clip-path: polygon(0% 0%, 85% 0%, 100% 50%, 85% 100%, 0% 100%); + clip-path: polygon(0% 0%, 85% 0%, 100% 50%, 85% 100%, 0% 100%); + } + + .arrow-card-avatar { + -webkit-clip-path: polygon(0 0, 100% 0%, 75% 100%, 0% 100%); + clip-path: polygon(0 0, 100% 0%, 75% 100%, 0% 100%); + } +} \ No newline at end of file diff --git a/src/app/(BioLayout)/edit/[username]/AutoCompleteFolders.tsx b/src/app/(BioLayout)/edit/[username]/AutoCompleteFolders.tsx new file mode 100644 index 0000000..4275f5a --- /dev/null +++ b/src/app/(BioLayout)/edit/[username]/AutoCompleteFolders.tsx @@ -0,0 +1,64 @@ +import React from "react"; +import { useQuery } from "@tanstack/react-query"; +import { getFolders } from "@/services/LinkService"; +import { errorToast } from "@/utils/toaster"; +import { MediaIconProps } from "@/types/MediaIconProps"; +import { AutoComplete } from "@/components/Inputs"; +import { LinkProps } from "@/types/LinkProps"; +import { getIcon } from "@/utils/IconsList"; + +type AutoCompleteFoldersProps = { + label: string; + pagename: string; + disabled?: boolean; + iconAdornment?: JSX.Element; + selected?: string; + setSelected: (value: string) => void; +}; + +const AutoCompleteFolders = ({ + label, + disabled = false, + pagename, + selected = "", + setSelected, +}: AutoCompleteFoldersProps) => { + const queryPage = useQuery({ + queryKey: ["getFolders"], + queryFn: () => getFolders(pagename), + }); + + if (queryPage.isError) { + errorToast(queryPage.error as Error); + } + + const list = new Map([]); + list.set("", { + icon: getIcon("banned")?.icon || "", + label: "None", + }); + + if (queryPage.data?.folders) { + queryPage.data.folders.map((folder: LinkProps) => { + list.set(folder._id, { + icon: getIcon(folder.icon)?.icon || "", + label: folder.label, + }); + }); + } + + return ( + + ); +}; + +export default React.memo(AutoCompleteFolders); diff --git a/src/app/(BioLayout)/edit/[username]/ButtonCard.tsx b/src/app/(BioLayout)/edit/[username]/ButtonCard.tsx new file mode 100644 index 0000000..a56a5a7 --- /dev/null +++ b/src/app/(BioLayout)/edit/[username]/ButtonCard.tsx @@ -0,0 +1,40 @@ +import { PageProps } from "@/types/PageProps"; +import { defaultPage } from "@/utils/BioVariables"; +import { ReactNode, memo } from "react"; +import { twMerge } from "tailwind-merge"; + +type ButtonCardProps = { + label: string; + page: PageProps; + onClick: () => void; + className?: string; + iconAdornment?: ReactNode; +}; + +const ButtonCard = ({ label, className, page, onClick, iconAdornment }: ButtonCardProps) => { + const cardBlur = page?.cardBlur || defaultPage.cardBlur; + const cardHueRotate = page?.cardHueRotate || defaultPage.cardHueRotate; + const primaryColor = page?.primaryColor || defaultPage.primaryColor; + const fontColor = page?.fontColor || defaultPage.fontColor; + + return ( + + ); +}; +export default memo(ButtonCard); diff --git a/src/app/(BioLayout)/edit/[username]/Dialogs/DialogEditBadges.tsx b/src/app/(BioLayout)/edit/[username]/Dialogs/DialogEditBadges.tsx new file mode 100644 index 0000000..f0049d7 --- /dev/null +++ b/src/app/(BioLayout)/edit/[username]/Dialogs/DialogEditBadges.tsx @@ -0,0 +1,83 @@ +import { Button } from "@/components/Buttons"; +import Dialog from "@/components/Dialogs"; +import { saveBadges } from "@/services/PageService"; +import { PageProps } from "@/types/PageProps"; +import { badgeList } from "@/utils/IconsList"; +import { errorToast } from "@/utils/toaster"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { memo } from "react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +const createBadgesFormSchema = z.object({ + badges: z.string().array(), +}); + +type CreateBadgesFormData = z.infer; + +type DialogEditBadgesProps = { + isOpen: boolean; + page: PageProps; + setIsOpen: (value: boolean) => void; +}; + +const DialogEditBadges = ({ isOpen, page, setIsOpen }: DialogEditBadgesProps) => { + const submitPageBadges = (data: CreateBadgesFormData) => { + if (data.badges.length <= 10) { + saveBadges(data.badges, page.pagename) + .then(() => { + setIsOpen(false); + window.location.reload(); + }) + .catch(error => { + errorToast(error); + }); + } else { + errorToast("You can only show a maximum of 10 badges"); + } + }; + + const { + watch, + handleSubmit, + setValue, + formState: { isSubmitting }, + } = useForm({ + resolver: zodResolver(createBadgesFormSchema), + defaultValues: { badges: page.badges || [] }, + }); + + const pageBadges = watch("badges"); + return ( + +
+
+ {Array.from(badgeList).map((badge, idx) => ( + -1 ? "bg-violet-200 text-violet-800 " : "text-white"}` + } + onClick={() => { + if (pageBadges.indexOf(badge[0]) > -1) { + const newBadges = pageBadges.filter(value => { + return !(badge[0] === value); + }); + setValue("badges", newBadges); + } else { + setValue("badges", [...pageBadges, badge[0]]); + } + }} + > + {badge[1].label} + + ))} +
+
+ ); +}; + +export default memo(DialogEditBadges); diff --git a/src/app/(BioLayout)/edit/[username]/Dialogs/DialogEditInfos.tsx b/src/app/(BioLayout)/edit/[username]/Dialogs/DialogEditInfos.tsx new file mode 100644 index 0000000..7afd744 --- /dev/null +++ b/src/app/(BioLayout)/edit/[username]/Dialogs/DialogEditInfos.tsx @@ -0,0 +1,130 @@ +import { Button } from "@/components/Buttons"; +import Dialog from "@/components/Dialogs"; +import { Input } from "@/components/Inputs"; +import { checkPagename, savePageInfos } from "@/services/PageService"; +import { PageProps } from "@/types/PageProps"; +import { errorToast } from "@/utils/toaster"; +import { CheckIcon, XMarkIcon } from "@heroicons/react/20/solid"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useEffect, useState } from "react"; +import { memo } from "react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +const createInfosFormSchema = z.object({ + uname: z.string().nonempty("Display Name is required").min(1, "Display Name must have at least 4 characters"), + bio: z.string(), + pagename: z + .string() + .nonempty("Page name is required") + .min(4, "Page name must have at least 4 characters") + .refine(value => /^[a-zA-Z0-9_.]+$/.test(value), "Page Name allows only alphabets, numbers, _ or ."), +}); + +type CreateInfosFormData = z.infer; + +type DialogEditInfosProps = { + isOpen: boolean; + page: PageProps; + setIsOpen: (value: boolean) => void; +}; + +const DialogEditInfos = ({ isOpen, setIsOpen, page }: DialogEditInfosProps) => { + const [newPagename, setNewPagename] = useState(""); + const [isPagenameAvailable, setPagenameAvailable] = useState(true); + + useEffect(() => { + const pagenameQuery = setTimeout(() => { + if (page?.pagename && newPagename === page.pagename) { + setPagenameAvailable(true); + } else if (newPagename && newPagename.length > 0) + checkPagename(newPagename) + .then(response => { + setPagenameAvailable(response.isAvailable); + }) + .catch(error => { + errorToast(error); + }); + }, 300); + return () => { + clearTimeout(pagenameQuery); + }; + }, [newPagename]); + + const submitPageInfos = (data: CreateInfosFormData) => { + savePageInfos(data.uname, data.bio, page.pagename, newPagename?.length > 1 ? newPagename : page.pagename) + .then(() => { + setIsOpen(false); + window.location.reload(); + }) + .catch(err => { + errorToast(err); + }); + }; + + const { + watch, + register, + handleSubmit, + formState: { errors, isSubmitting }, + } = useForm({ + resolver: zodResolver(createInfosFormSchema), + defaultValues: { uname: page?.uname, bio: page?.bio, pagename: page?.pagename }, + }); + + if (watch("pagename") !== newPagename) setNewPagename(watch("pagename")); + + return ( + +
+ + + ) : ( + + ) + } + /> + +
+ ); +}; + +export default memo(DialogEditInfos); diff --git a/src/app/(BioLayout)/edit/[username]/Dialogs/DialogEditLink.tsx b/src/app/(BioLayout)/edit/[username]/Dialogs/DialogEditLink.tsx new file mode 100644 index 0000000..9810708 --- /dev/null +++ b/src/app/(BioLayout)/edit/[username]/Dialogs/DialogEditLink.tsx @@ -0,0 +1,198 @@ +import { Button } from "@/components/Buttons"; +import Dialog from "@/components/Dialogs/Dialog"; +import { Input, RadioGroup } from "@/components/Inputs"; +import { deleteLink, updateLink } from "@/services/LinkService"; +import { LinkProps } from "@/types/LinkProps"; +import { PageProps } from "@/types/PageProps"; +import { errorToast } from "@/utils/toaster"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { memo, useState } from "react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import AutoCompleteFolders from "../AutoCompleteFolders"; +import ConfirmationDialog from "@/components/Dialogs/ConfirmationDialog"; + +type DialogEditLinkProps = { + page: PageProps; + link: LinkProps; + setSelectedLink: (value: LinkProps | null) => void; +}; + +const createLinkFormSchema = z.object({ + _id: z.string(), + url: z.string().nonempty("Url is required"), + label: z.string().nonempty("Link label is required"), + embedded: z.string().nonempty(), + isFolder: z.boolean(), + folderOwner: z.string().nullish(), +}); + +// TODO - add banner to form +const DialogEditLink = ({ page, link, setSelectedLink }: DialogEditLinkProps) => { + const { + watch, + register, + setValue, + handleSubmit, + formState: { errors, isSubmitting }, + } = useForm({ + resolver: zodResolver(createLinkFormSchema), + defaultValues: { + _id: link._id, + url: link.url, + label: link.label, + embedded: link.embedded, + isFolder: link.isFolder, + folderOwner: link.folderOwner, + }, + }); + const [confirmationDialog, setConfirmationDialog] = useState(false); + + const submitLinkInfos = (data: LinkProps) => { + updateLink(data, page.pagename) + .then(() => { + setSelectedLink(null); + window.location.reload(); + }) + .catch(error => { + errorToast(error); + }); + }; + + const isFolder = watch("isFolder"); + return ( + { + setSelectedLink(null); + }} + > +
+ {/* TODO - move this to the side of Save Button */} +
+ ); +}; + +export default memo(DialogEditLink); diff --git a/src/app/(BioLayout)/edit/[username]/Dialogs/DialogEditSocials.tsx b/src/app/(BioLayout)/edit/[username]/Dialogs/DialogEditSocials.tsx new file mode 100644 index 0000000..9494775 --- /dev/null +++ b/src/app/(BioLayout)/edit/[username]/Dialogs/DialogEditSocials.tsx @@ -0,0 +1,186 @@ +import { memo, useEffect, useState } from "react"; +import { PageProps, PageSocialMediaProps } from "@/types/PageProps"; +import { PlusIcon, XMarkIcon } from "@heroicons/react/20/solid"; +import Dialog from "@/components/Dialogs"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import { errorToast, successToast } from "@/utils/toaster"; +import { saveSocialMedia } from "@/services/PageService"; +import { Button } from "@/components/Buttons"; +import BioIcon from "@/app/(BioLayout)/[username]/BioIcon"; +import { getSocialIcon, socialIconsList } from "@/utils/IconsList"; +import { AutoComplete, Input } from "@/components/Inputs"; + +const createSocialsFormSchema = z.object({ + account: z.string().nonempty("This field is a required"), +}); + +type CreateSocialsFormData = z.infer; + +type DialogEditSocialsProps = { + isOpen: boolean; + page: PageProps; + setIsOpen: (value: boolean) => void; +}; + +// TODO - refactor +const DialogEditSocials = ({ isOpen, page, setIsOpen }: DialogEditSocialsProps) => { + const [items, setItems] = useState(); + const [mediaSelected, setMediaSelected] = useState("discord"); + + useEffect(() => { + if (isOpen && page && page.socialMedias) { + setItems(Object.assign([], page.socialMedias)); + } + }, [isOpen]); + + const submitNewSocial = (data: CreateSocialsFormData) => { + if (data.account && items && items.length < 25) { + if (items?.some(item => item.key === mediaSelected)) { + errorToast("This account has already been added!"); + } else { + const newItems = Object.assign([], items); + newItems?.push({ + key: mediaSelected, + username: data.account, + }); + setItems(newItems); + } + } + }; + + const { + watch, + register, + handleSubmit, + formState: { errors, isSubmitting }, + } = useForm({ + resolver: zodResolver(createSocialsFormSchema), + defaultValues: { account: "" }, + }); + + const account = watch("account"); + + return ( + +
+ {/* SOCIALS ADDED */} +
+
{items?.length || "0"}/25
+ {items?.map((item, idx) => ( +
+ +
+ { + const newItems = items.filter((value: PageSocialMediaProps) => { + return !(item.key === value.key && item.username === value.username); + }); + setItems(newItems); + }} + /> +
+
+ ))} +
+ {/* SOCIALS INPUTS */} +
+ +
+
+ +
+ {mediaSelected ? ( +
+ {getSocialIcon(mediaSelected)?.url?.(account) ? ( + <> + ↪ will open this url +
+ + {getSocialIcon(mediaSelected)?.url?.("")} + + {account || "😠 fill the required input"} + {account || "😡 fill the required input"} + + + + ) : ( + <> + ↪ will be copied to clipboard: +
+ { + successToast(`Copied: ${account}`); + if (navigator.clipboard) { + navigator.clipboard.writeText(account); + } + }} + > + {account || "😠 fill the required input"} + {account || "😡 fill the required input"} + + + )} +
+ ) : null} + +
+
+ ); +}; + +export default memo(DialogEditSocials); diff --git a/src/app/(BioLayout)/edit/[username]/Dialogs/DialogNewLink.tsx b/src/app/(BioLayout)/edit/[username]/Dialogs/DialogNewLink.tsx new file mode 100644 index 0000000..9bc394a --- /dev/null +++ b/src/app/(BioLayout)/edit/[username]/Dialogs/DialogNewLink.tsx @@ -0,0 +1,150 @@ +import { Button } from "@/components/Buttons"; +import Dialog from "@/components/Dialogs/Dialog"; +import { Input, RadioGroup } from "@/components/Inputs"; +import { createLink } from "@/services/LinkService"; +import { LinkProps } from "@/types/LinkProps"; +import { PageProps } from "@/types/PageProps"; +import { errorToast } from "@/utils/toaster"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { memo } from "react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import AutoCompleteFolders from "../AutoCompleteFolders"; + +type DialogNewLinkProps = { + isOpen: boolean; + page: PageProps; + setIsOpen: (value: boolean) => void; +}; + +const createLinkFormSchema = z.object({ + url: z.string().nonempty("Url is required"), + label: z.string().nonempty("Link label is required"), + embedded: z.string().nonempty(), + isFolder: z.boolean(), + folderOwner: z.string(), +}); + +// TODO - add banner to form +const DialogNewLink = ({ isOpen, page, setIsOpen }: DialogNewLinkProps) => { + const { + watch, + register, + setValue, + handleSubmit, + formState: { errors, isSubmitting }, + } = useForm({ + resolver: zodResolver(createLinkFormSchema), + defaultValues: { url: "", label: "", embedded: "none", isFolder: false, folderOwner: "" }, + }); + + const submitLinkInfos = (data: LinkProps) => { + createLink(data, page.pagename) + .then(() => { + setIsOpen(false); + window.location.reload(); + }) + .catch(error => { + errorToast(error); + }); + }; + + const isFolder = watch("isFolder"); + return ( + +
+ Link, + }, + { + value: "folder", + component: Folder, + }, + ]} + onChange={(value: string) => { + setValue("isFolder", value === "folder"); + setValue("embedded", "none"); + setValue("url", value === "folder" ? "/" : ""); + setValue("folderOwner", ""); + }} + /> + {/* TODO - this is bugged on mobile */} + None, + }, + { + value: "spotify", + component: Spotify, + color: "#1ed760", + }, + { + value: "youtube", + component: Youtube, + color: "#fe0000", + }, + { + value: "soundcloud", + component: Soundcloud, + color: "#ff5500", + }, + ]} + onChange={(value: string) => { + setValue("embedded", value); + }} + /> + { + setValue("folderOwner", value); + }} + /> + + +
+ ); +}; + +export default memo(DialogNewLink); diff --git a/src/app/(RootLayout)/account/DialogNewPage.tsx b/src/app/(BioLayout)/edit/[username]/Dialogs/DialogNewPage.tsx similarity index 64% rename from src/app/(RootLayout)/account/DialogNewPage.tsx rename to src/app/(BioLayout)/edit/[username]/Dialogs/DialogNewPage.tsx index ced8ea7..59c03e5 100644 --- a/src/app/(RootLayout)/account/DialogNewPage.tsx +++ b/src/app/(BioLayout)/edit/[username]/Dialogs/DialogNewPage.tsx @@ -1,14 +1,14 @@ -import { memo, useEffect, useState } from "react"; -// import { useToasts } from "../../context/ToastProvider/useToasts"; -import { PageProps } from "@/types/PageProps"; -import { CheckIcon, XMarkIcon } from "@heroicons/react/20/solid"; +import { Button } from "@/components/Buttons"; +import Dialog from "@/components/Dialogs"; import { Input } from "@/components/Inputs"; import { checkPagename, createPage } from "@/services/PageService"; -import { Button } from "@/components/Buttons"; +import { PageProps } from "@/types/PageProps"; +import { errorToast, successToast } from "@/utils/toaster"; +import { CheckIcon, XMarkIcon } from "@heroicons/react/20/solid"; import { zodResolver } from "@hookform/resolvers/zod"; +import { memo, useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; -import Dialog from "@/components/Dialogs"; type DialogNewPageProps = { isOpen: boolean; @@ -41,7 +41,7 @@ const createPageFormSchema = z.object({ .string() .nonempty("Page name is required") .min(4, "Page name must have at least 4 characters") - .refine(value => /^[a-zA-Z]+[-'s]?[a-zA-Z ]+$/.test(value), "Name should contain only alphabets"), + .refine(value => /^[a-zA-Z0-9_.]+$/.test(value), "Page Name allows only alphabets, numbers, _ or ."), }); type CreatePageFormData = z.infer; @@ -50,7 +50,6 @@ const DialogNewPage = ({ isOpen, setIsOpen, addNewPage }: DialogNewPageProps) => const [pagename, setPagename] = useState(""); const [isPagenameAvailable, setPagenameAvailable] = useState(true); const [examplePagename, setExamplePagename] = useState(""); - // const { errorToast, successToast } = useToasts(); const { watch, @@ -77,9 +76,8 @@ const DialogNewPage = ({ isOpen, setIsOpen, addNewPage }: DialogNewPageProps) => .then(response => { setPagenameAvailable(response.isAvailable); }) - .catch(error => { - console.log(error); - // errorToast(error.message); + .catch(err => { + errorToast(err); }); }, 300); return () => { @@ -87,16 +85,15 @@ const DialogNewPage = ({ isOpen, setIsOpen, addNewPage }: DialogNewPageProps) => }; }, [pagename]); - const createNewPage = (data: CreatePageFormData) => { + const submitNewPage = (data: CreatePageFormData) => { createPage(data.pagename) .then(res => { addNewPage(res.page); - // successToast("Page successfully created."); + successToast("Page successfully created."); setIsOpen(false); }) - .catch(error => { - console.log(error); - // errorToast(error.message); + .catch(err => { + errorToast(err); }); }; @@ -114,45 +111,44 @@ const DialogNewPage = ({ isOpen, setIsOpen, addNewPage }: DialogNewPageProps) => >

- To prevent a BOT rush to get all short size page names, you can only create pages with at least 4 characters. - If you still wants a short page name, there's some ways to get one: + To prevent a rush of BOTs trying to claim all short-sized page names, you can only create pages with at least + 4 characters. However, if you still want a short page name, there are a few ways to obtain one:
- 1. Subscriptions. (SOON) + 1. Subscriptions. (COMING SOON)
- 2. Prove that you owns that pagename in some other social medias like Instagram, Twitter, TikTok, etc. + 2. Prove that you own that page name on other social media platforms such as Instagram, Twitter, TikTok, etc.
- 3. Future events on discord. (SOON) + 3. Future events on Discord. (COMING SOON)
- 4. Be a nicely and lovely person on our discord server maybe?! 😳 + 4. Maybe be a nice and lovely person on our Discord server?! 😳

- You can also create up to 2 pages per account without any subscription. + Additionally, you can create up to 2 pages per account without any subscription.

-
-
- - ) : ( - - ) - } - /> -
+ + + ) : ( + + ) + } + /> - + {showPickers ? : } + +
+ {rgbaColor ? ( + + ) : ( + + )} + {/* TODO - refactor this button */} + +
+ ); }; -type PageEditColorsProps = { - page: PageProps; - setPage: (page: PageProps) => void; -}; - -const PageEditColors = ({ page, setPage }: PageEditColorsProps) => { - const { errorToast, successToast } = useToasts(); - +const EditColors = ({ page }: { page: PageProps }) => { const primaryColor = page?.primaryColor || defaultPage.primaryColor; const secondaryColor = page?.secondaryColor || defaultPage.secondaryColor; const fontColor = page?.fontColor || defaultPage.fontColor; @@ -146,14 +132,11 @@ const PageEditColors = ({ page, setPage }: PageEditColorsProps) => { font: false, }); - const updateColors = (value: SubmitProps) => { + const changeColors = (value: SubmitProps) => { setIsSubmitting({ ...isSubmitting, ...value }); - pageService - .updateColors(rgbaPrimaryColor, rgbaSecondaryColor, hexFontColor, page.pagename) - .then(response => { - successToast(response.message); - setIsSubmitting({ primary: false, secondary: false, font: false }); - setPage(response.page); + updateColors(rgbaPrimaryColor, rgbaSecondaryColor, hexFontColor, page.pagename) + .then(() => { + window.location.reload(); }) .catch(error => { errorToast(error.message); @@ -161,8 +144,8 @@ const PageEditColors = ({ page, setPage }: PageEditColorsProps) => { }; return ( -
-
+
+
Edit colors
@@ -175,7 +158,7 @@ const PageEditColors = ({ page, setPage }: PageEditColorsProps) => { isSubmitting={isSubmitting.primary} onClick={() => { if (showPickers.primary) { - updateColors({ primary: true }); + changeColors({ primary: true }); } setShowPickers({ primary: !showPickers.primary, @@ -196,7 +179,7 @@ const PageEditColors = ({ page, setPage }: PageEditColorsProps) => { isSubmitting={isSubmitting.secondary} onClick={() => { if (showPickers.secondary) { - updateColors({ secondary: true }); + changeColors({ secondary: true }); } setShowPickers({ primary: false, @@ -217,7 +200,7 @@ const PageEditColors = ({ page, setPage }: PageEditColorsProps) => { isSubmitting={isSubmitting.font} onClick={() => { if (showPickers.font) { - updateColors({ font: true }); + changeColors({ font: true }); } setShowPickers({ primary: false, @@ -232,4 +215,4 @@ const PageEditColors = ({ page, setPage }: PageEditColorsProps) => { ); }; -export default PageEditColors; +export default memo(EditColors); diff --git a/src/app/(BioLayout)/edit/[username]/EditInfos.tsx b/src/app/(BioLayout)/edit/[username]/EditInfos.tsx new file mode 100644 index 0000000..004fccb --- /dev/null +++ b/src/app/(BioLayout)/edit/[username]/EditInfos.tsx @@ -0,0 +1,70 @@ +"use client"; +import { memo, useState } from "react"; +import { PageProps } from "@/types/PageProps"; +import { getAdornmentIcon } from "@/utils/IconsList"; +import DialogEditInfos from "./Dialogs/DialogEditInfos"; +import { PencilSquareIcon } from "@heroicons/react/24/outline"; +import { DefaultTooltip } from "@/components/Tooltip"; +import { defaultPage } from "@/utils/BioVariables"; + +const EditInfos = ({ page }: { page: PageProps }) => { + const fontColor = page?.fontColor || defaultPage.fontColor; + const [dialogEditInfos, setDialogEditInfos] = useState(false); + + return ( + +
{ + setDialogEditInfos(true); + }} + > +
+

+ + {page?.uname || "No name~"} + + {page?.adornment ? ( + {getAdornmentIcon(page.adornment)?.label} + ) : null} + + +

+ + zoz.bio/{page?.pagename} + +
+ {page?.bio} +
+
+ +
+ +
+ ); +}; + +export default memo(EditInfos); diff --git a/src/app/(BioLayout)/edit/[username]/EditLink.tsx b/src/app/(BioLayout)/edit/[username]/EditLink.tsx new file mode 100644 index 0000000..15a9f8d --- /dev/null +++ b/src/app/(BioLayout)/edit/[username]/EditLink.tsx @@ -0,0 +1,124 @@ +import { LinkProps } from "@/types/LinkProps"; +import { PageProps } from "@/types/PageProps"; +import { defaultPage } from "@/utils/BioVariables"; +import { getIcon } from "@/utils/IconsList"; +import clsx from "clsx"; +import Image from "next/image"; +import Link from "next/link"; +import { memo } from "react"; + +type EditLinkProps = { + page: PageProps; + link: LinkProps; + setFolderOwner: (link: LinkProps) => void; + editLink?: (link: LinkProps) => void; +}; + +const BannerComponent = ({ link, fontColor }: { link: LinkProps; fontColor: string }) => { + const typeCover = link.bannerUrl || ""; + + return ( + <> + {typeCover && ( + link banner + )} +
+ {`folder + Edit +
+ + ); +}; + +const LinkComponent = ({ page, link, setFolderOwner }: EditLinkProps) => { + const fontColor = page?.fontColor || defaultPage.fontColor; + const h2ClassName = + "sm:ml-7 flex-1 flex-shrink-0 truncate whitespace-pre-wrap text-center font-bold tracking-wide overflow-visible whitespace-nowrap text-lg sm:text-xl"; + if (link.isFolder) + return ( +
{ + link.isSelected = true; + setFolderOwner(link); + }} + > +

+ {/* TODO - UX upgrade */} + {link.isSelected ? "Click to go back" : link.label} +

+
+ ); + return ( + +

+ {link.label} +

+ + ); +}; + +const EditLink = ({ page, link, setFolderOwner, editLink }: EditLinkProps) => { + const primaryColor = page?.primaryColor || defaultPage.primaryColor; + const fontColor = page?.fontColor || defaultPage.fontColor; + const cardBlur = page?.cardBlur || defaultPage.cardBlur; + const cardHueRotate = page?.cardHueRotate || defaultPage.cardHueRotate; + + const cardStyle = { + backgroundColor: `rgb(${primaryColor.r},${primaryColor.g},${primaryColor.b},${primaryColor.a})`, + }; + + return ( + <> +
+
editLink && editLink(link)} + > + +
+
+ +
+
+ +
+
+ + ); +}; + +export default memo(EditLink); diff --git a/src/app/(BioLayout)/edit/[username]/EditLinks.tsx b/src/app/(BioLayout)/edit/[username]/EditLinks.tsx new file mode 100644 index 0000000..35866b0 --- /dev/null +++ b/src/app/(BioLayout)/edit/[username]/EditLinks.tsx @@ -0,0 +1,118 @@ +"use client"; +import BioIFrames from "@/app/(BioLayout)/[username]/BioIFrames"; +import { LinkProps } from "@/types/LinkProps"; +import { PageProps } from "@/types/PageProps"; +import { Cog6ToothIcon, PlusIcon } from "@heroicons/react/20/solid"; +import clsx from "clsx"; +import { memo, useState } from "react"; +import ButtonCard from "./ButtonCard"; +import DialogNewLink from "./Dialogs/DialogNewLink"; +import EditLink from "./EditLink"; +import DialogEditLink from "./Dialogs/DialogEditLink"; + +type EditLinksProps = { + page: PageProps; +}; + +const EditLinks = ({ page }: EditLinksProps) => { + const [folderOwner, setFolderOwner] = useState(); + const [selectedLink, setSelectedLink] = useState(); + const [dialogNewLink, setDialogNewLink] = useState(false); + + const pageLinks = page?.pageLinks + ? folderOwner + ? page.pageLinks + .filter(link => { + return link.folderOwner === folderOwner._id; + }) + .sort(function (a, b) { + return a.position - b.position; + }) + : page.pageLinks + .filter(link => { + return !link.folderOwner; + }) + .sort(function (a, b) { + return a.position - b.position; + }) + : []; + + return ( + <> +
+ setDialogNewLink(true)} + iconAdornment={} + /> + {page.subscription !== "none" && ( + console.log("click")} + iconAdornment={} + /> + )} +
+ {folderOwner ? ( +
+ { + link.isSelected = false; + setFolderOwner(null); + }} + editLink={() => { + setSelectedLink(folderOwner); + }} + /> +
+ ) : null} + {pageLinks.map((link, idx) => + link.embedded === "none" ? ( +
+ { + setSelectedLink(link); + }} + /> +
+ ) : ( +
+
{ + setSelectedLink(link); + }} + > + Click to Edit +
+
setSelectedLink(link)}> + +
+
+ ) + )} + + {selectedLink && ( + { + setSelectedLink(link); + }} + page={page} + link={selectedLink} + /> + )} + + ); +}; + +export default memo(EditLinks); diff --git a/src/app/(BioLayout)/edit/[username]/EditSocials.tsx b/src/app/(BioLayout)/edit/[username]/EditSocials.tsx new file mode 100644 index 0000000..518e6af --- /dev/null +++ b/src/app/(BioLayout)/edit/[username]/EditSocials.tsx @@ -0,0 +1,31 @@ +"use client"; +import BioIcon from "@/app/(BioLayout)/[username]/BioIcon"; +import { DefaultTooltip } from "@/components/Tooltip"; +import { PageProps } from "@/types/PageProps"; +import { PencilSquareIcon } from "@heroicons/react/24/outline"; +import { memo, useState } from "react"; +import DialogEditSocials from "./Dialogs/DialogEditSocials"; +import { defaultPage } from "@/utils/BioVariables"; + +const EditSocials = ({ page }: { page: PageProps }) => { + const socialMedias = page?.socialMedias?.length > 0 ? page.socialMedias : defaultPage.pageSocialMedias; + const [dialogEditSocials, setDialogEditSocials] = useState(false); + + return ( + +
{ + setDialogEditSocials(true); + }} + > +
+ {socialMedias && socialMedias.map((media, idx) => )} +
+ +
+ +
+ ); +}; +export default memo(EditSocials); diff --git a/src/app/(BioLayout)/edit/[username]/layout.tsx b/src/app/(BioLayout)/edit/[username]/layout.tsx new file mode 100644 index 0000000..e3fb0c0 --- /dev/null +++ b/src/app/(BioLayout)/edit/[username]/layout.tsx @@ -0,0 +1,37 @@ +// import Script from "next/script"; +import "@/app/(BioLayout)/biolayout.css"; +import "react-toastify/dist/ReactToastify.css"; +import "tippy.js/animations/perspective.css"; +import "tippy.js/dist/tippy.css"; +import "tippy.js/themes/translucent.css"; +import ToastProvider from "@/providers/ToastProvider"; +import { ReactNode } from "react"; +import Analytics from "@/components/Analytics"; + +// METADATA EXAMPLE - https://nextjs.org/docs/app/api-reference/functions/generate-metadata + +export default function BioLayout({ children }: { children: ReactNode }) { + //TODO - OG GRAPH IMAGE TWITTER E FB + return ( + + + + + + + + {process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS_HOME && process.env.NEXT_PUBLIC_MICROSOFT_CLARITY && ( + + )} + {children} + + + + ); +} diff --git a/src/app/(BioLayout)/edit/[username]/page.tsx b/src/app/(BioLayout)/edit/[username]/page.tsx new file mode 100644 index 0000000..7c5d857 --- /dev/null +++ b/src/app/(BioLayout)/edit/[username]/page.tsx @@ -0,0 +1,40 @@ +import { QueryClientProviderComponent } from "@/providers/QueryClientProvider"; +import { fetchEditPage } from "@/services/PageService"; +import { PageProps } from "@/types/PageProps"; +import { ZOZ_META_DESCRIPTION, ZOZ_META_TITLE } from "@/utils/Constants"; +import { cookies } from "next/headers"; +import { NotFound } from "../../[username]/NotFound"; +import { EditComponent } from "./Edit"; + +let pageData: PageProps | undefined = undefined; + +export async function generateMetadata({ params }: { params: { username: string } }) { + if (pageData) { + return { + // TODO - generate profile img, change title + title: `zoz.bio - ${params.username}`, + description: pageData.bio || ZOZ_META_DESCRIPTION, + // openGraph: { + // images: ['/some-specific-page-image.jpg', ...previousImages], + // }, + }; + } + + return { + title: ZOZ_META_TITLE, + description: ZOZ_META_DESCRIPTION, + }; +} + +export default async function BioPage({ params }: { params: { username: string } }) { + const res = await fetchEditPage(params.username, cookies().toString()); + pageData = res?.page; + + return ( +
+ + {pageData ? : } + +
+ ); +} diff --git a/src/app/(RootLayout)/about/page.tsx b/src/app/(RootLayout)/about/page.tsx index 5a29a50..2b2f53a 100644 --- a/src/app/(RootLayout)/about/page.tsx +++ b/src/app/(RootLayout)/about/page.tsx @@ -1,3 +1,3 @@ export default function AboutPage() { - return
about page
; + return
about page
; } diff --git a/src/app/(RootLayout)/account/Account.tsx b/src/app/(RootLayout)/account/Account.tsx index 2e7113e..48b8622 100644 --- a/src/app/(RootLayout)/account/Account.tsx +++ b/src/app/(RootLayout)/account/Account.tsx @@ -1,63 +1,20 @@ "use client"; -import { useEffect, useState } from "react"; -import { useQuery } from "@tanstack/react-query"; +import { getAccount, sendConfirmEmail } from "@/services/AccountService"; import { PageProps } from "@/types/PageProps"; import { UserProps } from "@/types/UserProps"; -// import { useToasts } from "../../context/ToastProvider/useToasts"; -import Tabs from "@/components/Tabs"; -// import { Cog6ToothIcon } from "@heroicons/react/20/solid"; -// import { BigHead } from "@bigheads/core"; -import PageEdit from "./PageEdit"; -import DialogNewPage from "./DialogNewPage"; -import AccountTabSettings from "./AccountTabSettings"; -// import { LazyLoadImage } from "@/components/Loadings"; -import BioCard from "@/app/(BioLayout)/[username]/BioCard"; -import { getAccount } from "@/services/AccountService"; -import Tooltip from "@/components/Tooltip/Tooltip"; - -const accountSettings = (account: UserProps) => { - return ( - -
- -
-
- ); -}; - -const accountSubscription = (account: UserProps) => { - return ( - -
SOON
-
- ); -}; +import { errorToast, successToast } from "@/utils/toaster"; +import { useQuery } from "@tanstack/react-query"; +import { deleteCookie } from "cookies-next"; +import { useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; +import { AccountTabs } from "./AccountTabs"; +import { LabelButton } from "@/components/Buttons"; +// TODO - maybe change this to server component const AccountComponent = () => { - const [page, setPage] = useState(); const [pages, setPages] = useState(); - const [account, setAccount] = useState(); - const [dialogNewPageOpen, setDialogNewPageOpen] = useState(false); - // const { errorToast } = useToasts(); - - const queryAccount = useQuery({ - queryKey: ["getAccount"], - queryFn: () => getAccount(), - }); - - if (queryAccount.isError) { - const error = queryAccount.error as Error; - console.log(error); - // errorToast(error.message); - } - - if (queryAccount.data?.pages && !pages) { - setPages(queryAccount.data.pages); - } - if (queryAccount.data?.user && !account) { - console.log(account); - setAccount(queryAccount.data.user); - } + const [account, setAccount] = useState(); + const router = useRouter(); useEffect(() => { const scrollContainer = document.getElementById("pages"); @@ -67,110 +24,63 @@ const AccountComponent = () => { scrollContainer.scrollLeft += evt.deltaY; }); } - }); //fix this use effect later add dependency + }); //TODO - fix this use effect later add dependency const addNewPage = (page: PageProps) => { if (pages) setPages([...pages, page]); }; - return ( -
- {page ? ( -
pagedit
- ) : ( - // { - // setPage(page); - // setPages( - // pages?.map(item => { - // return item.pagename === page?.pagename ? page : item; - // }) - // ); - // }} - // /> - <> -
-
- - Create a new page or click on the page you want to edit -
+ // TODO - query cache + const queryAccount = useQuery({ + queryKey: ["getAccount"], + queryFn: () => getAccount(), + }); -
-
setDialogNewPageOpen(true)} - className="group mx-1 my-2 flex h-24 w-24 flex-shrink-0 cursor-pointer flex-col items-center justify-center rounded-full bg-secondary/30 ring-4 ring-violet-200/80 hover:bg-secondary/50 hover:ring-white" - > - + - New Page -
+ if (queryAccount.isError) { + if (queryAccount.error === "Unauthorized") { + errorToast("Session expired, please sign in again"); + deleteCookie("zoz_user"); + router.refresh(); + router.push("/login"); + } else { + errorToast(queryAccount.error as Error); + } + } - {pages - ? pages.map((page: PageProps, idx: number) => ( -
- {/* - {page.pagename} - */} + if (queryAccount.data?.pages && !pages) { + setPages(queryAccount.data.pages); + } - - {page.pfpUrl ? ( - setPage(page)} - alt="pfp" - loading="lazy" - /> - ) : ( -
setPage(page)} - > - {/* */} -
- )} -
-
- )) - : null} -
+ if (queryAccount.data?.user && !account) { + setAccount(queryAccount.data.user); + } -
- {/* TODO melhorar essa tabs */} - -
-
- - + return ( +
+ {account && !account.isEmailConfirmed && ( +
+ {/* TODO - send confirmation email and validade time to be 1min cooldown */} + + We noticed that your email has not been confirmed yet. So your account is limited, and can be suspended + without further notice. Check your spam folder if you can't find it, or{" "} + { + sendConfirmEmail(account.email || "", "aa") + .then(() => { + successToast("Confirmation email successfully sended, check your inbox or spam directory"); + }) + .catch(err => { + errorToast(err); + }); + }} + /> + +
)} +
); }; diff --git a/src/app/(RootLayout)/account/AccountTabSettings.tsx b/src/app/(RootLayout)/account/AccountTabSettings.tsx index 106b539..a7e0a63 100644 --- a/src/app/(RootLayout)/account/AccountTabSettings.tsx +++ b/src/app/(RootLayout)/account/AccountTabSettings.tsx @@ -1,50 +1,28 @@ +import BioCard from "@/app/(BioLayout)/[username]/BioCard"; +import { Input } from "@/components/Inputs"; import { UserProps } from "@/types/UserProps"; import { memo } from "react"; const AccountTabSettings = ({ account }: { account?: UserProps }) => { return ( - <> -
-
- {account?.email} - {/* console.log(1)} - bgColor="bg-violet-900 bg-opacity-25" - // onChange={formik.handleChange} - // onBlur={formik.handleBlur} - // errors={ - // formik.touched.cpassword && formik.errors.cpassword - // ? formik.errors.cpassword - // : undefined - // } - /> - console.log(1)} - bgColor="bg-violet-900 bg-opacity-25" + +
+
+ + + */} + />
- +
); }; diff --git a/src/app/(RootLayout)/account/AccountTabSubscriptions.tsx b/src/app/(RootLayout)/account/AccountTabSubscriptions.tsx new file mode 100644 index 0000000..86f5b50 --- /dev/null +++ b/src/app/(RootLayout)/account/AccountTabSubscriptions.tsx @@ -0,0 +1,17 @@ +import BioCard from "@/app/(BioLayout)/[username]/BioCard"; +import { UserProps } from "@/types/UserProps"; +import { memo } from "react"; + +const AccountTabSubscriptions = ({ account }: { account?: UserProps }) => { + return ( + +
+ + + +
+
+ ); +}; + +export default memo(AccountTabSubscriptions); diff --git a/src/app/(RootLayout)/account/AccountTabs.tsx b/src/app/(RootLayout)/account/AccountTabs.tsx new file mode 100644 index 0000000..7305a35 --- /dev/null +++ b/src/app/(RootLayout)/account/AccountTabs.tsx @@ -0,0 +1,96 @@ +import Tabs from "@/components/Tabs"; +import { Tooltip } from "@/components/Tooltip"; +import { PageProps } from "@/types/PageProps"; +import { UserProps } from "@/types/UserProps"; +import Image from "next/image"; +import { useState } from "react"; +import DialogNewPage from "../../(BioLayout)/edit/[username]/Dialogs/DialogNewPage"; +import AccountTabSettings from "./AccountTabSettings"; +import AccountTabSubscriptions from "./AccountTabSubscriptions"; +import { defaultPage } from "@/utils/BioVariables"; +import { useRouter } from "next/navigation"; + +type AcountTabsProps = { + account: UserProps | undefined; + pages: PageProps[] | undefined; + addNewPage: (page: PageProps) => void; +}; + +export const AccountTabs = ({ account, pages, addNewPage }: AcountTabsProps) => { + const [dialogNewPageOpen, setDialogNewPageOpen] = useState(false); + const router = useRouter(); + + return ( + <> +
+
+ + Create a new page or click on the page you want to edit +
+ +
+
setDialogNewPageOpen(true)} + className="group mx-1 my-2 flex h-20 w-20 2xl:h-24 2xl:w-24 flex-shrink-0 cursor-pointer flex-col items-center justify-center rounded-full bg-secondary/30 ring-4 ring-violet-200/80 hover:bg-secondary/50 hover:ring-white" + > + + + New Page +
+ + {pages + ? pages.map((page: PageProps, idx: number) => ( +
+ + { + router.push(`/edit/${page.pagename}`); + }} + alt="pfp" + quality={50} + placeholder="empty" + loading="lazy" + /> + +
+ )) + : null} +
+ +
+ {/* TODO melhorar essa tabs */} + , + }, + { + label: "⚠️ Subscriptions", + disabled: true, + component: , + }, + { + label: "💸 Pages Analytics", + disabled: true, + component: , + }, + { + label: "😍 🫣 🤓", + disabled: true, + component: , + }, + ]} + /> +
+
+ + + ); +}; diff --git a/src/app/(RootLayout)/account/AutoCompleteFolders.tsx b/src/app/(RootLayout)/account/AutoCompleteFolders.tsx index 488f10d..bcbf11b 100644 --- a/src/app/(RootLayout)/account/AutoCompleteFolders.tsx +++ b/src/app/(RootLayout)/account/AutoCompleteFolders.tsx @@ -1,11 +1,11 @@ -import React from "react"; +import React, { memo } from "react"; import { useQuery } from "@tanstack/react-query"; -// import { useToasts } from "../../context/ToastProvider/useToasts"; import { AutoComplete } from "@/components/Inputs"; import { LinkProps } from "@/types/LinkProps"; import { MediaIconProps } from "@/types/MediaIconProps"; import { getIcon } from "@/utils/IconsList"; import { getFolders } from "@/services/LinkService"; +import { errorToast } from "@/utils/toaster"; type AutoCompleteFoldersProps = { label: string; @@ -14,7 +14,6 @@ type AutoCompleteFoldersProps = { iconAdornment?: JSX.Element; selected?: string; setSelected: (value: string) => void; - onBlur?: (e: React.ChangeEvent) => void; }; const AutoCompleteFolders = ({ @@ -24,17 +23,13 @@ const AutoCompleteFolders = ({ selected = "", setSelected, }: AutoCompleteFoldersProps) => { - // const { errorToast } = useToasts(); - const queryPage = useQuery({ queryKey: ["getFolders"], queryFn: () => getFolders(pagename), }); if (queryPage.isError) { - const error = queryPage.error as Error; - // errorToast(error.message); - console.log(error); + errorToast(queryPage.error as Error); } const list = new Map([]); @@ -65,4 +60,4 @@ const AutoCompleteFolders = ({ ); }; -export default React.memo(AutoCompleteFolders); +export default memo(AutoCompleteFolders); diff --git a/src/app/(RootLayout)/account/DialogEditBadges.tsx b/src/app/(RootLayout)/account/DialogEditBadges.tsx deleted file mode 100644 index 862dcdf..0000000 --- a/src/app/(RootLayout)/account/DialogEditBadges.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { useEffect, useState } from "react"; -import { useToasts } from "../../context/ToastProvider/useToasts"; -import { PageProps } from "../../types/PageProps"; -import { useFormik } from "formik"; -import ZozDialog from "../../components/Dialogs"; - -type DialogEditBadgesProps = { - isOpen: boolean; - page: PageProps; - setIsOpen: (value: boolean) => void; - setPage: (value: PageProps | undefined) => void; - addNewPage?: (page: PageProps) => void; -}; - -const DialogEditBadges = ({ isOpen, page, setIsOpen, setPage }: DialogEditBadgesProps) => { - const { errorToast, successToast } = useToasts(); - const [badges, setbadges] = useState([]); - - useEffect(() => { - if (isOpen && page && page.badges) { - setbadges(Object.assign([], page.badges)); - } - }, [isOpen]); - - const formik = useFormik({ - initialValues: { - badges: [], - }, - onSubmit: values => { - if (values.badges.length < 10) { - pageService - .saveBadges(badges, page.pagename) - .then(response => { - successToast(response.message); - setPage(response.page); - setIsOpen(false); - }) - .catch(error => { - errorToast(error.message); - }) - .finally(() => { - formik.resetForm(); - formik.setSubmitting(false); - }); - } - }, - }); - - return ( - -
-
- {Array.from(badgeList).map((badge, idx) => ( - -1 ? "bg-violet-200 text-violet-800 " : "text-white"}` - } - onClick={() => { - console.log(badges.indexOf(badge[0])); - if (badges.indexOf(badge[0]) > -1) { - const newBadges = badges.filter(value => { - return !(badge[0] === value); - }); - setbadges(newBadges); - } else { - setbadges([...badges, badge[0]]); - } - }} - > - {badge[1].label} - - ))} -
- -
-
- ); -}; - -export default DialogEditBadges; diff --git a/src/app/(RootLayout)/account/DialogEditInfos.tsx b/src/app/(RootLayout)/account/DialogEditInfos.tsx deleted file mode 100644 index efad2ef..0000000 --- a/src/app/(RootLayout)/account/DialogEditInfos.tsx +++ /dev/null @@ -1,158 +0,0 @@ -import { useEffect, useState } from "react"; -import { useToasts } from "../../context/ToastProvider/useToasts"; -import { PageProps } from "../../types/PageProps"; -import { useFormik } from "formik"; -import { CheckIcon, XMarkIcon } from "@heroicons/react/20/solid"; -import React from "react"; -import { ZozInput } from "../../components/Inputs"; -import ZozDialog from "../../components/Dialogs"; -import pageService from "../../services/page.service"; -import * as yup from "yup"; - -type DialogEditInfosProps = { - isOpen: boolean; - page: PageProps; - setIsOpen: (value: boolean) => void; - setPage: (value: PageProps | undefined) => void; - addNewPage?: (page: PageProps) => void; -}; - -const DialogEditInfos = ({ isOpen, setIsOpen, page, setPage }: DialogEditInfosProps) => { - const { errorToast, successToast } = useToasts(); - const [newPagename, setNewPagename] = useState(""); - const [isPagenameAvailable, setPagenameAvailable] = useState(true); - - useEffect(() => { - if (page?.pagename) setNewPagename(page.pagename); - }, [isOpen]); - - useEffect(() => { - let pagenameQuery: any; - if (page?.pagename && newPagename === page.pagename) { - setPagenameAvailable(true); - } else { - pagenameQuery = setTimeout(() => { - if (newPagename && newPagename.length > 0) - pageService - .checkPagename(newPagename) - .then(response => { - setPagenameAvailable(response.isAvailable); - }) - .catch(error => { - errorToast(error.message); - }); - }, 300); - } - return () => { - clearTimeout(pagenameQuery); - }; - }, [newPagename]); - - const formik = useFormik({ - initialValues: { - uname: page?.uname || "", - bio: page?.bio || "", - }, - validationSchema: yup.object({ - uname: yup.string().required("Display Name is required"), - }), - onSubmit: values => { - pageService - .savePageInfos(values.uname, values.bio, page.pagename, newPagename?.length > 1 ? newPagename : page.pagename) - .then(response => { - successToast(response.message); - setPage(response.page); - setIsOpen(false); - }) - .catch(error => { - errorToast(error.message); - if (error.errors) formik.setErrors(error.errors); - }) - .finally(() => formik.setSubmitting(false)); - }, - }); - - return ( - -
-
- -
-
- -
-
- - ) : ( - - ) - } - errors={ - isPagenameAvailable - ? newPagename.length < 5 - ? "Cant use pagenames with 4 or less characters yet" - : undefined - : "Paganame already taken" - } - onChange={e => { - const pagename = e.target.value.replace(/[^a-z0-9_-]+|\s+/gim, ""); - setNewPagename(pagename); - }} - /> -
- - -
-
- ); -}; - -export default React.memo(DialogEditInfos); diff --git a/src/app/(RootLayout)/account/DialogEditSocials.tsx b/src/app/(RootLayout)/account/DialogEditSocials.tsx deleted file mode 100644 index b9be092..0000000 --- a/src/app/(RootLayout)/account/DialogEditSocials.tsx +++ /dev/null @@ -1,196 +0,0 @@ -import React, { useEffect, useState } from "react"; -import { useToasts } from "../../context/ToastProvider/useToasts"; -import { PageProps, PagePropsSocialMedia } from "../../types/PageProps"; -import { useFormik } from "formik"; -import { PlusIcon, XMarkIcon } from "@heroicons/react/20/solid"; -import { ZozAutoComplete, ZozInput } from "../../components/Inputs"; -import { getSocialIcon } from "../Page/IconsList"; -import { socialIconsList } from "../Page/IconsList"; -import ZozDialog from "../../components/Dialogs"; -import PageIcon from "../Page/PageIcon"; -import pageService from "../../services/page.service"; -import * as yup from "yup"; - -type DialogEditSocialsProps = { - isOpen: boolean; - page: PageProps; - setIsOpen: (value: boolean) => void; - setPage: (value: PageProps | undefined) => void; - addNewPage?: (page: PageProps) => void; -}; - -const DialogEditSocials = ({ isOpen, page, setIsOpen, setPage }: DialogEditSocialsProps) => { - const { errorToast, successToast } = useToasts(); - const [items, setItems] = useState(); - const [mediaSelected, setMediaSelected] = useState("discord"); - - useEffect(() => { - if (isOpen && page && page.socialMedias) { - setItems(Object.assign([], page.socialMedias)); - } - }, [isOpen]); - - const formik = useFormik({ - initialValues: { - username: "", - }, - validationSchema: yup.object({ - username: yup.string().required("Username is a required field"), - }), - onSubmit: values => { - if (values.username && items && items.length < 30) { - if (items?.some(item => item.key === mediaSelected)) { - errorToast("This account has already been added!"); - } else { - const newItems = Object.assign([], items); - newItems?.push({ - key: mediaSelected, - username: values.username, - }); - setItems(newItems); - } - formik.resetForm(); - formik.setSubmitting(false); - } - }, - }); - - return ( - -
- {/* SOCIALS ADDED */} -
-
{items?.length || "0"}/30
- {items && items.length > 0 - ? items.map((item, idx) => ( -
- -
- { - const newItems = items.filter(value => { - return !(item.key === value.key && item.username === value.username); - }); - setItems(newItems); - }} - /> -
-
- )) - : null} -
- {/* SOCIALS INPUTS */} -
- -
-
- -
- {mediaSelected ? ( -
- {getSocialIcon(mediaSelected)?.url?.("") ? ( - - ↪ will open this url -
- - {getSocialIcon(mediaSelected)?.url?.("")} - - - {formik.values.username || "😠 fill the required input"} - - - {formik.values.username || "😡 fill the required input"} - - - -
- ) : ( - - ↪ will be copied to clipboard: -
- { - successToast(`Copied: ${formik.values.username}`); - if (navigator.clipboard) { - navigator.clipboard.writeText(formik.values.username); - } - }} - > - - {formik.values.username || "😠 fill the required input"} - - - {formik.values.username || "😡 fill the required input"} - - -
- )} -
- ) : null} - - - -
-
- ); -}; - -export default DialogEditSocials; diff --git a/src/app/(RootLayout)/account/DialogNewLink.tsx b/src/app/(RootLayout)/account/DialogNewLink.tsx deleted file mode 100644 index 632b8e1..0000000 --- a/src/app/(RootLayout)/account/DialogNewLink.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import { useToasts } from "../../context/ToastProvider/useToasts"; -import { PageProps } from "../../types/PageProps"; -import { useFormik } from "formik"; -import { ZozInput, ZozRadioGroup } from "../../components/Inputs"; -import { LinkProps } from "../../types/LinkProps"; -import ZozDialog from "../../components/Dialogs"; -import linkService from "../../services/link.service"; -import * as yup from "yup"; -import AutoCompleteFolders from "./AutoCompleteFolders"; - -type DialogNewLinkProps = { - isOpen: boolean; - page: PageProps; - setIsOpen: (value: boolean) => void; - setPage: (value: PageProps | undefined) => void; - addNewPage?: (page: PageProps) => void; -}; - -const DialogNewLink = ({ isOpen, page, setIsOpen, setPage }: DialogNewLinkProps) => { - const { errorToast, successToast } = useToasts(); - - const formik = useFormik({ - initialValues: { - url: "", - label: "", - icon: "", - embedded: "none", - isFolder: false, - folderOwner: "", - } as LinkProps, - validationSchema: yup.object({ - label: yup.string().required("Label is a required field"), - }), - onSubmit: values => { - linkService - .createLink(values, page.pagename) - .then(response => { - successToast(response.message); - setPage(response.page); - setIsOpen(false); - formik.resetForm(); - }) - .catch(error => { - errorToast(error.message); - if (error.errors) formik.setErrors(error.errors); - }) - .finally(() => formik.setSubmitting(false)); - }, - }); - - return ( - -
-
- Link, - }, - { - value: "folder", - component: Folder, - }, - ]} - onChange={(value: string) => { - formik.setFieldValue("isFolder", value === "folder"); - formik.setFieldValue("embedded", "none"); - formik.setFieldValue("url", ""); - formik.setFieldValue("folderOwner", ""); - }} - /> -
-
- None, - }, - { - value: "spotify", - component: Spotify, - color: "#1ed760", - }, - { - value: "youtube", - component: Youtube, - color: "#fe0000", - }, - { - value: "soundcloud", - component: Soundcloud, - color: "#ff5500", - }, - ]} - onChange={(value: string) => { - formik.setFieldValue("embedded", value); - }} - /> -
-
- { - formik.setFieldValue("folderOwner", value); - }} - /> -
-
- -
-
- -
- - -
-
- ); -}; - -export default DialogNewLink; diff --git a/src/app/(RootLayout)/account/PageEdit.tsx b/src/app/(RootLayout)/account/PageEdit.tsx deleted file mode 100644 index f351478..0000000 --- a/src/app/(RootLayout)/account/PageEdit.tsx +++ /dev/null @@ -1,265 +0,0 @@ -import React, { useState } from "react"; -import { PageProps, PagePropsSocialMedia, PagePropsStatus } from "../../types/PageProps"; -import { useAuth } from "../../context/AuthProvider/useAuth"; -import { Cog6ToothIcon, PencilSquareIcon, ArrowUpTrayIcon, PlusIcon } from "@heroicons/react/20/solid"; -import { BigHead } from "@bigheads/core"; -import { getBadge, getStatusIcon } from "../Page/IconsList"; -import { defaultPage, setCssVariables } from "../Page/PageVariables"; -import { useToasts } from "../../context/ToastProvider/useToasts"; -import DialogEditInfos from "./DialogEditInfos"; -import SectionCard from "../Page/SectionCard"; -import PageIcon from "../Page/PageIcon"; -import PageInfos from "../Page/PageInfos"; -import pageService from "../../services/page.service"; -import PageEditColors from "./PageEditColors"; -import DialogEditSocials from "./DialogEditSocials"; -import DialogEditBadges from "./DialogEditBadges"; -import DialogNewLink from "./DialogNewLink"; -import PageLinks from "../Page/PageLinks"; -import "../Page/Page.css"; -import { LazyLoadImage } from "../../components/Loading"; - -const mapSocials = (pageSocialMedias: PagePropsSocialMedia[]) => { - return ( -
- {pageSocialMedias.map((media, idx) => ( - - ))} -
- ); -}; - -const mapBadges = (pageBadges: string[]) => { - return ( -
- {pageBadges.map((badge, idx) => - getBadge(badge) ? ( - - {getBadge(badge)?.label} - - ) : null - )} -
- ); -}; - -const getPageStatus = (status: PagePropsStatus) => { - const statusIcon = getStatusIcon(status.key); - return statusIcon ? ( -
- {statusIcon.label} -
- ) : null; -}; - -const getAvatar = (pfpUrl: string | undefined, uploadAvatar: (value: File) => void) => { - return ( -
- -
- ); -}; - -const IconOpenDialog = ({ setDialogOpen, label }: { setDialogOpen: (value: boolean) => void; label: string }) => { - return ( -
setDialogOpen(true)}> - - ← {label} - - -
- ); -}; - -const PageEdit = ({ page, setPage }: { page: PageProps; setPage: (value: PageProps | undefined) => void }) => { - const auth = useAuth(); - const { errorToast, successToast } = useToasts(); - - const primaryColor = page?.primaryColor || defaultPage.primaryColor; - const secondaryColor = page?.secondaryColor || defaultPage.secondaryColor; - const fontColor = page?.fontColor || defaultPage.fontColor; - const pfpUrl = page?.pfpUrl || undefined; - const backgroundUrl = page?.backgroundUrl || defaultPage.bgUrl; - const backgroundSize = page?.backgroundSize || defaultPage.bgSize; - const backGroundOpacity = page?.backGroundOpacity || defaultPage.bgOpacity; - const pageSocialMedias = page?.socialMedias?.length > 0 ? page.socialMedias : defaultPage.pageSocialMedias; - - const pageBadges = page?.badges?.length > 0 ? page.badges : defaultPage.pageBadges; - const pageStatus = page?.status || defaultPage.pageStatus; - - const cardBlur = page?.cardBlur || defaultPage.cardBlur; - const cardHueRotate = page?.cardHueRotate || defaultPage.cardHueRotate; - - setCssVariables(primaryColor, secondaryColor, fontColor); - - const [dialogEditPage, setDialogEditPage] = useState(false); - const [dialogEditSocial, setDialogEditSocial] = useState(false); - const [dialogEditBadges, setDialogEditBadges] = useState(false); - const [dialogNewLink, setDialogNewLink] = useState(false); - - const uploadAvatar = (file: File) => { - pageService - .uploadAvatar(file, page.pagename) - .then(response => { - successToast(response.message); - setPage(response.page); - }) - .catch(error => { - errorToast(error.message); - }); - }; - - const uploadBackground = (file: File) => { - pageService - .uploadBackground(file, page.pagename) - .then(response => { - successToast(response.message); - setPage(response.page); - }) - .catch(error => { - errorToast(error.message); - }); - }; - - return ( - - {/* Link to Account settings */} - {auth && auth.email ? ( -
- -
- ) : null} - {/* Page Background */} - - -
-
- - -
- {/* Page Primary Card */} - - - {getPageStatus(pageStatus)} - {getAvatar(pfpUrl, uploadAvatar)} -
-
- - -
-
- {mapBadges(pageBadges)} - -
-
- {mapSocials(pageSocialMedias)} - -
-
-
-
- - - - {/* Page Other Cards */} - -
- - {/* Dialogs to edit page */} - - {/* Dialogs to edit badges */} - - {/* Dialogs to edit social media */} - - {/* Dialogs to insert a new Link */} - -
- ); -}; - -export default React.memo(PageEdit); diff --git a/src/app/(RootLayout)/account/page.tsx b/src/app/(RootLayout)/account/page.tsx index b96b68f..68b5153 100644 --- a/src/app/(RootLayout)/account/page.tsx +++ b/src/app/(RootLayout)/account/page.tsx @@ -7,6 +7,7 @@ import Main from "@/components/Main/Main"; export default function AccountPage() { const cookieStore = cookies(); const userCookie = cookieStore.get("zoz_user"); + // TODO - userCookie is enought to validate this I think, just test and remove const user const user = userCookie ? JSON.parse(userCookie?.value) : undefined; if (user) @@ -18,4 +19,5 @@ export default function AccountPage() { ); else return ; + // else return redirect("/") } diff --git a/src/app/(RootLayout)/confirm/[token]/ConfirmationButton.tsx b/src/app/(RootLayout)/confirm/[token]/ConfirmationButton.tsx new file mode 100644 index 0000000..c17f74d --- /dev/null +++ b/src/app/(RootLayout)/confirm/[token]/ConfirmationButton.tsx @@ -0,0 +1,25 @@ +"use client"; +import { LabelButton } from "@/components/Buttons"; +import { sendConfirmEmail } from "@/services/AccountService"; +import { errorToast, successToast } from "@/utils/toaster"; +import { memo } from "react"; + +const ConfirmationButton = ({ email }: { email: string }) => { + return ( + { + sendConfirmEmail(email, "aa") + .then(() => { + successToast("Confirmation email successfully sended, check your inbox or spam directory"); + }) + .catch(err => { + errorToast(err); + }); + }} + /> + ); +}; +export default memo(ConfirmationButton); diff --git a/src/app/(RootLayout)/confirm/[token]/page.tsx b/src/app/(RootLayout)/confirm/[token]/page.tsx new file mode 100644 index 0000000..50168d0 --- /dev/null +++ b/src/app/(RootLayout)/confirm/[token]/page.tsx @@ -0,0 +1,61 @@ +import { Link } from "@/components/Buttons"; +import Main from "@/components/Main/Main"; +import Redirect from "@/components/Redirect"; +import { confirmEmail } from "@/services/TokenService"; +import { cookies } from "next/headers"; +import Image from "next/image"; +import ConfirmationButton from "./ConfirmationButton"; + +export default async function ConfirmPage({ params }: { params: { token: string } }) { + const cookieStore = cookies(); + const userCookie = cookieStore.get("zoz_user"); + const user = userCookie ? JSON.parse(userCookie?.value) : undefined; + + let confirmated = false; + try { + const req = await confirmEmail(params.token); + confirmated = req.confirmated; + } catch (err) { + confirmated = false; + } + + if (user?.isEmailConfirmed) return ; + + return ( +
+ + login image + +

Account confirmation

+ {confirmated ? ( +

+ Account has been successfully confirmed{" "} + {user ? ( + + ) : ( + + )} +

+ ) : ( +

+ Token invalid or expired{" "} + {user ? ( + + ) : ( + + )} +

+ )} +
+ ); +} diff --git a/src/app/(RootLayout)/confirm/page.tsx b/src/app/(RootLayout)/confirm/page.tsx index adf5c64..da16bc2 100644 --- a/src/app/(RootLayout)/confirm/page.tsx +++ b/src/app/(RootLayout)/confirm/page.tsx @@ -1,3 +1,5 @@ +import Redirect from "@/components/Redirect"; + export default function ConfirmPage() { - return
confirm page
; + return ; } diff --git a/src/app/(RootLayout)/home/Banner.tsx b/src/app/(RootLayout)/home/Banner.tsx new file mode 100644 index 0000000..495c3ef --- /dev/null +++ b/src/app/(RootLayout)/home/Banner.tsx @@ -0,0 +1,29 @@ +import Image from "next/image"; + +const Banner = () => { + return ( +
+
+
+

+ Full customizable page just for you! +

+

+ You can match your background with the colors your like, choose custom badges and show your favorite songs. +

+
+
+ home image example +
+
+
+ ); +}; +export default Banner; diff --git a/src/app/(RootLayout)/home/Hero.tsx b/src/app/(RootLayout)/home/Hero.tsx new file mode 100644 index 0000000..f1b696b --- /dev/null +++ b/src/app/(RootLayout)/home/Hero.tsx @@ -0,0 +1,32 @@ +import { LinkButton } from "@/components/Buttons"; +import Socials from "./Socials"; +import clsx from "clsx"; +import Username from "./Username"; + +const Hero = () => { + return ( +
+
+ +

+ Link all your socials in one place +

+ +

+ Manage one or more pages with just one account, create links and folders, track the views and engagement of + your socials and links and much more. +

+ + Create Account + +
+
+ ); +}; +export default Hero; diff --git a/src/app/(RootLayout)/home/Home.tsx b/src/app/(RootLayout)/home/Home.tsx index 8112717..6640933 100644 --- a/src/app/(RootLayout)/home/Home.tsx +++ b/src/app/(RootLayout)/home/Home.tsx @@ -1,47 +1,13 @@ +import Banner from "./Banner"; +import Hero from "./Hero"; +// import Users from "./Users"; + export default function Home() { return ( -
-
-

- Payments tool for software companies -

-

- From checkout to global sales tax compliance, companies around the world use Flowbite to simplify their - payment stack. -

- - Get started - - - - - - Speak to Sales - -
-
- mockup -
-
+ <> + + + {/* */} + ); } diff --git a/src/app/(RootLayout)/home/Socials.tsx b/src/app/(RootLayout)/home/Socials.tsx new file mode 100644 index 0000000..55b49a1 --- /dev/null +++ b/src/app/(RootLayout)/home/Socials.tsx @@ -0,0 +1,24 @@ +const Socials = () => { + const ICONS = [ + "/icons/social/discord.png", + "/icons/social/facebook.png", + "/icons/social/instagram.png", + "/icons/social/twitter.png", + "/icons/social/tiktok.png", + "/icons/social/spotify.png", + "/icons/social/youtube.png", + "/icons/social/github.png", + "/icons/social/steam.png", + ]; + + return ( +
+ {ICONS.map((src, idx) => ( +
+ {`icon`} +
+ ))} +
+ ); +}; +export default Socials; diff --git a/src/app/(RootLayout)/home/Username.tsx b/src/app/(RootLayout)/home/Username.tsx new file mode 100644 index 0000000..3f93716 --- /dev/null +++ b/src/app/(RootLayout)/home/Username.tsx @@ -0,0 +1,20 @@ +"use client"; +import { useEffect } from "react"; + +const Username = () => { + const usernameInit = "username"; + + useEffect(() => { + console.log("effect"); + }, []); + + return ( +

+ zoz.bio/ + + {usernameInit} + +

+ ); +}; +export default Username; diff --git a/src/app/(RootLayout)/home/Users.tsx b/src/app/(RootLayout)/home/Users.tsx index 84f6352..f6197f4 100644 --- a/src/app/(RootLayout)/home/Users.tsx +++ b/src/app/(RootLayout)/home/Users.tsx @@ -3,14 +3,14 @@ export default function Users() {
-

Our Users

-

+

Our Users

+

Explore the whole collection of open-source web components and elements built with the utility classNamees from Tailwind

-
+
-

+

Bonnie Green

- CEO & Web Developer -

+ CEO & Web Developer +

Bonnie drives the technical strategy of the flowbite platform and brand.

-
+
-

+

Jese Leos

- CTO -

+ CTO +

Jese drives the technical strategy of the flowbite platform and brand.

-
+
-

+

Michael Gough

- Senior Front-end Developer -

+ Senior Front-end Developer +

Michael drives the technical strategy of the flowbite platform and brand.

-
+
-

+

Lana Byrd

- Marketing & Sale -

+ Marketing & Sale +

Lana drives the technical strategy of the flowbite platform and brand.

-
- -
{output}
- + + ); +}; + +export default memo(LabelButton); diff --git a/src/components/Buttons/Link.tsx b/src/components/Buttons/Link.tsx index 5ac50d8..8b779d7 100644 --- a/src/components/Buttons/Link.tsx +++ b/src/components/Buttons/Link.tsx @@ -1,23 +1,21 @@ -import { memo } from "react"; import Link from "next/link"; +import { memo, ReactNode } from "react"; import { twMerge } from "tailwind-merge"; -const LinkComponent = ({ - href, - label, - onClick, - className, - children, -}: { +type LinkComponentProps = { href: string; label?: string; + target?: string; onClick?: () => void; className?: string; - children?: JSX.Element[]; -}) => { + children?: ReactNode; +}; + +const LinkComponent = ({ href, label, target, onClick, className, children }: LinkComponentProps) => { return ( void; className?: string; - children?: JSX.Element[]; + children?: ReactNode; }) => { return ( diff --git a/src/components/Buttons/index.ts b/src/components/Buttons/index.ts index 4e8cabe..1f998d6 100644 --- a/src/components/Buttons/index.ts +++ b/src/components/Buttons/index.ts @@ -1,4 +1,5 @@ import Button from "./Button"; import Link from "./Link"; import LinkButton from "./LinkButton"; -export { Button, LinkButton, Link }; +import LabelButton from "./LabelButton"; +export { Button, LinkButton, Link, LabelButton }; diff --git a/src/components/Cards/Card.tsx b/src/components/Cards/Card.tsx new file mode 100644 index 0000000..c56f462 --- /dev/null +++ b/src/components/Cards/Card.tsx @@ -0,0 +1,7 @@ +import { ReactNode, memo } from "react"; +import { twMerge } from "tailwind-merge"; + +const Card = ({ children, className }: { children: ReactNode; className?: string }) => { + return
{children}
; +}; +export default memo(Card); diff --git a/src/components/Cards/index.ts b/src/components/Cards/index.ts new file mode 100644 index 0000000..fd206d3 --- /dev/null +++ b/src/components/Cards/index.ts @@ -0,0 +1,2 @@ +import Card from "./Card"; +export { Card }; diff --git a/src/components/CssDoodle/CssDoodle.tsx b/src/components/CssDoodle/CssDoodle.tsx new file mode 100644 index 0000000..cf0e105 --- /dev/null +++ b/src/components/CssDoodle/CssDoodle.tsx @@ -0,0 +1,50 @@ +/* eslint-disable */ +"use client"; +import Doodle from "@/utils/doodle"; +import { useEffect, useState } from "react"; +import { twMerge } from "tailwind-merge"; + +const CssDoodle = ({ className }: { className?: string }) => { + const [ready, setReady] = useState(false); + + useEffect(() => { + setReady(true); + }, []); + + return ( +
+ {ready && ( + + )} +
+ ); +}; +export default CssDoodle; diff --git a/src/components/Dialogs/ConfirmationDialog.tsx b/src/components/Dialogs/ConfirmationDialog.tsx new file mode 100644 index 0000000..97dc85a --- /dev/null +++ b/src/components/Dialogs/ConfirmationDialog.tsx @@ -0,0 +1,88 @@ +import { Dialog, Transition } from "@headlessui/react"; +import clsx from "clsx"; +import { memo } from "react"; +import { Fragment } from "react"; + +type ConfirmationDialogProps = { + isOpen: boolean; + setIsOpen: (value: boolean) => void; + message?: string | JSX.Element; + confirmText?: string; + doAfterConfirm: () => void; +}; + +const ConfirmationDialog = ({ + message, + isOpen, + confirmText = "Confirm", + setIsOpen, + doAfterConfirm, +}: ConfirmationDialogProps) => { + return ( + + setIsOpen(false)}> + +
+ + +
+
+ + + + {message || "Click confirm to proceed or cancel to go back."} + + +
+ + +
+
+
+
+
+
+
+ ); +}; + +export default memo(ConfirmationDialog); diff --git a/src/components/Dialogs/Dialog.tsx b/src/components/Dialogs/Dialog.tsx index 7c43198..944f0d8 100644 --- a/src/components/Dialogs/Dialog.tsx +++ b/src/components/Dialogs/Dialog.tsx @@ -1,9 +1,9 @@ import { Dialog, Transition } from "@headlessui/react"; -import { memo } from "react"; +import { memo, ReactNode } from "react"; import { Fragment } from "react"; type DialogComponentProps = { - children: React.ReactNode; + children: ReactNode; title?: string | JSX.Element; isOpen: boolean; setIsOpen: (value: boolean) => void; @@ -12,7 +12,7 @@ type DialogComponentProps = { const DialogComponent = ({ title, children, isOpen, setIsOpen }: DialogComponentProps) => { return ( - setIsOpen(false)}> + setIsOpen(false)}> -
+
diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx index 518ed89..df82549 100644 --- a/src/components/Footer/Footer.tsx +++ b/src/components/Footer/Footer.tsx @@ -1,22 +1,29 @@ import { memo } from "react"; import { Link } from "@/components/Buttons"; +import { DISCORD_INVITE } from "@/utils/Constants"; const FooterComponent = () => { return ( -