├── .babelrc ├── .env ├── .eslintignore ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── main.yml ├── .gitignore ├── App.tsx ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Main.tsx ├── README.md ├── abi ├── Nude.json ├── NudeDEX.json └── NudeNFT.json ├── api ├── auth.ts ├── ipfs.ts ├── login.ts ├── nft.ts ├── posts.ts ├── register.ts ├── user.ts └── verify.ts ├── components ├── AvatarCircle.tsx ├── Dropdown.tsx ├── EllipseExtras.tsx ├── Footer.tsx ├── HashtagWordCloud.tsx ├── LazyImage.tsx ├── MaticLogoText.tsx ├── Modal.tsx ├── NFTCard.tsx ├── NftReportModal.tsx ├── NudeLogoText.tsx ├── Tabs.tsx ├── VerifyStepper.tsx ├── form │ ├── FormCheckboxInput.tsx │ ├── FormFileInputButton.tsx │ ├── FormHashtagInput.tsx │ ├── FormInput.tsx │ └── FormTextArea.tsx ├── nav │ ├── NavWallet.tsx │ ├── Navbar.tsx │ └── SideNav.tsx └── notifications │ ├── NotificationCard.tsx │ └── NotificationsManager.tsx ├── global.d.ts ├── hooks └── useWallet.tsx ├── index.html ├── lib ├── blockchain.ts ├── helpers.ts ├── routes.ts └── zindex.ts ├── media ├── arrow.svg ├── candylogo.svg ├── card.png ├── defaults │ ├── catnft.png │ ├── neon-missing-profile.png │ ├── neon-missing-profile.svg │ ├── old-missing-profile.png │ └── stars-banner.png ├── favicons │ ├── android-icon-144x144.png │ ├── android-icon-192x192.png │ ├── android-icon-36x36.png │ ├── android-icon-48x48.png │ ├── android-icon-72x72.png │ ├── android-icon-96x96.png │ ├── apple-icon-114x114.png │ ├── apple-icon-120x120.png │ ├── apple-icon-144x144.png │ ├── apple-icon-152x152.png │ ├── apple-icon-180x180.png │ ├── apple-icon-57x57.png │ ├── apple-icon-60x60.png │ ├── apple-icon-72x72.png │ ├── apple-icon-76x76.png │ ├── apple-icon-precomposed.png │ ├── apple-icon.png │ ├── browserconfig.xml │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon-96x96.png │ ├── favicon.ico │ ├── manifest.json │ ├── ms-icon-144x144.png │ ├── ms-icon-150x150.png │ ├── ms-icon-310x310.png │ └── ms-icon-70x70.png ├── girl.png ├── hamburger.svg ├── icons │ ├── socials │ │ ├── color │ │ │ ├── facebook.svg │ │ │ ├── instagram.svg │ │ │ ├── linkedin.svg │ │ │ ├── twitter.svg │ │ │ └── youtube.svg │ │ └── white │ │ │ ├── discord.svg │ │ │ ├── facebook.svg │ │ │ ├── instagram.svg │ │ │ ├── linkedin.svg │ │ │ ├── medium.svg │ │ │ ├── telegram.svg │ │ │ ├── twitter.svg │ │ │ └── youtube.svg │ └── x.svg ├── loading.svg ├── login-background-ipad.svg ├── login-background-mobile.svg ├── login-background.svg ├── matic.svg ├── metamask.svg ├── missing-profile.png ├── own-me-logo.svg ├── own-me-spinner.svg ├── pink-candy.svg ├── user.png └── verified.png ├── package-lock.json ├── package.json ├── pages ├── auctionhouse │ └── AuctionHousePage.tsx ├── candyshop │ └── CandyShopPage.tsx ├── gumballmachine │ ├── Blender.tsx │ ├── Canvas.tsx │ ├── Floor.tsx │ ├── GumballMachine.tsx │ ├── GumballMachinePage.tsx │ ├── OrbitalControls │ │ └── CameraControls.tsx │ ├── gumballmachine.glb │ └── gumballmachinewithtable.glb ├── home │ └── HomePage.tsx ├── login │ ├── LoginForm.tsx │ ├── LoginPage.tsx │ └── LoginSocials.tsx ├── mint │ ├── DragDropInput.tsx │ ├── MintPage.tsx │ └── Switch.tsx ├── nft │ └── NftPage.tsx ├── nudeswap │ ├── NudeSwapBuy.tsx │ ├── NudeSwapPage.tsx │ └── NudeSwapSell.tsx ├── posts │ ├── CreatePost.tsx │ ├── PostPage.tsx │ ├── PostsList.tsx │ └── ProfilePosts.tsx ├── profile │ ├── EditProfileForm.tsx │ ├── Profile.tsx │ ├── Profile404.tsx │ ├── ProfileCard.tsx │ ├── ProfileCardList.tsx │ └── ProfilePage.tsx ├── register │ ├── RegisterForm.tsx │ └── RegisterPage.tsx └── search │ └── SearchPage.tsx ├── redux ├── hooks.tsx ├── slices │ ├── app.tsx │ ├── user.tsx │ └── wallet.tsx └── store.tsx ├── styles └── theme.ts ├── tsconfig.json ├── web3-login-flow.drawio ├── webpack.config.dev.js └── webpack.config.prod.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react", 5 | "@babel/preset-typescript" 6 | ], 7 | "plugins": [ 8 | [ 9 | "@babel/plugin-transform-runtime", 10 | { 11 | "regenerator": true 12 | } 13 | ], 14 | [ 15 | "babel-plugin-styled-components", 16 | { 17 | "displayName": true 18 | } 19 | ] 20 | ] 21 | } -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | IS_DEV=false 2 | NUDE_ADDRESS=0xDca8383B473304C316f35fD6666B9f5D03FC69B3 3 | NUDE_NFT_ADDRESS=0x46134236bF7c53E2C520B4d86cBfa267834d06Dc 4 | NUDE_DEX_ADDRESS=0xFA4b4B968Db83F1c8326204E53998422118AC75F -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | gumball-machine -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: "@typescript-eslint/parser", 4 | parserOptions: { 5 | ecmaVersion: 2020, 6 | ecmaFeatures: { 7 | jsx: true 8 | }, 9 | sourceType: "module" 10 | }, 11 | plugins: [ 12 | "react", 13 | "react-hooks", 14 | "@typescript-eslint" 15 | ], 16 | settings: { 17 | react: { 18 | pragma: "React", 19 | version: "detect" 20 | } 21 | }, 22 | extends: [ 23 | "eslint:recommended", 24 | "plugin:react-hooks/recommended", 25 | "plugin:@typescript-eslint/recommended" 26 | ], 27 | env: { 28 | browser: true, 29 | es6: true, 30 | "amd": true, 31 | "node": true 32 | }, 33 | rules: { 34 | "react-hooks/rules-of-hooks": "error", 35 | "react-hooks/exhaustive-deps": "warn", 36 | "react/prop-types": "off", 37 | 38 | // enable additional rules 39 | "indent": ["error", 4], 40 | 41 | "quotes": ["error", "double"], 42 | "semi": ["error", "always"], 43 | 44 | // override configuration set by extending "eslint:recommended" 45 | "no-empty": "warn", 46 | "no-cond-assign": ["error", "always"], 47 | 48 | // disable rules from base configurations 49 | "for-direction": "off", 50 | } 51 | }; -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Upload Website 2 | on: 3 | push: 4 | branches: [ main ] 5 | jobs: 6 | deploy: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v3 12 | 13 | - name: Configure AWS Credentials 14 | uses: aws-actions/configure-aws-credentials@v1 15 | with: 16 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 17 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 18 | aws-region: us-west-1 19 | 20 | - name: npm install && npm run build 21 | run: npm install && npm run build 22 | 23 | - name: Deploy to S3 bucket 24 | run: aws s3 sync ./dist/ s3://app.ownme.io --delete -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | prod 3 | .DS_Store 4 | typechain 5 | dist -------------------------------------------------------------------------------- /App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import "@fontsource/rock-salt"; 4 | import "@fontsource/yeseva-one"; 5 | import "@fontsource/poppins"; 6 | import "@fontsource/shadows-into-light"; 7 | import { store } from "./redux/store"; 8 | import { Provider } from "react-redux"; 9 | import Main from "./Main"; 10 | import { BrowserRouter } from "react-router-dom"; 11 | import { ThemeProvider } from "styled-components"; 12 | import { theme } from "./styles/theme"; 13 | 14 | function App() { 15 | return ( 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | ); 24 | } 25 | 26 | createRoot(document.getElementById("react-container")).render(); -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our pledge from Own Me to the Github community: 4 | 5 | Absolutely no adult content, nude content or pornography is allowed to be commited to Github. We only publish clean code. 6 | 7 | DO NOT COMMIT ANY NUDE OR SEXUAL CONTENT - CLEAN CODE ONLY OR YOU ARE BANNED, NO SECOND CHANCES, NO MERCY 8 | 9 | We as members, contributors, and leaders pledge to make participation in our 10 | community a harassment-free experience for everyone, regardless of age, body 11 | size, visible or invisible disability, ethnicity, sex characteristics, gender 12 | identity and expression, level of experience, education, socio-economic status, 13 | nationality, personal appearance, race, religion, or sexual identity 14 | and orientation. 15 | 16 | We pledge to act and interact in ways that contribute to an open, welcoming, 17 | diverse, inclusive, and healthy community. 18 | 19 | ## Our Standards 20 | 21 | Examples of behavior that contributes to a positive environment for our 22 | community include: 23 | 24 | * Demonstrating empathy and kindness toward other people 25 | * Being respectful of differing opinions, viewpoints, and experiences 26 | * Giving and gracefully accepting constructive feedback 27 | * Accepting responsibility and apologizing to those affected by our mistakes, 28 | and learning from the experience 29 | * Focusing on what is best not just for us as individuals, but for the 30 | overall community 31 | 32 | Examples of unacceptable behavior include: 33 | 34 | * The use of sexualized language or imagery, and sexual attention or 35 | advances of any kind 36 | * Trolling, insulting or derogatory comments, and personal or political attacks 37 | * Public or private harassment 38 | * Publishing others' private information, such as a physical or email 39 | address, without their explicit permission 40 | * Other conduct which could reasonably be considered inappropriate in a 41 | professional setting 42 | 43 | ## Enforcement Responsibilities 44 | 45 | Community leaders are responsible for clarifying and enforcing our standards of 46 | acceptable behavior and will take appropriate and fair corrective action in 47 | response to any behavior that they deem inappropriate, threatening, offensive, 48 | or harmful. 49 | 50 | Community leaders have the right and responsibility to remove, edit, or reject 51 | comments, commits, code, wiki edits, issues, and other contributions that are 52 | not aligned to this Code of Conduct, and will communicate reasons for moderation 53 | decisions when appropriate. 54 | 55 | ## Scope 56 | 57 | This Code of Conduct applies within all community spaces, and also applies when 58 | an individual is officially representing the community in public spaces. 59 | Examples of representing our community include using an official e-mail address, 60 | posting via an official social media account, or acting as an appointed 61 | representative at an online or offline event. 62 | 63 | ## Enforcement 64 | 65 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 66 | reported to the community leaders responsible for enforcement at 67 | ownme.contact@protonmail.com. 68 | All complaints will be reviewed and investigated promptly and fairly. 69 | 70 | All community leaders are obligated to respect the privacy and security of the 71 | reporter of any incident. 72 | 73 | ## Enforcement Guidelines 74 | 75 | Community leaders will follow these Community Impact Guidelines in determining 76 | the consequences for any action they deem in violation of this Code of Conduct: 77 | 78 | ### 1. Correction 79 | 80 | **Community Impact**: Use of inappropriate language or other behavior deemed 81 | unprofessional or unwelcome in the community. 82 | 83 | **Consequence**: A private, written warning from community leaders, providing 84 | clarity around the nature of the violation and an explanation of why the 85 | behavior was inappropriate. A public apology may be requested. 86 | 87 | ### 2. Warning 88 | 89 | **Community Impact**: A violation through a single incident or series 90 | of actions. 91 | 92 | **Consequence**: A warning with consequences for continued behavior. No 93 | interaction with the people involved, including unsolicited interaction with 94 | those enforcing the Code of Conduct, for a specified period of time. This 95 | includes avoiding interactions in community spaces as well as external channels 96 | like social media. Violating these terms may lead to a temporary or 97 | permanent ban. 98 | 99 | ### 3. Temporary Ban 100 | 101 | **Community Impact**: A serious violation of community standards, including 102 | sustained inappropriate behavior. 103 | 104 | **Consequence**: A temporary ban from any sort of interaction or public 105 | communication with the community for a specified period of time. No public or 106 | private interaction with the people involved, including unsolicited interaction 107 | with those enforcing the Code of Conduct, is allowed during this period. 108 | Violating these terms may lead to a permanent ban. 109 | 110 | ### 4. Permanent Ban 111 | 112 | **Community Impact**: Demonstrating a pattern of violation of community 113 | standards, including sustained inappropriate behavior, harassment of an 114 | individual, or aggression toward or disparagement of classes of individuals. 115 | 116 | **Consequence**: A permanent ban from any sort of public interaction within 117 | the community. 118 | 119 | ## Attribution 120 | 121 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 122 | version 2.0, available at 123 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 124 | 125 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 126 | enforcement ladder](https://github.com/mozilla/diversity). 127 | 128 | [homepage]: https://www.contributor-covenant.org 129 | 130 | For answers to common questions about this code of conduct, see the FAQ at 131 | https://www.contributor-covenant.org/faq. Translations are available at 132 | https://www.contributor-covenant.org/translations. 133 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Make a branch, create a PR, it gets reviewed... our team merges it into master. Simple as. 2 | 3 | DO NOT COMMIT ANY NUDE OR SEXUAL CONTENT - CLEAN CODE ONLY OR YOU ARE BANNED, NO SECOND CHANCES, NO MERCY 4 | 5 | Have fun. 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2022, Own Me Inc. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /Main.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import styled from "styled-components"; 3 | import { Helmet } from "react-helmet"; 4 | import { Routes, Route, Navigate, useLocation } from "react-router-dom"; 5 | import { useAppDispatch, useAppSelector } from "./redux/hooks"; 6 | import LoginPage from "./pages/login/LoginPage"; 7 | import RegisterPage from "./pages/register/RegisterPage"; 8 | import ProfilePage from "./pages/profile/ProfilePage"; 9 | import AuctionHousePage from "./pages/auctionhouse/AuctionHousePage"; 10 | import GumballMachinePage from "./pages/gumballmachine/GumballMachinePage"; 11 | import MintPage from "./pages/mint/MintPage"; 12 | import NftPage from "./pages/nft/NftPage"; 13 | import PostPage from "./pages/posts/PostPage"; 14 | import SearchPage from "./pages/search/SearchPage"; 15 | import NudeSwapPage from "./pages/nudeswap/NudeSwapPage"; 16 | import Navbar, { TOTAL_HEIGHT } from "./components/nav/Navbar"; 17 | import NotificationsManager from "./components/notifications/NotificationsManager"; 18 | import { routes } from "./lib/routes"; 19 | import { useGetInitialLoginInfoQuery } from "./api/user"; 20 | import { setInitialLoginInfo } from "./redux/slices/user"; 21 | import useWallet from "./hooks/useWallet"; 22 | 23 | const MainContainer = styled.div<{ $isLoggedIn: boolean, $isDarkMode: boolean }>` 24 | height: calc(100% - ${props => props.$isLoggedIn ? TOTAL_HEIGHT : 0}px); 25 | margin-top: ${props => props.$isLoggedIn ? TOTAL_HEIGHT : 0}px; 26 | width: 100%; 27 | overflow-y: auto; 28 | position: relative; 29 | background-color: ${props => props.$isDarkMode ? props.theme.dark.backgroundColor : props.theme.light.backgroundColor}; 30 | color: ${props => props.$isDarkMode ? props.theme.dark.textColor : props.theme.light.textColor}; 31 | transition: background 500ms ease-in, color 500ms ease-in; 32 | `; 33 | 34 | export default function Main() { 35 | const dispatch = useAppDispatch(); 36 | const location = useLocation(); 37 | const { address } = useWallet(); 38 | const { isLoggedIn, token } = useAppSelector(state => state.user); 39 | const { isDarkMode } = useAppSelector(state => state.app); 40 | 41 | const { 42 | data: initialLoginInfoData, 43 | isSuccess: isGetInitialLoginInfoSuccess, 44 | } = useGetInitialLoginInfoQuery({ token }, { 45 | skip: !token || !address, 46 | }); 47 | 48 | useEffect(() => { 49 | if (isGetInitialLoginInfoSuccess && initialLoginInfoData) { 50 | dispatch(setInitialLoginInfo(initialLoginInfoData)); 51 | } 52 | }, [dispatch, initialLoginInfoData, isGetInitialLoginInfoSuccess]); 53 | 54 | return ( 55 | 56 | 57 | Own Me | {routes[location.pathname]?.title || "App"} 58 | 59 | {isLoggedIn && } 60 | 61 | 62 | } /> 63 | } /> 64 | : 66 | } /> 67 | : 69 | } /> 70 | : 72 | } /> 73 | : 75 | } /> 76 | : 78 | } /> 79 | : 81 | } /> 82 | : 84 | } /> 85 | : 87 | } /> 88 | : 90 | } /> 91 | 92 | 93 | ); 94 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nude-dapp 2 | Frontend for the Own Me web app. 3 | 4 | > Project is dead. We had fun, but failed the business aspect. 5 | 6 | ## Dev Setup 7 | 8 | 1.) `npm install` 9 | 10 | 2.) `npm run dev` 11 | 12 | ## Rules / Code of Conduct 13 | 14 | Read the CODE_OF_CONDUCT.md for more details. 15 | 16 | **OUR MAIN RULE:** Absolutely no adult content, nude content or pornography is allowed to be committed to Github. We only publish clean code. 17 | -------------------------------------------------------------------------------- /api/auth.ts: -------------------------------------------------------------------------------- 1 | import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"; 2 | 3 | interface AuthRequest { 4 | address: string; 5 | signature: string; 6 | nonce: string; 7 | } 8 | 9 | interface AuthResponse { 10 | address: string; 11 | message: string; 12 | token: string; 13 | } 14 | 15 | export const authApi = createApi({ 16 | reducerPath: "authApi", 17 | baseQuery: fetchBaseQuery({ baseUrl: process.env.IS_DEV === "true" ? "http://localhost:3000/" : "https://api.ownme.io/" }), 18 | endpoints: (builder) => ({ 19 | postAuth: builder.mutation({ 20 | query: ({ address, signature, nonce }) => ({ 21 | url: "auth/", 22 | method: "POST", 23 | body: { 24 | address, 25 | signature, 26 | nonce 27 | } 28 | }) 29 | }) 30 | }) 31 | }); 32 | 33 | export const { usePostAuthMutation } = authApi; -------------------------------------------------------------------------------- /api/ipfs.ts: -------------------------------------------------------------------------------- 1 | import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"; 2 | 3 | export const ipfsApi = createApi({ 4 | reducerPath: "ipfsApi", 5 | baseQuery: fetchBaseQuery({ baseUrl: process.env.IS_DEV === "true" ? "http://localhost:3000/" : "https://api.ownme.io/" }), 6 | endpoints: (builder) => ({ 7 | postIpfsUpload: builder.mutation<{ ipfsUrl?: string, message?: string, error?: string }, FormData>({ 8 | query: (formData) => ({ 9 | url: "ipfs/upload", 10 | method: "POST", 11 | contentType: "multipart/form-data", 12 | body: formData, 13 | headers: { 14 | ...(localStorage.getItem("token") && { Authorization: `Bearer ${localStorage.getItem("token")}` }) 15 | } 16 | }) 17 | }) 18 | }) 19 | }); 20 | 21 | export const { usePostIpfsUploadMutation } = ipfsApi; -------------------------------------------------------------------------------- /api/login.ts: -------------------------------------------------------------------------------- 1 | import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"; 2 | 3 | interface LoginResponse { 4 | address: string; 5 | message: string; 6 | nonce?: string; 7 | token?: string; 8 | } 9 | 10 | export const loginApi = createApi({ 11 | reducerPath: "loginApi", 12 | baseQuery: fetchBaseQuery({ baseUrl: process.env.IS_DEV === "true" ? "http://localhost:3000/" : "https://api.ownme.io/" }), 13 | endpoints: (builder) => ({ 14 | postLogin: builder.mutation({ 15 | query: ({ address }) => ({ 16 | url: "auth/login/", 17 | method: "POST", 18 | body: { 19 | address 20 | }, 21 | headers: { 22 | ...(localStorage.getItem("token") && { Authorization: `Bearer ${localStorage.getItem("token")}` }) 23 | } 24 | }) 25 | }) 26 | }) 27 | }); 28 | 29 | export const { usePostLoginMutation } = loginApi; -------------------------------------------------------------------------------- /api/nft.ts: -------------------------------------------------------------------------------- 1 | import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"; 2 | 3 | export interface TokenURIInterface { 4 | title: string; 5 | description: string; 6 | image: string; 7 | hashtags: string[]; 8 | } 9 | 10 | interface NFTCount { 11 | nft_mumbai_likes: number; 12 | nft_mumbai_views: number; 13 | } 14 | 15 | export interface NftInterface { 16 | tokenId: number; 17 | recipient: string; 18 | address: string; 19 | transactionHash: string; 20 | transactionIndex: number; 21 | blockHash: string; 22 | blockNumber: number; 23 | tokenURI: TokenURIInterface; 24 | price: string; 25 | ownerName?: string; 26 | isLiked?: boolean; 27 | _count: NFTCount; 28 | } 29 | 30 | interface GetNftResponse { 31 | nft: NftInterface; 32 | ownerName?: string; 33 | isLiked?: boolean; 34 | likesCount: number; 35 | viewsCount: number; 36 | } 37 | 38 | export const nftApi = createApi({ 39 | reducerPath: "nftApi", 40 | baseQuery: fetchBaseQuery({ baseUrl: process.env.IS_DEV === "true" ? "http://localhost:3000/" : "https://api.ownme.io/" }), 41 | endpoints: (builder) => ({ 42 | getNft: builder.query({ 43 | query: ({ tokenId }) => ({ 44 | url: `nft/${tokenId}`, 45 | method: "GET", 46 | headers: { 47 | ...(localStorage.getItem("token") && { Authorization: `Bearer ${localStorage.getItem("token")}` }) 48 | } 49 | }) 50 | }), 51 | getUserNfts: builder.query({ 52 | query: ({ address }) => ({ 53 | url: `nft/user/${address}`, 54 | method: "GET", 55 | headers: { 56 | ...(localStorage.getItem("token") && { Authorization: `Bearer ${localStorage.getItem("token")}` }) 57 | } 58 | }) 59 | }), 60 | postNftLike: builder.mutation<{ message?: string }, { tokenId: number }>({ 61 | query: ({ tokenId }) => ({ 62 | url: "nft/like", 63 | method: "POST", 64 | body: { 65 | tokenId 66 | }, 67 | headers: { 68 | ...(localStorage.getItem("token") && { Authorization: `Bearer ${localStorage.getItem("token")}` }) 69 | } 70 | }) 71 | }), 72 | postNftUnlike: builder.mutation<{ message?: string }, { tokenId: number }>({ 73 | query: ({ tokenId }) => ({ 74 | url: "nft/unlike", 75 | method: "POST", 76 | body: { 77 | tokenId 78 | }, 79 | headers: { 80 | ...(localStorage.getItem("token") && { Authorization: `Bearer ${localStorage.getItem("token")}` }) 81 | } 82 | }) 83 | }), 84 | getSearchNfts: builder.query<{ nfts: NftInterface[], message?: string, error?: string }, { query: string, page: number }>({ 85 | query: ({ query, page }) => ({ 86 | url: `nft/search/${query}?page=${page}`, 87 | method: "GET", 88 | headers: { 89 | ...(localStorage.getItem("token") && { Authorization: `Bearer ${localStorage.getItem("token")}` }) 90 | } 91 | }) 92 | }), 93 | postNftReport: builder.mutation<{ message?: string }, { tokenId: number, reason: string }>({ 94 | query: ({ tokenId, reason }) => ({ 95 | url: "nft/report", 96 | method: "POST", 97 | body: { 98 | tokenId, 99 | reason 100 | }, 101 | headers: { 102 | ...(localStorage.getItem("token") && { Authorization: `Bearer ${localStorage.getItem("token")}` }) 103 | } 104 | }) 105 | }) 106 | }) 107 | }); 108 | 109 | export const { 110 | useGetNftQuery, 111 | useGetUserNftsQuery, 112 | usePostNftLikeMutation, 113 | usePostNftUnlikeMutation, 114 | usePostNftReportMutation, 115 | useGetSearchNftsQuery 116 | } = nftApi; -------------------------------------------------------------------------------- /api/posts.ts: -------------------------------------------------------------------------------- 1 | import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"; 2 | 3 | export interface Post { 4 | id: number; 5 | childOf?: number | null; 6 | text: string; 7 | userAddress: string; 8 | userName?: string; 9 | dateCreated: Date; 10 | likesCount: number; 11 | commentsCount: number; 12 | imageUrl?: string | null; 13 | profileImageUrl?: string; 14 | comments: Post[]; 15 | isLiked: boolean; 16 | } 17 | 18 | export const postsApi = createApi({ 19 | reducerPath: "postsApi", 20 | baseQuery: fetchBaseQuery({ baseUrl: process.env.IS_DEV === "true" ? "http://localhost:3000/" : "https://api.ownme.io/" }), 21 | endpoints: (builder) => ({ 22 | getPost: builder.query({ 23 | query: ({ postId }) => ({ 24 | url: `posts/${postId}`, 25 | method: "GET", 26 | headers: { 27 | ...(localStorage.getItem("token") && { Authorization: `Bearer ${localStorage.getItem("token")}` }) 28 | } 29 | }) 30 | }), 31 | getUserPosts: builder.query({ 32 | query: ({ userAddress }) => ({ 33 | url: `posts/user/${userAddress}`, 34 | method: "GET", 35 | headers: { 36 | ...(localStorage.getItem("token") && { Authorization: `Bearer ${localStorage.getItem("token")}` }) 37 | } 38 | }) 39 | }), 40 | postsPost: builder.mutation<{ message?: string, error?: string }, { text: string, childOf?: number, imageUrl?: string }>({ 41 | query: ({ text, childOf, imageUrl }) => ({ 42 | url: "posts/", 43 | method: "POST", 44 | body: { 45 | childOf, 46 | text, 47 | imageUrl 48 | }, 49 | headers: { 50 | ...(localStorage.getItem("token") && { Authorization: `Bearer ${localStorage.getItem("token")}` }) 51 | } 52 | }) 53 | }), 54 | likePost: builder.mutation<{ message?: string, error?: string }, { postId: number }>({ 55 | query: ({ postId }) => ({ 56 | url: `posts/like/${postId}`, 57 | method: "POST", 58 | headers: { 59 | ...(localStorage.getItem("token") && { Authorization: `Bearer ${localStorage.getItem("token")}` }) 60 | } 61 | }) 62 | }), 63 | unlikePost: builder.mutation<{ message?: string, error?: string }, { postId: number }>({ 64 | query: ({ postId }) => ({ 65 | url: `posts/unlike/${postId}`, 66 | method: "POST", 67 | headers: { 68 | ...(localStorage.getItem("token") && { Authorization: `Bearer ${localStorage.getItem("token")}` }) 69 | } 70 | }) 71 | }), 72 | getSearchPosts: builder.query<{ posts?: Post[], message?: string, error?: string }, { query: string }>({ 73 | query: ({ query }) => ({ 74 | url: `posts/search/${query}`, 75 | method: "GET", 76 | headers: { 77 | ...(localStorage.getItem("token") && { Authorization: `Bearer ${localStorage.getItem("token")}` }) 78 | } 79 | }) 80 | }) 81 | }) 82 | }); 83 | 84 | export const { 85 | useGetPostQuery, 86 | useGetUserPostsQuery, 87 | usePostsPostMutation, 88 | useLikePostMutation, 89 | useUnlikePostMutation, 90 | useGetSearchPostsQuery 91 | } = postsApi; -------------------------------------------------------------------------------- /api/register.ts: -------------------------------------------------------------------------------- 1 | import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"; 2 | 3 | interface RegisterRequest { 4 | address: string; 5 | isAgeConfirmed: boolean; 6 | } 7 | 8 | export const registerApi = createApi({ 9 | reducerPath: "registerApi", 10 | baseQuery: fetchBaseQuery({ baseUrl: process.env.IS_DEV === "true" ? "http://localhost:3000/" : "https://api.ownme.io/" }), 11 | endpoints: (builder) => ({ 12 | postRegister: builder.mutation<{ message: string }, RegisterRequest>({ 13 | query: ({ address, isAgeConfirmed }) => ({ 14 | url: "auth/register/", 15 | method: "POST", 16 | body: { 17 | address, 18 | isAgeConfirmed 19 | } 20 | }) 21 | }), 22 | verifyEmail: builder.mutation<{ email: string }, { email: string }>({ 23 | query: ({ email }) => ({ 24 | url: "register/verify-email", 25 | method: "POST", 26 | body: { 27 | email 28 | } 29 | }) 30 | }), 31 | }), 32 | }); 33 | 34 | export const { usePostRegisterMutation, useVerifyEmailMutation } = registerApi; -------------------------------------------------------------------------------- /api/user.ts: -------------------------------------------------------------------------------- 1 | import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"; 2 | 3 | export interface User { 4 | id?: string; 5 | address?: string; 6 | name?: string; 7 | birthDate?: string; 8 | registrationDate?: string; 9 | lastLoginDate?: string; 10 | profileImageUrl?: string; 11 | bannerImageUrl?: string; 12 | bio?: string; 13 | link?: string; 14 | isFollowing?: boolean; 15 | following?: Following[]; 16 | adultVerified?: boolean; 17 | message?: string; 18 | error?: string; 19 | } 20 | 21 | interface UserEditRequest { 22 | name?: string; 23 | bio?: string; 24 | link?: string; 25 | profileImageUrl?: string; 26 | bannerImageUrl?: string; 27 | } 28 | 29 | export interface InitialLoginInfoResponse { 30 | address?: string; 31 | name?: string; 32 | profileImageUrl?: string; 33 | message?: string; 34 | error?: string; 35 | } 36 | 37 | export interface Following { 38 | fromAddress: string; 39 | toAddress: string; 40 | toProfileImageUrl: string; 41 | name: string; 42 | followersCount: number; 43 | nftsCount: number; 44 | } 45 | 46 | export const userApi = createApi({ 47 | reducerPath: "userApi", 48 | baseQuery: fetchBaseQuery({ baseUrl: process.env.IS_DEV === "true" ? "http://localhost:3000/" : "https://api.ownme.io/" }), 49 | endpoints: (builder) => ({ 50 | getUser: builder.query({ 51 | query: ({ address }) => ({ 52 | url: `user/${address}`, 53 | method: "GET", 54 | headers: { 55 | ...(localStorage.getItem("token") && { Authorization: `Bearer ${localStorage.getItem("token")}` }) 56 | } 57 | }) 58 | }), 59 | uploadProfileImage: builder.mutation<{ profileImageUrl?: string, message?: string, error?: string }, { formData: FormData }>({ 60 | query: ({ formData }) => ({ 61 | url: "user/profile-image", 62 | method: "POST", 63 | contentType: "multipart/form-data", 64 | body: formData, 65 | headers: { 66 | ...(localStorage.getItem("token") && { Authorization: `Bearer ${localStorage.getItem("token")}` }) 67 | } 68 | }) 69 | }), 70 | uploadProfileBanner: builder.mutation<{ bannerImageUrl?: string, message?: string, error?: string }, { formData: FormData }>({ 71 | query: ({ formData }) => ({ 72 | url: "user/profile-banner", 73 | method: "POST", 74 | contentType: "multipart/form-data", 75 | body: formData, 76 | headers: { 77 | ...(localStorage.getItem("token") && { Authorization: `Bearer ${localStorage.getItem("token")}` }) 78 | } 79 | }) 80 | }), 81 | editUser: builder.mutation<{ message?: string, error?: string }, UserEditRequest>({ 82 | query: ({ name, bio, link, profileImageUrl, bannerImageUrl }) => ({ 83 | url: "user/edit", 84 | method: "POST", 85 | body: { 86 | name, 87 | bio, 88 | link, 89 | profileImageUrl, 90 | bannerImageUrl 91 | }, 92 | headers: { 93 | ...(localStorage.getItem("token") && { Authorization: `Bearer ${localStorage.getItem("token")}` }) 94 | } 95 | }) 96 | }), 97 | getSearchUsers: builder.query<{ users?: User[], message?: string, error?: string }, { query: string, page: number }>({ 98 | query: ({ query, page }) => ({ 99 | url: `user/search/${query}?page=${page}`, 100 | method: "GET", 101 | headers: { 102 | ...(localStorage.getItem("token") && { Authorization: `Bearer ${localStorage.getItem("token")}` }) 103 | } 104 | }) 105 | }), 106 | getInitialLoginInfo: builder.query({ 107 | query: ({ token }) => ({ 108 | url: "user/initial-login-info", 109 | method: "GET", 110 | headers: { 111 | ...((token || localStorage.getItem("token")) && { Authorization: `Bearer ${token || localStorage.getItem("token")}` }) 112 | } 113 | }) 114 | }), 115 | postFollow: builder.mutation<{ message: string, ok: boolean }, { toAddress: string }>({ 116 | query: ({ toAddress }) => ({ 117 | url: "user/follow", 118 | method: "POST", 119 | body: { 120 | toAddress 121 | }, 122 | headers: { 123 | ...(localStorage.getItem("token") && { Authorization: `Bearer ${localStorage.getItem("token")}` }) 124 | } 125 | }) 126 | }), 127 | postUnfollow: builder.mutation<{ message: string, ok: boolean }, { toAddress: string }>({ 128 | query: ({ toAddress }) => ({ 129 | url: "user/unfollow", 130 | method: "POST", 131 | body: { 132 | toAddress 133 | }, 134 | headers: { 135 | ...(localStorage.getItem("token") && { Authorization: `Bearer ${localStorage.getItem("token")}` }) 136 | } 137 | }) 138 | }) 139 | }) 140 | }); 141 | 142 | export const { 143 | useGetUserQuery, 144 | useUploadProfileImageMutation, 145 | useUploadProfileBannerMutation, 146 | useEditUserMutation, 147 | useGetSearchUsersQuery, 148 | useGetInitialLoginInfoQuery, 149 | usePostFollowMutation, 150 | usePostUnfollowMutation 151 | } = userApi; -------------------------------------------------------------------------------- /api/verify.ts: -------------------------------------------------------------------------------- 1 | import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"; 2 | 3 | interface Person { 4 | firstName: string; 5 | lastName: string; 6 | idNumber?: string; 7 | gender?: string; 8 | dateOfBirth?: string; 9 | } 10 | 11 | interface Document { 12 | number: string, 13 | type: string, 14 | country: string 15 | } 16 | 17 | interface Verification { 18 | callback: string; 19 | person: Person; 20 | document?: Document; 21 | vendorData?: string; 22 | timestamp: string; 23 | } 24 | 25 | interface CreateVerifySessionRequest { 26 | verification: Verification; 27 | } 28 | 29 | interface VerificationResponse { 30 | id: string; 31 | url: string; 32 | sessionToken: string; 33 | baseUrl: string; 34 | } 35 | 36 | interface CreateVerifySessionResponse { 37 | status: string; 38 | verification: VerificationResponse; 39 | } 40 | 41 | export const verifyApi = createApi({ 42 | reducerPath: "verifyApi", 43 | baseQuery: fetchBaseQuery({ baseUrl: "https://stationapi.veriff.com/v1/" }), 44 | endpoints: (builder) => ({ 45 | createVerifySession: builder.mutation({ 46 | query: (body) => ({ 47 | url: "sessions/", 48 | method: "POST", 49 | body, 50 | headers: { 51 | "X-AUTH-CLIENT": "abc8c2f3-41ae-44bc-97bf-fff9d0ae0864", 52 | "Content-Type": "application/json" 53 | } 54 | }) 55 | }) 56 | }) 57 | }); 58 | 59 | export const { useCreateVerifySessionMutation } = verifyApi; -------------------------------------------------------------------------------- /components/AvatarCircle.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from "react"; 2 | import styled from "styled-components"; 3 | 4 | const AvatarCircleContainer = styled.div` 5 | height: 46px; 6 | width: 46px; 7 | border-radius: 50px; 8 | cursor: pointer; 9 | padding-left: 20px; 10 | `; 11 | 12 | const AvatarImage = styled.img` 13 | height: 100%; 14 | width: 100%; 15 | border: 2px solid white; 16 | border-radius: 50px; 17 | background: white; 18 | box-shadow: 0 1px 3px rgb(0 0 0 / 12%), 0 1px 2px rgb(0 0 0 / 24%); 19 | `; 20 | 21 | interface AvatarCircleProps { 22 | image: string; 23 | onClick: () => void; 24 | } 25 | 26 | const AvatarCircle = memo(({ image, onClick }: AvatarCircleProps) => { 27 | return ( 28 | 29 | 30 | 31 | ); 32 | }); 33 | 34 | export default AvatarCircle; -------------------------------------------------------------------------------- /components/Dropdown.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from "react"; 2 | import styled from "styled-components"; 3 | import { Transition } from "react-transition-group"; 4 | import { useAppSelector } from "../redux/hooks"; 5 | 6 | const DropdownContainer = styled.div<{ $isDarkMode: boolean }>` 7 | padding: 40px 40px 20px 40px; 8 | background-color: ${props => props.$isDarkMode ? props.theme.dark.backgroundColor2 : props.theme.light.backgroundColor}; 9 | color: ${props => props.$isDarkMode ? props.theme.dark.textColor : props.theme.light.textColor}; 10 | position: fixed; 11 | top: 88px; 12 | right: 0px; 13 | align-content: center; 14 | align-items: center; 15 | flex-direction: column; 16 | border-radius: 10px; 17 | border: 1px solid #FECDFF; 18 | transition: opacity 150ms ease-in-out; 19 | box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); 20 | 21 | &.entering { 22 | opacity: 0; 23 | } 24 | 25 | &.entered { 26 | opacity: 1; 27 | } 28 | `; 29 | 30 | interface DropdownProps { 31 | children: React.ReactNode; 32 | isOpen: boolean; 33 | } 34 | 35 | const Dropdown = memo(({ children, isOpen }: DropdownProps) => { 36 | const isDarkMode = useAppSelector(state => state.app.isDarkMode); 37 | 38 | return ( 39 | 40 | {transitionState => ( 41 | 42 | {children} 43 | 44 | )} 45 | 46 | ); 47 | }); 48 | 49 | export default Dropdown; -------------------------------------------------------------------------------- /components/EllipseExtras.tsx: -------------------------------------------------------------------------------- 1 | import { EllipsisOutlined } from "@ant-design/icons"; 2 | import React, { memo, useState } from "react"; 3 | import { Transition } from "react-transition-group"; 4 | import styled from "styled-components"; 5 | 6 | export const EllipseExtrasContainer = styled.div` 7 | position: relative; 8 | `; 9 | 10 | const ExtrasIcon = styled(EllipsisOutlined)` 11 | cursor: pointer; 12 | padding-right: 5px; 13 | font-size: 20px; 14 | padding: 5px; 15 | 16 | :hover { 17 | color: #FF81EB; 18 | } 19 | `; 20 | 21 | const ExtrasPanelContainer = styled.div` 22 | min-height: 60px; 23 | min-width: 80px; 24 | background-color: #FF81EB; 25 | position: absolute; 26 | bottom: 30px; 27 | right: 5px; 28 | z-index: 4; 29 | border-radius: 5px; 30 | box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); 31 | padding: 5px 10px; 32 | text-align: right; 33 | 34 | &.entering { 35 | opacity: 0; 36 | } 37 | 38 | &.entered { 39 | opacity: 1; 40 | } 41 | `; 42 | 43 | const ExtrasPanelAction = styled.div` 44 | color: white; 45 | cursor: pointer; 46 | 47 | a { 48 | text-decoration: none; 49 | color: white; 50 | cursor: pointer; 51 | } 52 | 53 | a:hover { 54 | color: #8336ff; 55 | } 56 | 57 | :hover { 58 | color: #8336ff; 59 | } 60 | `; 61 | 62 | export interface ExtraAction { 63 | text: string; 64 | onClick?: (e: React.MouseEvent) => void; 65 | link?: string; 66 | } 67 | 68 | interface EllispeExtrasProps { 69 | extraActions: ExtraAction[]; 70 | className?: string; 71 | } 72 | 73 | const ExtrasPanel = memo(({ extraActions = [] }: EllispeExtrasProps) => { 74 | return ( 75 | 76 | { 77 | extraActions.map((item, index) => ( 78 | 79 | { 80 | item.link ? {item.text} : item.text 81 | } 82 | 83 | )) 84 | } 85 | 86 | ); 87 | }); 88 | 89 | const EllipseExtras = memo(({ extraActions = [], className }: EllispeExtrasProps) => { 90 | const [isOpen, setIsOpen] = useState(false); 91 | 92 | const handleEllipseClick = (e) => { 93 | e.preventDefault(); 94 | e.stopPropagation(); 95 | setIsOpen(!isOpen); 96 | }; 97 | 98 | return ( 99 | setIsOpen(true)} onMouseLeave={() => setIsOpen(false)}> 100 | 101 | 102 | {transitionState => ( 103 | 104 | )} 105 | 106 | 107 | ); 108 | }); 109 | 110 | export default EllipseExtras; -------------------------------------------------------------------------------- /components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useMemo } from "react"; 2 | import styled from "styled-components"; 3 | 4 | const FooterContainer = styled.div` 5 | height: 50px; 6 | background: white; 7 | color: black; 8 | justify-content: center; 9 | display: flex; 10 | `; 11 | 12 | const Footer = memo(() => { 13 | const year = useMemo(() => new Date().getFullYear(), []); 14 | return ( 15 | 16 |

Copyright {year} Own Me Inc.

17 |
18 | ); 19 | }); 20 | 21 | export default Footer; -------------------------------------------------------------------------------- /components/HashtagWordCloud.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useMemo } from "react"; 2 | import styled from "styled-components"; 3 | import { Text } from "@visx/text"; 4 | import { scaleLog } from "@visx/scale"; 5 | import { Wordcloud } from "@visx/wordcloud"; 6 | 7 | const SvgContainer = styled.div` 8 | display: flex; 9 | justify-content: center; 10 | align-items: center; 11 | `; 12 | 13 | const WordText = styled(Text)` 14 | cursor: pointer; 15 | &:hover { 16 | fill: yellow; 17 | } 18 | `; 19 | 20 | const colors = ["#6c27a5", "#c252d1", "#c73c9d"]; 21 | 22 | interface HashtagWordCloudProps { 23 | width: number; 24 | height: number; 25 | hashtags: string[]; 26 | } 27 | 28 | export interface WordData { 29 | text: string; 30 | value: number; 31 | } 32 | 33 | const HashtagWordCloud = ({ width, height, hashtags }: HashtagWordCloudProps) => { 34 | 35 | const wordFreq = useCallback((text: string): WordData[] => { 36 | const words: string[] = text.replace(/\./g, "").split(/\s/); 37 | const freqMap: Record = {}; 38 | 39 | for (const w of words) { 40 | if (!freqMap[w]) freqMap[w] = 0; 41 | freqMap[w] += 1; 42 | } 43 | return Object.keys(freqMap).map((word) => ({ text: word, value: freqMap[word] })); 44 | }, []); 45 | 46 | const words = useMemo(() => wordFreq(hashtags.join(" ")), [hashtags, wordFreq]); 47 | 48 | const fontScale = scaleLog({ 49 | domain: [Math.min(...words.map((w) => w.value)), Math.max(...words.map((w) => w.value))], 50 | range: [20, 150], 51 | }); 52 | 53 | const fontSizeSetter = (datum: WordData) => fontScale(datum.value); 54 | 55 | const fixedValueGenerator = () => 0.5; 56 | 57 | return ( 58 | 59 | 70 | {(cloudWords) => 71 | cloudWords.map((w, i) => ( 72 | 80 | {w.text} 81 | 82 | )) 83 | } 84 | 85 | 86 | ); 87 | }; 88 | 89 | export default HashtagWordCloud; -------------------------------------------------------------------------------- /components/LazyImage.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useEffect, useState } from "react"; 2 | import ContentLoader from "react-content-loader"; 3 | import styled from "styled-components"; 4 | import { useAppSelector } from "../redux/hooks"; 5 | 6 | export const LazyImageContainer = styled.div` 7 | 8 | `; 9 | 10 | export const Image = styled.img` 11 | 12 | `; 13 | 14 | export const LoadingImage = styled(ContentLoader)` 15 | height: 100%; 16 | width: 100%; 17 | `; 18 | 19 | interface LazyImageProps { 20 | src: string; 21 | alt?: string; 22 | className?: string; 23 | } 24 | 25 | const LazyImage = memo(({ src, alt, className }: LazyImageProps) => { 26 | const [loaded, setLoaded] = useState(false); 27 | const [imgSrc, setImgSrc] = useState(""); 28 | 29 | const isDarkMode = useAppSelector(state => state.app.isDarkMode); 30 | 31 | useEffect(() => { 32 | if (src) { 33 | fetch(src).then(res => { 34 | if (res.ok) { 35 | setImgSrc(src); 36 | setTimeout(() => { 37 | setLoaded(true); 38 | }, 1000); 39 | } 40 | }); 41 | } 42 | }, [src]); 43 | 44 | return 45 | { 46 | loaded ? {alt : 51 | 52 | 53 | } 54 | ; 55 | }); 56 | 57 | export default LazyImage; -------------------------------------------------------------------------------- /components/MaticLogoText.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import matic from "./../media/matic.svg"; 4 | 5 | const MainContainer = styled.div` 6 | display: flex; 7 | flex-direction: row; 8 | align-items: center; 9 | `; 10 | 11 | const Logo = styled.img` 12 | width: 30px; 13 | height: 30px; 14 | margin: 0px 10px; 15 | 16 | @media(max-width: 768px) { 17 | width: 20px; 18 | height: 30px; 19 | margin: 0px 5px; 20 | } 21 | `; 22 | 23 | const LogoText = styled.div` 24 | font-size: 24px; 25 | 26 | @media(max-width: 768px) { 27 | font-size: 16px; 28 | } 29 | `; 30 | 31 | 32 | export default function MaticLogoText() { 33 | return ( 34 | 35 | 36 | $MATIC 37 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /components/Modal.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from "react"; 2 | import { createPortal } from "react-dom"; 3 | import styled from "styled-components"; 4 | import xIcon from "../media/icons/x.svg"; 5 | import { useAppSelector } from "../redux/hooks"; 6 | 7 | const MOUNTING_ID = "react-container"; 8 | 9 | const ModalContainer = styled.div` 10 | position: fixed; 11 | height: 100%; 12 | width: 100%; 13 | display: flex; 14 | align-content: center; 15 | justify-content: center; 16 | align-items: center; 17 | background: #000000a6; 18 | z-index: 10; 19 | top: 0; 20 | `; 21 | 22 | const ModalContent = styled.div<{ $isDarkMode: boolean }>` 23 | background-color: ${props => props.$isDarkMode ? props.theme.dark.backgroundColor2 : props.theme.light.backgroundColor}; 24 | min-height: 55%; 25 | width: 60%; 26 | max-width: 800px; 27 | border-radius: 20px; 28 | border: 1px solid #000000a6; 29 | box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); 30 | position: relative; 31 | color: ${props => props.$isDarkMode ? props.theme.dark.textColor : props.theme.light.textColor}; 32 | 33 | @media (max-width: 1200px) { 34 | width: 90%; 35 | } 36 | `; 37 | 38 | const CloseIcon = styled.img` 39 | float: right; 40 | margin: 33px; 41 | cursor: pointer; 42 | height: 25px; 43 | 44 | :hover { 45 | transform: scale(1.1); 46 | } 47 | `; 48 | 49 | interface ModalProps { 50 | isOpen?: boolean; 51 | onClose?: () => void; 52 | children?: React.ReactNode; 53 | } 54 | 55 | const Modal = memo(({ isOpen, onClose, children }: ModalProps) => { 56 | const isDarkMode = useAppSelector(state => state.app.isDarkMode); 57 | 58 | return ( 59 | <> 60 | { 61 | isOpen && createPortal( 62 | 63 | 64 | {children} 65 | 66 | , document.getElementById(MOUNTING_ID)) 67 | } 68 | 69 | ); 70 | }); 71 | 72 | export default Modal; -------------------------------------------------------------------------------- /components/NftReportModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useState } from "react"; 2 | import styled from "styled-components"; 3 | import { shortenAddress } from "../lib/helpers"; 4 | import { NftInterface, usePostNftReportMutation } from "../api/nft"; 5 | import { useAppDispatch, useAppSelector } from "../redux/hooks"; 6 | import { toggleReportModal } from "../redux/slices/app"; 7 | import FormCheckboxInput from "./form/FormCheckboxInput"; 8 | import FormTextArea from "./form/FormTextArea"; 9 | import Modal from "./Modal"; 10 | 11 | const Header = styled.h1` 12 | font-family: "Poppins", sans-serif; 13 | padding: 0px 40px 20px 40px; 14 | border-bottom: 1px solid #e0e0e0; 15 | margin-bottom: 0px; 16 | `; 17 | 18 | const ModalContent = styled.div` 19 | padding: 30px; 20 | `; 21 | 22 | const SubmitButton = styled.button<{ $disabled?: boolean }>` 23 | font-family: Poppins, Open Sans; 24 | font-size: 22px; 25 | background-color: ${props => props.$disabled ? "#a3a3a3" : "#FE4848"}; 26 | color: white; 27 | border: none; 28 | padding: 8px 15px; 29 | border-radius: 6px; 30 | box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); 31 | cursor: ${props => props.$disabled ? "not-allowed" : "pointer"}; 32 | width: 80%; 33 | align-self: flex-end; 34 | opacity: ${props => props.$disabled ? 0.8 : 1}; 35 | 36 | :hover { 37 | background-color: ${props => props.$disabled ? "#8a8a8a" : "#fa2a2a"}; 38 | } 39 | `; 40 | 41 | const FormContainer = styled.div` 42 | `; 43 | 44 | const NftInfoContainer = styled.div` 45 | display: flex; 46 | align-items: flex-start; 47 | font-family: Poppins, Open Sans; 48 | width: 100%; 49 | `; 50 | 51 | const NftImage = styled.img` 52 | width: 300px; 53 | height: 300px; 54 | object-fit: cover; 55 | border-radius: 5px; 56 | `; 57 | 58 | const NftInfo = styled.div` 59 | width: 100%; 60 | height: 100%; 61 | padding-left: 20px; 62 | `; 63 | 64 | const NftTitle = styled.div` 65 | font-size: 25px; 66 | `; 67 | 68 | const NftOwner = styled.div` 69 | 70 | `; 71 | 72 | const FormFooter = styled.div` 73 | display: flex; 74 | justify-content: center; 75 | `; 76 | 77 | const ResponseContainer = styled.div` 78 | display: flex; 79 | justify-content: center; 80 | align-items: center; 81 | padding: 50px 0px; 82 | font-size: 25px; 83 | font-family: Poppins, Open Sans; 84 | `; 85 | 86 | interface NftReportModalProps { 87 | nft?: NftInterface; 88 | } 89 | 90 | const NftReportModal = memo(({ nft }: NftReportModalProps) => { 91 | const dispatch = useAppDispatch(); 92 | const isReportModalOpen = useAppSelector(state => state.app.isReportModalOpen); 93 | 94 | const [reason, setReason] = useState(""); 95 | const [confirmation, setConfirmation] = useState(false); 96 | 97 | const [postNftReport, { 98 | isSuccess: isNftReportSuccess, 99 | }] = usePostNftReportMutation(); 100 | 101 | const submitConditions = reason && confirmation; 102 | 103 | const handleSubmitReport = () => { 104 | if (submitConditions) { 105 | postNftReport({ tokenId: nft.tokenId, reason }); 106 | } 107 | }; 108 | 109 | return ( 110 | dispatch(toggleReportModal())}> 111 |
Report NFT
112 | 113 | 114 | 115 | 116 | {nft.tokenURI.title} 117 | {shortenAddress(nft.recipient, 18)} 118 | 119 | 120 | { 121 | !isNftReportSuccess ? ( 122 | 123 | setReason(value)} 126 | errorMessage="Reason is required." 127 | placeHolder="Please explain why you are reporting this NFT..." 128 | /> 129 |
130 | setConfirmation(checked)} 133 | /> 134 |
135 |
136 | 137 | Submit Report 138 | 139 |
140 | ) : ( 141 | 142 |

Thank you for your report!

143 |
144 | ) 145 | } 146 |
147 |
148 | ); 149 | }); 150 | 151 | export default NftReportModal; -------------------------------------------------------------------------------- /components/NudeLogoText.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import candylogo from "./../media/candylogo.svg"; 4 | 5 | const MainContainer = styled.div` 6 | display: flex; 7 | flex-direction: row; 8 | align-items: center; 9 | `; 10 | 11 | const Logo = styled.img` 12 | width: 30px; 13 | height: 30px; 14 | margin: 0px 10px; 15 | @media(max-width: 768px) { 16 | width: 20px; 17 | height: 30px; 18 | margin: 0px 5px; 19 | } 20 | `; 21 | 22 | const LogoText = styled.div` 23 | font-size: 24px; 24 | 25 | @media(max-width: 768px) { 26 | font-size: 16px; 27 | } 28 | `; 29 | 30 | 31 | export default function NudeLogoText() { 32 | return ( 33 | 34 | 35 | $NUDE 36 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /components/Tabs.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode, useState, useEffect, memo } from "react"; 2 | import styled, { css } from "styled-components"; 3 | 4 | export const TabsContainer = styled.div` 5 | width: 100%; 6 | margin: 0 auto; 7 | 8 | @media (max-width: 1200px) { 9 | width: 100%; 10 | } 11 | `; 12 | 13 | const TabsHeader = styled.div` 14 | font-family: Poppins, Open Sans; 15 | display: flex; 16 | justify-content: space-evenly; 17 | border-bottom: 1px #e0e0e0 solid; 18 | `; 19 | 20 | export const Tab = styled.div<{ $isActive: boolean }>` 21 | cursor: pointer; 22 | opacity: 0.8; 23 | font-size: 16px; 24 | padding: 10px 80px; 25 | 26 | ${props => props.$isActive && css` 27 | border-bottom: 3px solid #D14FFF; 28 | font-weight: 600; 29 | opacity: 1; 30 | `} 31 | 32 | &:hover { 33 | border-bottom: 3px solid #d972ff; 34 | opacity: 1; 35 | } 36 | 37 | @media (max-width: 1200px) { 38 | padding: 10px 20px; 39 | } 40 | `; 41 | 42 | export const TabContent = styled.div` 43 | 44 | `; 45 | 46 | interface TabsProps { 47 | children: ReactNode; 48 | tabs: Array; 49 | className?: string; 50 | onTabChange?: (tab: string) => void; 51 | } 52 | 53 | const Tabs = memo(({ children, tabs, className, onTabChange }: TabsProps) => { 54 | const [activeTab, setActiveTab] = useState(); 55 | 56 | useEffect(() => { 57 | setActiveTab(tabs && tabs.length > 0 ? tabs[0] : null); 58 | }, [tabs]); 59 | 60 | const handleTabChange = (tab: string) => { 61 | setActiveTab(tab); 62 | onTabChange && onTabChange(tab); 63 | }; 64 | 65 | return ( 66 | 67 | 68 | {tabs.map((tab: string, index: number) => { 69 | return handleTabChange(tab)}>{tab}; 70 | })} 71 | 72 | {tabs && tabs.length > 0 && children[tabs.indexOf(activeTab)]} 73 | 74 | ); 75 | }); 76 | 77 | export default Tabs; -------------------------------------------------------------------------------- /components/VerifyStepper.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useCallback, useEffect, useState } from "react"; 2 | import styled from "styled-components"; 3 | import { createVeriffFrame, MESSAGES } from "@veriff/incontext-sdk"; 4 | import Modal from "./Modal"; 5 | import FormInput from "./form/FormInput"; 6 | import { useCreateVerifySessionMutation } from "../api/verify"; 7 | 8 | const ModalContent = styled.div` 9 | padding: 30px; 10 | padding-top: 0px; 11 | `; 12 | 13 | const Header = styled.h1` 14 | font-family: "Poppins", sans-serif; 15 | padding: 0px 40px 20px 40px; 16 | border-bottom: 1px solid #e0e0e0; 17 | margin-bottom: 0px; 18 | `; 19 | 20 | const Message = styled.h4` 21 | font-family: "Poppins", sans-serif; 22 | `; 23 | 24 | const VerifyButton = styled.button<{ $disabled?: boolean }>` 25 | font-family: Poppins, Open Sans; 26 | font-size: 30px; 27 | background-color: ${props => props.$disabled ? "#a3a3a3" : "#FF81EB"}; 28 | color: white; 29 | border: none; 30 | padding: 8px 15px; 31 | border-radius: 6px; 32 | box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); 33 | cursor: ${props => props.$disabled ? "not-allowed" : "pointer"}; 34 | opacity: ${props => props.$disabled ? 0.8 : 1}; 35 | width: 100%; 36 | align-self: flex-end; 37 | margin-top: 25px; 38 | 39 | :hover { 40 | background-color: ${props => props.$disabled ? "#8a8a8a" : "#fb5de1"}; 41 | } 42 | `; 43 | 44 | interface VerifyStepperInterface { 45 | userAddress: string; 46 | onClose: () => void; 47 | } 48 | 49 | const VerifyStepper = memo(({ userAddress, onClose }: VerifyStepperInterface) => { 50 | const [firstName, setFirstName] = useState(null); 51 | const [lastName, setLastName] = useState(null); 52 | 53 | const [postCreateVerifySession, { 54 | isSuccess: isCreateVerifySessionSuccess, 55 | data: createVerifySessionData 56 | }] = useCreateVerifySessionMutation(); 57 | 58 | const handleGetVerified = useCallback(() => { 59 | postCreateVerifySession({ 60 | verification: { 61 | callback: "https://veriff.com", 62 | person: { 63 | firstName, 64 | lastName, 65 | idNumber: userAddress 66 | }, 67 | timestamp: new Date().toISOString() 68 | } 69 | }); 70 | }, [firstName, lastName, postCreateVerifySession, userAddress]); 71 | 72 | useEffect(() => { 73 | if (isCreateVerifySessionSuccess && createVerifySessionData?.verification?.url) { 74 | createVeriffFrame({ 75 | url: createVerifySessionData.verification.url, 76 | onEvent: msg => { 77 | switch (msg) { 78 | case MESSAGES.STARTED: 79 | console.log("STARTED verification"); 80 | break; 81 | 82 | case MESSAGES.CANCELED: 83 | console.log("CANCELED verification"); 84 | break; 85 | 86 | case MESSAGES.FINISHED: 87 | console.log("FINISHED verification"); 88 | break; 89 | } 90 | } 91 | }); 92 | } 93 | }, [createVerifySessionData?.verification?.url, isCreateVerifySessionSuccess]); 94 | 95 | return ( 96 | 97 |
Get Verified!
98 | 99 | We use Veriff a 3rd party company that takes minimum required documents to prove you are of legal age to be an adult content creator. 100 | Either we do KYC or go to jail basically. Own Me Inc. would not be a functional company and we don't want underage creators. 101 | We do not store any of your documents, the 3rd party handles everything. We just store a receipt of the verification for legal bookkeeping. 102 |
103 | setFirstName(e.target.value)} 107 | errorMessage="First Name is required." 108 | /> 109 | setLastName(e.target.value)} 113 | errorMessage="Last Name is required." 114 | /> 115 | Get Verified 116 |
117 |
118 | ); 119 | }); 120 | 121 | export default VerifyStepper; 122 | -------------------------------------------------------------------------------- /components/form/FormCheckboxInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useCallback, useState } from "react"; 2 | import { useLocation } from "react-router-dom"; 3 | import styled from "styled-components"; 4 | import { useAppSelector } from "../../redux/hooks"; 5 | 6 | export const FormFileInputContainer = styled.div<{ $isDarkMode: boolean }>` 7 | width: 100%; 8 | cursor: pointer; 9 | font-family: Poppins, Open Sans; 10 | color: ${props => props.$isDarkMode ? props.theme.dark.textColor : props.theme.light.textColor}; 11 | display: flex; 12 | `; 13 | 14 | const CheckboxInput = styled.input` 15 | height: 25px; 16 | width: 25px; 17 | margin-right: 10px; 18 | cursor: pointer; 19 | `; 20 | 21 | const Label = styled.label` 22 | 23 | `; 24 | 25 | interface FormCheckboxInputProps { 26 | label: string; 27 | onChecked?: (checked: boolean) => void; 28 | } 29 | 30 | const FormCheckboxInput = memo(({ label, onChecked }: FormCheckboxInputProps) => { 31 | const [checked, setChecked] = useState(false); 32 | const location = useLocation(); 33 | 34 | const isDarkMode = useAppSelector(state => state.app.isDarkMode) && location.pathname !== "/login" && location.pathname !== "/register"; 35 | 36 | const handleChange = useCallback(() => { 37 | onChecked && onChecked(!checked); 38 | setChecked(!checked); 39 | }, [checked, onChecked]); 40 | 41 | return ( 42 | 43 | 44 | 45 | 46 | ); 47 | }); 48 | 49 | export default FormCheckboxInput; -------------------------------------------------------------------------------- /components/form/FormFileInputButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, ReactNode } from "react"; 2 | import styled from "styled-components"; 3 | 4 | const WIDTH = 200; 5 | 6 | export const FormFileInputContainer = styled.div` 7 | width: ${WIDTH}px; 8 | position: relative; 9 | display: flex; 10 | cursor: pointer; 11 | `; 12 | 13 | const Button = styled.button` 14 | font-family: Poppins, Open Sans; 15 | height: 100%; 16 | font-family: Poppins,Open Sans; 17 | font-size: 16px; 18 | background-color: #FF81EB; 19 | color: white; 20 | border: none; 21 | padding: 5px 15px; 22 | border-radius: 25px; 23 | box-shadow: 0 3px 6px rgb(0 0 0 / 16%), 0 3px 6px rgb(0 0 0 / 23%); 24 | cursor: pointer; 25 | `; 26 | 27 | const Input = styled.input` 28 | position: absolute; 29 | height: 100%; 30 | width: 100%; 31 | opacity: 0; 32 | cursor: pointer; 33 | `; 34 | 35 | interface FormFileInputProps { 36 | onFile?: (file: File) => void; 37 | onData?: (data: string | ArrayBuffer) => void; 38 | children?: ReactNode; 39 | className?: string; 40 | } 41 | 42 | const FormFileInput = memo(({ onFile, onData, children, className }: FormFileInputProps) => { 43 | 44 | const handleChange = (e) => { 45 | e.preventDefault(); 46 | const file = e.target.files[0]; 47 | onFile && onFile(file); 48 | const reader = new FileReader(); 49 | reader.onload = (e) => { 50 | const data = e.target.result; 51 | onData && onData(data); 52 | }; 53 | reader.readAsDataURL(file); 54 | }; 55 | 56 | return ( 57 | 58 | 59 | 60 | 61 | ); 62 | }); 63 | 64 | export default FormFileInput; -------------------------------------------------------------------------------- /components/form/FormHashtagInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from "react"; 2 | import styled from "styled-components"; 3 | import FormInput, { FormInputProps } from "./FormInput"; 4 | 5 | const HashtagInput = styled(FormInput)` 6 | font-family: Poppins, Open Sans; 7 | width: 100%; 8 | margin: 10px 0px; 9 | position: relative; 10 | `; 11 | 12 | interface FormHashtagInputProps extends FormInputProps { 13 | onChange: (value) => void; 14 | } 15 | 16 | const FormHashtagInput = memo((props: FormHashtagInputProps) => { 17 | const handleChange = (e) => { 18 | let value = String(e.target.value); 19 | if (value.charAt(0) !== "#") { 20 | value = "#" + value; 21 | } 22 | if (value.charAt(value.length - 1) === " ") { 23 | value = value + "#"; 24 | } 25 | props.onChange(value); 26 | }; 27 | 28 | return ( 29 | 30 | ); 31 | }); 32 | 33 | export default FormHashtagInput; -------------------------------------------------------------------------------- /components/form/FormInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { InputHTMLAttributes, memo, useEffect, useState } from "react"; 2 | import styled, { css } from "styled-components"; 3 | import { useAppSelector } from "../../redux/hooks"; 4 | 5 | export const formStyles = css<{ $isError?: boolean, $isDarkMode: boolean }>` 6 | font-family: Poppins, Open Sans; 7 | background-color: ${props => props.$isDarkMode ? "#1c012a" : "#FFFDFF"}; 8 | color: ${props => props.$isDarkMode ? "white" : "black"}; 9 | border: 1px solid ${props => props.$isError ? "red" : "#cc00ff"}; 10 | padding: 10px 15px; 11 | border-radius: 5px; 12 | font-size: 20px; 13 | box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); 14 | width: 100%; 15 | `; 16 | 17 | const FormInputContainer = styled.div` 18 | font-family: Poppins, Open Sans; 19 | width: 100%; 20 | margin: 10px 0px; 21 | position: relative; 22 | `; 23 | 24 | const Input = styled.input<{ $isError?: boolean, $isDarkMode: boolean }>` 25 | ${formStyles}; 26 | `; 27 | 28 | const Label = styled.label` 29 | text-align: left; 30 | display: block; 31 | font-size: 26px; 32 | padding: 10px 0px; 33 | `; 34 | 35 | const InfoText = styled.span` 36 | color: red; 37 | position: absolute; 38 | right: 15px; 39 | top: 25px; 40 | `; 41 | 42 | const ErrorText = styled(InfoText)` 43 | color: red; 44 | 45 | `; 46 | 47 | const OptionalText = styled(InfoText)` 48 | color: #505050; 49 | `; 50 | 51 | export interface FormInputProps extends InputHTMLAttributes { 52 | label: string; 53 | type: "text" | "email" | "password" | "number" | "select" | "checkbox" | "radio"; 54 | inputValue?: string; 55 | errorMessage?: string; 56 | optional?: boolean; 57 | } 58 | 59 | const FormInput = memo(({ label, onChange, inputValue, errorMessage, type, placeholder, min, optional }: FormInputProps) => { 60 | const [value, setValue] = useState(inputValue || ""); 61 | const [error, setError] = useState(""); 62 | 63 | const isDarkMode = useAppSelector(state => state.app.isDarkMode); 64 | 65 | const handleChange = (e) => { 66 | const value = e.target?.value; 67 | if (!value && !optional) { 68 | setError(errorMessage || "Input invalid."); 69 | } else if (error && value) { 70 | setError(""); 71 | } 72 | setValue(value); 73 | onChange && onChange(e); 74 | }; 75 | 76 | useEffect(() => { 77 | inputValue && setValue(inputValue || ""); 78 | }, [inputValue]); 79 | 80 | return ( 81 | 82 | 83 | {error && {error}} 84 | {optional && !error && optional} 85 | 86 | 87 | ); 88 | }); 89 | 90 | export default FormInput; -------------------------------------------------------------------------------- /components/form/FormTextArea.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useState } from "react"; 2 | import styled from "styled-components"; 3 | import { useAppSelector } from "../../redux/hooks"; 4 | import { formStyles } from "./FormInput"; 5 | 6 | const FormTextAreaContainer = styled.div` 7 | font-family: Poppins, Open Sans; 8 | width: 100%; 9 | margin: 10px 0px; 10 | position: relative; 11 | `; 12 | 13 | const Textarea = styled.textarea<{ $isError: boolean, $isDarkMode: boolean }>` 14 | ${formStyles}; 15 | max-width: 100%; 16 | `; 17 | 18 | const Label = styled.label` 19 | text-align: left; 20 | display: block; 21 | font-size: 26px; 22 | padding: 10px 0px; 23 | `; 24 | 25 | const Error = styled.span` 26 | color: red; 27 | position: absolute; 28 | right: 15px; 29 | top: 25px; 30 | `; 31 | 32 | interface FormTextAreaProps { 33 | label: string; 34 | onChange?: (value: string) => void; 35 | inputValue?: string; 36 | errorMessage?: string; 37 | placeHolder?: string; 38 | } 39 | 40 | const FormTextArea = memo(({ label, onChange, inputValue, errorMessage, placeHolder }: FormTextAreaProps) => { 41 | const [value, setValue] = useState(inputValue || ""); 42 | const [error, setError] = useState(""); 43 | 44 | const isDarkMode = useAppSelector(state => state.app.isDarkMode); 45 | 46 | const handleChange = (e) => { 47 | const value = e.target?.value; 48 | setError(!value ? (errorMessage || "Error, input is invalid.") : ""); 49 | setValue(value); 50 | onChange && onChange(value); 51 | }; 52 | 53 | return ( 54 | 55 | 56 | {error} 57 |