diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..d43c37e --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,55 @@ +name: Deploy + +on: + pull_request: + push: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + deployments: write + name: Publish to Cloudflare Pages + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Cache node modules + id: cache-npm + uses: actions/cache@v3 + env: + cache-name: cache-node-modules + with: + # npm cache files are stored in `~/.npm` on Linux/macOS + path: ~/.npm + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-build-${{ env.cache-name }}- + ${{ runner.os }}-build- + ${{ runner.os }}- + + - if: ${{ steps.cache-npm.outputs.cache-hit != 'true' }} + name: List the state of node modules + continue-on-error: true + working-directory: ./frontend + run: npm list + + - name: Install dependencies + working-directory: ./frontend + run: npm install + + - name: Build + working-directory: ./frontend + run: npm run build + + - name: Publish to Cloudflare Pages + uses: cloudflare/pages-action@1 + with: + apiToken: ${{ secrets.CLOUDFLARE_PAGES_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + projectName: alliance-nft + directory: ./frontend/build + gitHubToken: ${{ secrets.GITHUB_TOKEN }} diff --git a/frontend/src/assets/CircleClear.svg b/frontend/src/assets/CircleClear.svg new file mode 100644 index 0000000..0269af0 --- /dev/null +++ b/frontend/src/assets/CircleClear.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/assets/DropdownArrow.svg b/frontend/src/assets/DropdownArrow.svg new file mode 100644 index 0000000..f5f4448 --- /dev/null +++ b/frontend/src/assets/DropdownArrow.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/src/assets/ExternalLink.svg b/frontend/src/assets/ExternalLink.svg new file mode 100644 index 0000000..65ac377 --- /dev/null +++ b/frontend/src/assets/ExternalLink.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/src/assets/Filter.svg b/frontend/src/assets/Filter.svg new file mode 100644 index 0000000..805fa52 --- /dev/null +++ b/frontend/src/assets/Filter.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/assets/Search.svg b/frontend/src/assets/Search.svg new file mode 100644 index 0000000..d434fd6 --- /dev/null +++ b/frontend/src/assets/Search.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/src/assets/socials/Medium.svg b/frontend/src/assets/socials/Medium.svg new file mode 100644 index 0000000..591c00d --- /dev/null +++ b/frontend/src/assets/socials/Medium.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/src/assets/socials/Telegram.svg b/frontend/src/assets/socials/Telegram.svg new file mode 100644 index 0000000..49c2957 --- /dev/null +++ b/frontend/src/assets/socials/Telegram.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/src/assets/socials/Twitter.svg b/frontend/src/assets/socials/Twitter.svg new file mode 100644 index 0000000..e6329a3 --- /dev/null +++ b/frontend/src/assets/socials/Twitter.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/Connect.module.scss b/frontend/src/components/Connect.module.scss deleted file mode 100644 index 1db5fa8..0000000 --- a/frontend/src/components/Connect.module.scss +++ /dev/null @@ -1,3 +0,0 @@ -.connectWrapper { - border: 1px solid red -} \ No newline at end of file diff --git a/frontend/src/components/Connect.tsx b/frontend/src/components/Connect.tsx deleted file mode 100644 index 6863a03..0000000 --- a/frontend/src/components/Connect.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import styles from "./Connect.module.scss" - -export const Connect = () => { - return ( -
-

Connect

-
- ) -} diff --git a/frontend/src/components/checkbox/checkbox.module.scss b/frontend/src/components/checkbox/checkbox.module.scss new file mode 100644 index 0000000..9b544e1 --- /dev/null +++ b/frontend/src/components/checkbox/checkbox.module.scss @@ -0,0 +1,58 @@ +// @import 'scss/font_mixins'; + +.checkbox { + display: inline-flex; + justify-content: center; + align-items: center; + + cursor: pointer; + gap: 8px; + user-select: none; + + &.checked .indicator { + opacity: 1; + } + + &.disabled { + cursor: not-allowed; + opacity: 0.5; + } + + .text { + flex: 1; + // @include Small; + color: var(--token-dark-500); + } + + &.checked { + .track { + border: solid 1px var(--token-primary-400); + } + + .text { + color: var(--token-light-white); + transition: color 100ms; + } + } +} + +.track { + display: flex; + justify-content: center; + align-items: center; + + border: solid 1px var(--token-dark-500); + border-radius: 6px; + width: 18px; + height: 18px; +} + +.indicator { + background: var(--token-primary-500); + border-radius: 2px; + width: 8px; + height: 8px; + + opacity: 0; + transition: opacity 100ms; +} diff --git a/frontend/src/components/checkbox/checkbox.tsx b/frontend/src/components/checkbox/checkbox.tsx new file mode 100644 index 0000000..9ed730b --- /dev/null +++ b/frontend/src/components/checkbox/checkbox.tsx @@ -0,0 +1,30 @@ +import { ForwardedRef, forwardRef, InputHTMLAttributes } from 'react'; +import classNames from 'classnames/bind'; +import styles from './checkbox.module.scss'; + +const cx = classNames.bind(styles); + +export interface CheckboxProps extends InputHTMLAttributes { + label: string + checked?: boolean +} + +const Checkbox = forwardRef( + ( + { className, label, checked, ...attrs }: CheckboxProps, + ref: ForwardedRef + ) => { + const { disabled } = attrs; + return ( + + ); + }, +); + +export default Checkbox; diff --git a/frontend/src/components/filters/dropdowns/FilterDropdown.module.scss b/frontend/src/components/filters/dropdowns/FilterDropdown.module.scss new file mode 100644 index 0000000..f7d8ef2 --- /dev/null +++ b/frontend/src/components/filters/dropdowns/FilterDropdown.module.scss @@ -0,0 +1,123 @@ +.filter__row { + display: flex; + gap: 24px; + align-items: center; + margin-bottom: 24px; +} + +.filter__dropdown__container { + max-width: 200px; + width: 100%; + display: flex; + gap: 24px; + position: relative; + + .selector__wrapper { + display: flex; + align-items: center; + gap: 4px; + width: 100%; + + .selector { + padding: 8px 12px; + background: var(--token-light-500); + border: 1px solid var(--token-light-800); + border-radius: 8px; + overflow: hidden; + display: flex; + justify-content: space-between; + align-items: center; + gap: 8px; + appearance: none; + width: 100%; + + .selected__wrapper { + display: flex; + align-items: center; + gap: 8px; + } + + span { + display: flex; + font-size: 14px; + font-weight: 500; + color: var(--token-dark-500); + } + + &.open { + background-color: var(--token-light-800); + border-color: var(--token-dark-700); + } + + &:hover { + background-color: var(--token-light-800); + border-color: var(--token-dark-700); + transition: all .25s; + } + } + + .clear__wrapper { + display: flex; + align-items: center; + } + } + + .options { + padding: 8px 2px 8px 12px; + position: absolute; + width: 100%; + background: var(--token-light-800); + border: 1px solid var(--token-dark-700); + + border-radius: 8px; + z-index: 10; + max-height: 200px; + box-shadow: 0px 5px 7px 0px #0000009c; + top: 40px; + overflow: hidden; + + .options__container { + padding-right: 10px; + padding-bottom: 8px; + overflow: auto; + max-height: 190px; + display: flex; + flex-direction: column; + gap: 4px; + + &::-webkit-scrollbar { + width: 12px; + display: block; + } + + &::-webkit-scrollbar-thumb { + border-radius: 16px; + background-color: hsl(0, 0%, 47%); + border: 2px solid var(--token-light-800); + } + + &::-webkit-scrollbar-track { + background-color: transparent; + } + } + } +} + +@media (max-width: 590px) { + .filter__row { + // flex-direction: column; + flex-wrap: wrap; + align-items: flex-start; + gap: 4px; + } + + .filter__dropdown__container { + .selector__wrapper { + .selector { + span { + font-size: 12px; + } + } + } + } +} \ No newline at end of file diff --git a/frontend/src/components/filters/dropdowns/InhabitantFilter.tsx b/frontend/src/components/filters/dropdowns/InhabitantFilter.tsx new file mode 100644 index 0000000..e8b895f --- /dev/null +++ b/frontend/src/components/filters/dropdowns/InhabitantFilter.tsx @@ -0,0 +1,119 @@ +import { useEffect, useRef, useState } from 'react'; +import classNames from 'classnames/bind'; +import { inhabitantOptions } from '../options'; +import { GalleryFiltersProps } from 'pages/nft/NFTs'; +import { ReactComponent as DropdownArrowIcon } from "assets/DropdownArrow.svg"; +import { ReactComponent as CircleClearIcon } from "assets/CircleClear.svg"; +import Checkbox from 'components/checkbox/checkbox'; +import styles from './FilterDropdown.module.scss'; + +const cx = classNames.bind(styles); + +export const InhabitantFilter = ({ + galleryFilters, + setGalleryFilters, +}: { + galleryFilters: GalleryFiltersProps + setGalleryFilters: ({ + planetNumber, + planetNames, + planetInhabitants, + nftObjects + }: GalleryFiltersProps) => void, +}) => { + const [open, setOpen] = useState(false) + const ref = useRef(null) + + useEffect(() => { + document.addEventListener("mousedown", handleClickOutside) + return () => { + document.removeEventListener("mousedown", handleClickOutside) + } + }, []) + + const handleClickOutside = (event: MouseEvent) => { + if (ref.current && !ref.current.contains(event.target as Node)) { + setOpen(false) + } + } + + const [selectedInhabitants, setSelectedInhabitants] = useState(galleryFilters.planetInhabitants || []); + const inhabitantDropdownValue = + selectedInhabitants.length ? + selectedInhabitants.length === 1 ? + selectedInhabitants[0] : `${selectedInhabitants.length} inhabitant selected` + : "Select an inhabitant"; + + useEffect(() => { + setGalleryFilters({ + ...galleryFilters, + planetInhabitants: selectedInhabitants, + }); + }, [selectedInhabitants]); + + const handleInhabitantClick = (inhabitantsName: string) => { + if (selectedInhabitants?.includes(inhabitantsName)) { + setSelectedInhabitants(selectedInhabitants.filter(name => name !== inhabitantsName)); + } else { + setSelectedInhabitants(selectedInhabitants ? [...selectedInhabitants, inhabitantsName] : [inhabitantsName]); + } + } + + const clearInhabitantFilters = () => { + setGalleryFilters({ + ...galleryFilters, + planetInhabitants: [], + }); + setSelectedInhabitants([]); + } + + return ( +
+
+ + {selectedInhabitants.length > 0 && ( +
+ +
+ )} +
+ {open && ( +
+
+ {inhabitantOptions.map(inhabitant => ( + handleInhabitantClick(inhabitant)} + /> + ))} +
+
+ )} +
+ ) +} diff --git a/frontend/src/components/filters/dropdowns/ObjectFilter.tsx b/frontend/src/components/filters/dropdowns/ObjectFilter.tsx new file mode 100644 index 0000000..3252701 --- /dev/null +++ b/frontend/src/components/filters/dropdowns/ObjectFilter.tsx @@ -0,0 +1,119 @@ +import { useEffect, useRef, useState } from 'react'; +import classNames from 'classnames/bind'; +import { objectOptions } from '../options'; +import { GalleryFiltersProps } from 'pages/nft/NFTs'; +import { ReactComponent as DropdownArrowIcon } from "assets/DropdownArrow.svg"; +import { ReactComponent as CircleClearIcon } from "assets/CircleClear.svg"; +import Checkbox from 'components/checkbox/checkbox'; +import styles from './FilterDropdown.module.scss'; + +const cx = classNames.bind(styles); + +export const ObjectFilter = ({ + galleryFilters, + setGalleryFilters, +}: { + galleryFilters: GalleryFiltersProps + setGalleryFilters: ({ + planetNumber, + planetNames, + planetInhabitants, + nftObjects + }: GalleryFiltersProps) => void, +}) => { + const [open, setOpen] = useState(false) + const ref = useRef(null) + + useEffect(() => { + document.addEventListener("mousedown", handleClickOutside) + return () => { + document.removeEventListener("mousedown", handleClickOutside) + } + }, []) + + const handleClickOutside = (event: MouseEvent) => { + if (ref.current && !ref.current.contains(event.target as Node)) { + setOpen(false) + } + } + + const [selectedObjects, setSelectedObjects] = useState(galleryFilters.nftObjects || []); + const objectDropdownValue = + selectedObjects.length ? + selectedObjects.length === 1 ? + selectedObjects[0] : `${selectedObjects.length} objects selected` + : "Select an object"; + + useEffect(() => { + setGalleryFilters({ + ...galleryFilters, + nftObjects: selectedObjects, + }); + }, [selectedObjects]); + + const handleObjectClick = (objectName: string) => { + if (selectedObjects?.includes(objectName)) { + setSelectedObjects(selectedObjects.filter(name => name !== objectName)); + } else { + setSelectedObjects(selectedObjects ? [...selectedObjects, objectName] : [objectName]); + } + } + + const clearObjectFilters = () => { + setGalleryFilters({ + ...galleryFilters, + nftObjects: [], + }); + setSelectedObjects([]); + } + + return ( +
+
+ + {selectedObjects.length > 0 && ( +
+ +
+ )} +
+ {open && ( +
+
+ {objectOptions.map(nftObject => ( + handleObjectClick(nftObject)} + /> + ))} +
+
+ )} +
+ ) +} diff --git a/frontend/src/components/filters/dropdowns/PlanetFilter.tsx b/frontend/src/components/filters/dropdowns/PlanetFilter.tsx new file mode 100644 index 0000000..2305644 --- /dev/null +++ b/frontend/src/components/filters/dropdowns/PlanetFilter.tsx @@ -0,0 +1,119 @@ +import { useEffect, useRef, useState } from 'react'; +import classNames from 'classnames/bind'; +import { planetOptions } from '../options'; +import { GalleryFiltersProps } from 'pages/nft/NFTs'; +import { ReactComponent as DropdownArrowIcon } from "assets/DropdownArrow.svg"; +import { ReactComponent as CircleClearIcon } from "assets/CircleClear.svg"; +import Checkbox from 'components/checkbox/checkbox'; +import styles from './FilterDropdown.module.scss'; + +const cx = classNames.bind(styles); + +export const PlanetFilter = ({ + galleryFilters, + setGalleryFilters, +}: { + galleryFilters: GalleryFiltersProps + setGalleryFilters: ({ + planetNumber, + planetNames, + planetInhabitants, + nftObjects + }: GalleryFiltersProps) => void, +}) => { + const [open, setOpen] = useState(false) + const ref = useRef(null) + + useEffect(() => { + document.addEventListener("mousedown", handleClickOutside) + return () => { + document.removeEventListener("mousedown", handleClickOutside) + } + }, []) + + const handleClickOutside = (event: MouseEvent) => { + if (ref.current && !ref.current.contains(event.target as Node)) { + setOpen(false) + } + } + + const [selectedPlanets, setSelectedPlanets] = useState(galleryFilters.planetNames || []); + const planetDropdownValue = + selectedPlanets.length ? + selectedPlanets.length === 1 ? + selectedPlanets[0] : `${selectedPlanets.length} planets selected` + : "Select a planet"; + + useEffect(() => { + setGalleryFilters({ + ...galleryFilters, + planetNames: selectedPlanets, + }); + }, [selectedPlanets]); + + const handlePlanetClick = (planetName: string) => { + if (selectedPlanets?.includes(planetName)) { + setSelectedPlanets(selectedPlanets.filter(name => name !== planetName)); + } else { + setSelectedPlanets(selectedPlanets ? [...selectedPlanets, planetName] : [planetName]); + } + } + + const clearPlanetFilters = () => { + setGalleryFilters({ + ...galleryFilters, + planetNames: [], + }); + setSelectedPlanets([]); + } + + return ( +
+
+ + {selectedPlanets.length > 0 && ( +
+ +
+ )} +
+ {open && ( +
+
+ {planetOptions.map(planet => ( + handlePlanetClick(planet)} + /> + ))} +
+
+ )} +
+ ) +} diff --git a/frontend/src/components/filters/dropdowns/index.tsx b/frontend/src/components/filters/dropdowns/index.tsx new file mode 100644 index 0000000..dd3869d --- /dev/null +++ b/frontend/src/components/filters/dropdowns/index.tsx @@ -0,0 +1,36 @@ +import { PlanetFilter } from './PlanetFilter'; +import { InhabitantFilter } from './InhabitantFilter'; +import { ObjectFilter } from './ObjectFilter'; +import { GalleryFiltersProps } from 'pages/nft/NFTs'; + +import styles from './FilterDropdown.module.scss'; + +export const FilterDropdowns = ({ + galleryFilters, + setGalleryFilters, +}: { + galleryFilters: GalleryFiltersProps + setGalleryFilters: ({ + planetNumber, + planetNames, + planetInhabitants, + nftObjects + }: GalleryFiltersProps) => void, +}) => { + return ( +
+ + + +
+ ); +}; diff --git a/frontend/src/components/filters/facet.module.scss b/frontend/src/components/filters/facet.module.scss new file mode 100644 index 0000000..072ba59 --- /dev/null +++ b/frontend/src/components/filters/facet.module.scss @@ -0,0 +1,95 @@ +.facet { + display: flex; + flex-direction: column; + gap: 24px; + width: 20%; + border-right: 1px solid #E5E5E5; + padding: 0px 8px 0px 24px; + + position: fixed; + left: -100%; + width: 20%; + display: flex; + justify-content: space-between; + flex-direction: column; + z-index: 100; + + &.open { + left: 0; + transition: left 0.5s cubic-bezier(0.22, 1, 0.36, 1); + } + + &.closed { + left: -100%; + transition: left 2s cubic-bezier(0.22, 1, 0.36, 1); + } + + .facet__header { + display: flex; + justify-content: space-between; + + .facet__title { + h3 { + color: var(--token-dark-500); + font-size: 16px; + font-weight: 600; + } + } + + .facet__clear { + .clear__button { + font-size: 12px; + font-weight: 600; + color: #797979; + margin-right: 12px; + + &:hover { + color: #000; + } + } + } + } + + .facet__body { + display: flex; + flex-direction: column; + gap: 24px; + + .filter__section { + display: flex; + flex-direction: column; + gap: 4px; + + .filter__section__header { + h4 { + font-size: 16px; + font-weight: 600; + color: var(--token-dark-500); + } + } + + .filter__section__body { + max-height: 120px; + overflow: auto; + } + } + + .facet__item { + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + + .facet__item__checkbox { + input { + + } + } + .facet__item__name { + span { + + } + } + } + } +} \ No newline at end of file diff --git a/frontend/src/components/filters/facet.tsx b/frontend/src/components/filters/facet.tsx new file mode 100644 index 0000000..99b0ae6 --- /dev/null +++ b/frontend/src/components/filters/facet.tsx @@ -0,0 +1,150 @@ +/* eslint-disable react-hooks/exhaustive-deps */ + +import { useEffect, useState } from 'react'; +import styles from './facet.module.scss'; +import { planetOptions, inhabitantOptions, objectOptions } from './options'; +import { GalleryFiltersProps } from 'pages/nft/NFTs'; + +export const Facet = ({ + galleryFilters, + setGalleryFilters, + displayFacet, +}: { + galleryFilters: GalleryFiltersProps + setGalleryFilters: ({ + planetNumber, + planetNames, + planetInhabitants, + nftObjects + }: GalleryFiltersProps) => void, + displayFacet: boolean, +}) => { + const [selectedPlanets, setSelectedPlanets] = useState([]); + const [selectedInhabitants, setSelectedInhabitants] = useState([]); + const [selectedObjects, setSelectedObjects] = useState([]); + + useEffect(() => { + setGalleryFilters({ + ...galleryFilters, + planetNames: selectedPlanets, + planetInhabitants: selectedInhabitants, + nftObjects: selectedObjects, + }); + }, [selectedPlanets, selectedInhabitants, selectedObjects]); + + const clearFilters = () => { + setGalleryFilters({ + planetNumber: null, + planetNames: [], + planetInhabitants: [], + nftObjects: [], + }); + setSelectedPlanets([]); + setSelectedInhabitants([]); + setSelectedObjects([]); + }; + + const handlePlanetClick = (planetName: string) => { + if (selectedPlanets?.includes(planetName)) { + setSelectedPlanets(selectedPlanets.filter(name => name !== planetName)); + } else { + setSelectedPlanets(selectedPlanets ? [...selectedPlanets, planetName] : [planetName]); + } + } + + const handleInhabitantClick = (inhabitant: string) => { + if (selectedInhabitants?.includes(inhabitant)) { + setSelectedInhabitants(selectedInhabitants.filter(name => name !== inhabitant)); + } else { + setSelectedInhabitants(selectedInhabitants ? [...selectedInhabitants, inhabitant] : [inhabitant]); + } + } + + const handleObjectClick = (object: string) => { + if (selectedObjects?.includes(object)) { + setSelectedObjects(selectedObjects.filter(name => name !== object)); + } else { + setSelectedObjects(selectedObjects ? [...selectedObjects, object] : [object]); + } + } + + return ( +
+
+
+

Gallery Filters

+
+
+ +
+
+
+
+
+

Planet Type

+
+
+ {planetOptions.map(planet => ( +
+ {/*
*/} + handlePlanetClick(planet)} + checked={selectedPlanets?.includes(planet)} + /> + {/*
*/} +
+ {planet} +
+
+ ))} +
+
+ +
+
+

Inhabitants

+
+
+ {inhabitantOptions.map(inhabitant => ( +
+
+ handleInhabitantClick(inhabitant)} + checked={selectedInhabitants?.includes(inhabitant)} + /> +
+
+ {inhabitant} +
+
+ ))} +
+
+ +
+
+

Objects

+
+
+ {objectOptions.map(nftObject => ( +
+
+ handleObjectClick(nftObject)} + checked={selectedObjects?.includes(nftObject)} + /> +
+
+ {nftObject} +
+
+ ))} +
+
+
+
+ ); +}; diff --git a/frontend/src/components/filters/helpers.ts b/frontend/src/components/filters/helpers.ts new file mode 100644 index 0000000..3ec9f54 --- /dev/null +++ b/frontend/src/components/filters/helpers.ts @@ -0,0 +1,32 @@ +import { NFTType } from 'fakeData/mockNFTs'; +import { GalleryFiltersProps } from 'pages/nft/NFTs'; + +export const filterNFTs = ( + nfts: NFTType[], + filters: GalleryFiltersProps +) => { + return nfts.filter((nft) => { + let match = true; + + // Filter by planetNumber, if it's set + if (filters.planetNumber !== null) { + match = match && nft.id === filters.planetNumber; + } + + if (filters.planetNames.length !== 0) { + match = match && filters.planetNames.includes(nft.planet); + } + + // Filter by planetInhabitants, if it's set + if (filters.planetInhabitants.length !== 0) { + match = match && filters.planetInhabitants.includes(nft.character); + } + + // Filter by planetObjects, if it's set + if (filters.nftObjects.length !== 0) { + match = match && filters.nftObjects.includes(nft.object); + } + + return match; + }); +} \ No newline at end of file diff --git a/frontend/src/components/filters/options.ts b/frontend/src/components/filters/options.ts new file mode 100644 index 0000000..b82e3d7 --- /dev/null +++ b/frontend/src/components/filters/options.ts @@ -0,0 +1,88 @@ +export const planetOptions = [ + 'Cristall South', + 'Cristall North', + 'Crutha South', + 'Crutha North', + 'Gredica South', + 'Gredica North', + 'Kita South', + 'Kita North', + 'Lusa South', + 'Lusa North', + 'Minas South', + 'Minas North', + 'Ozara South', + 'Ozara North', + 'Pampas South', + 'Pampas North', + 'Sindari South', + 'Sindari North', + 'Zando South', + 'Zando North', +]; + +export const inhabitantOptions = [ + 'Cristallian F', + 'Cristallian M', + 'Cruthan F', + 'Cruthan M', + 'Gredican F', + 'Gredican M', + 'Kitan F', + 'Kitan M', + 'Lusan F', + 'Lusan M', + 'Minasan F', + 'Minasan M', + 'Ozaran F', + 'Ozaran M', + 'Pampan F', + 'Pampan M', + 'Sindarin F', + 'Sindarin M', + 'Zandoan F', + 'Zandoan M', +]; + +export const objectOptions = [ + 'Ancient Lusan Trident', + 'Battle Axe', + 'Battle Shovel', + 'Cristallian Bow', + 'Cristallian Ray Gun', + 'Cristallian Staff', + 'Cristallian Sword', + 'Cruthan Blaster', + 'Cruthan Death Mace', + 'Sindarin Flame Thrower', + 'Golden Hammer', + 'Gredican Power Staff', + 'Gredican Sword', + 'Ice Cleaver', + 'Kitan Ice Bow', + 'Kitan Ice Staff', + 'Kitan Ice Sword', + 'Lusan Water Saber', + 'Lusan Water Staff', + 'Lusan Xtreme Soaker', + 'Minasan Bow', + 'Minasan Ore Staff', + 'Minasan Ore Sword', + 'Ozaran Blaster', + 'Ozaran Bone Axe', + 'Ozaran Death Saber', + 'Ozaran Sand Staff', + 'Pampan Grass Staff', + 'Pampan Grass Sword', + 'Phoenix Rising', + 'Quartz Ray Gun', + 'Royal Ozaran Bow', + 'Sindarin Fire Bow', + 'Sindarin Fire Saber', + 'Sindarin Fire Staff', + 'SST Fishing Pole', + 'Sword of Zando', + 'The Eternal Torch', + 'Staff of Zando', + 'Zandoan Vine Bow', +]; \ No newline at end of file diff --git a/frontend/src/components/filters/search/SearchByID.module.scss b/frontend/src/components/filters/search/SearchByID.module.scss new file mode 100644 index 0000000..3f46aea --- /dev/null +++ b/frontend/src/components/filters/search/SearchByID.module.scss @@ -0,0 +1,48 @@ +.search__container { + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; + font-weight: 500; + margin-bottom: 8px; + + .search__wrapper { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 10px 12px; + background: var(--token-light-500); + border: 1px solid var(--token-light-800); + border-radius: 8px; + width: 200px; + + &.focused { + background-color: var(--token-light-800); + border-color: var(--token-dark-700); + } + + input { + background: transparent; + border: none; + outline: none; + font-size: 14px; + font-weight: 500; + color: var(--token-dark-500); + width: 100%; + } + } + + + input[type="number"]::-webkit-inner-spin-button, + input[type="number"]::-webkit-outer-spin-button { + -webkit-appearance: none; + appearance: none; + margin: 0; + } + + /* Firefox */ + input[type="number"] { + -moz-appearance: textfield; + } +} \ No newline at end of file diff --git a/frontend/src/components/filters/search/SearchByID.tsx b/frontend/src/components/filters/search/SearchByID.tsx new file mode 100644 index 0000000..468dae2 --- /dev/null +++ b/frontend/src/components/filters/search/SearchByID.tsx @@ -0,0 +1,76 @@ +import React, { useState } from 'react'; +import classNames from 'classnames/bind'; +import { ReactComponent as SearchIcon } from 'assets/Search.svg' +import LoadingCircular from 'components/loading/circular'; +import styles from './SearchByID.module.scss'; + +const cx = classNames.bind(styles); + +export const SearchByID = ({ + setSearchValue, + searchValue, + isLoading, +}: { + setSearchValue: (value: string) => void; + searchValue: string; + isLoading: boolean; +}) => { + const [currentSearchValue, setCurrentSearchValue] = useState(searchValue); + const [focused, setFocused] = useState(false); + + const handleSearch = async (e: React.ChangeEvent) => { + const inputValue = e.target.value; + if (/^\d+$/.test(inputValue)) { + const intValue = parseInt(inputValue); + if (intValue >= 0 && intValue <= 10000) { + setCurrentSearchValue(inputValue); + } else { + setCurrentSearchValue(''); + } + } else if (inputValue === '') { + setCurrentSearchValue(inputValue); + } + }; + + const handleKeyPress = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault(); + setSearchValue(currentSearchValue); + } + }; + + const handleBlur = () => { + setFocused(false); + setSearchValue(currentSearchValue); + } + + return ( +
+
+ setFocused(true)} + onBlur={handleBlur} + min="1" + max="10000" + /> + +
+ {isLoading && ( +
+ +
+ )} +
+ ); +}; \ No newline at end of file diff --git a/frontend/src/components/index.ts b/frontend/src/components/index.ts deleted file mode 100644 index 72a86e2..0000000 --- a/frontend/src/components/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { Connect } from "./Connect" diff --git a/frontend/src/components/loading/LoadingCircular.module.scss b/frontend/src/components/loading/LoadingCircular.module.scss new file mode 100644 index 0000000..403299e --- /dev/null +++ b/frontend/src/components/loading/LoadingCircular.module.scss @@ -0,0 +1,43 @@ +.circular__container { + width: 20px; + height: 20px; + display: flex; + display: inline-block; + color: var(--token-primary-500, #00C2FF); + animation: 1.4s linear 0s infinite normal none running rotate-animation; + + .circle { + stroke: currentcolor; + stroke-dasharray: 80px, 200px; + stroke-dashoffset: 0; + animation: 1.4s ease-in-out 0s infinite normal none running spin; + } +} + +@keyframes rotate-animation { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} + + +@keyframes spin { + 0% { + stroke-dasharray: 1px, 200px; + stroke-dashoffset: 0; + } + 50% { + stroke-dasharray: 100px, 200px; + stroke-dashoffset: -15px; + } + 100% { + stroke-dasharray: 100px, 200px; + stroke-dashoffset: -125px; + } +} + + diff --git a/frontend/src/components/loading/circular.tsx b/frontend/src/components/loading/circular.tsx new file mode 100644 index 0000000..034d37a --- /dev/null +++ b/frontend/src/components/loading/circular.tsx @@ -0,0 +1,13 @@ +import styles from './LoadingCircular.module.scss'; + +const LoadingCircular = () => { + return ( +
+ + + +
+ ); +}; + +export default LoadingCircular; \ No newline at end of file diff --git a/frontend/src/components/navigations/desktop/DesktopNav.module.scss b/frontend/src/components/navigations/desktop/DesktopNav.module.scss index 6ce45bc..047370a 100644 --- a/frontend/src/components/navigations/desktop/DesktopNav.module.scss +++ b/frontend/src/components/navigations/desktop/DesktopNav.module.scss @@ -12,6 +12,16 @@ } } + .socials { + display: flex; + gap: 12px; + align-items: center; + a { + display: flex; + align-items: center; + } + } + .link__container { display: flex; align-items: center; @@ -23,10 +33,22 @@ color: var(--token-dark-500); font-size: 16px; font-weight: 600; + display: flex; + align-items: center; + gap: 4px; + + svg { + opacity: 0; + } &:hover { text-decoration: none; cursor: pointer; + + svg { + opacity: 1; + transition: all .2s ease; + } } } diff --git a/frontend/src/components/navigations/desktop/DesktopNav.tsx b/frontend/src/components/navigations/desktop/DesktopNav.tsx index 738481d..78d286a 100644 --- a/frontend/src/components/navigations/desktop/DesktopNav.tsx +++ b/frontend/src/components/navigations/desktop/DesktopNav.tsx @@ -1,19 +1,24 @@ import classNames from 'classnames/bind'; import { Link, NavLink, useLocation } from 'react-router-dom'; import { ReactComponent as Logo } from 'assets/AllianceDAOLogo.svg'; +import { ReactComponent as ExternalLinkIcon } from 'assets/ExternalLink.svg'; import { ReactComponent as CheckIcon } from 'assets/check.svg'; +import { ReactComponent as TwitterIcon } from 'assets/socials/Twitter.svg'; +import { ReactComponent as MediumIcon } from 'assets/socials/Medium.svg'; +import { ReactComponent as TelegramIcon } from 'assets/socials/Telegram.svg'; import { useNav } from '../../../config/routes'; import styles from './DesktopNav.module.scss'; const cx = classNames.bind(styles); const DesktopNav = () => { + const socialSize = 16; const { pathname } = useLocation(); const { menu } = useNav(); return ( ); }; diff --git a/frontend/src/components/navigations/mobile/MobileNav.module.scss b/frontend/src/components/navigations/mobile/MobileNav.module.scss index 85eadbb..9c6c86a 100644 --- a/frontend/src/components/navigations/mobile/MobileNav.module.scss +++ b/frontend/src/components/navigations/mobile/MobileNav.module.scss @@ -99,6 +99,40 @@ color: white; font-size: 18px; font-weight: 600; + display: flex; + align-items: center; + gap: 4px; + + svg { + opacity: 0; + fill: white; + } + + &:hover { + text-decoration: none; + cursor: pointer; + + svg { + opacity: 1; + transition: all .2s ease; + } + } + } + } + } + + .bottom { + display: flex; + flex-direction: column; + gap: 32px; + + .socials { + display: flex; + gap: 24px; + align-items: center; + a { + display: flex; + align-items: center; } } } @@ -112,7 +146,7 @@ border-radius: 8px; background: var(--token-light-500); padding: 0px 24px; - margin-top: 48px; + // margin-top: 48px; color: var(--token-dark-500); font-size: 14px; diff --git a/frontend/src/components/navigations/mobile/MobileNav.tsx b/frontend/src/components/navigations/mobile/MobileNav.tsx index ddd25be..70f1a54 100644 --- a/frontend/src/components/navigations/mobile/MobileNav.tsx +++ b/frontend/src/components/navigations/mobile/MobileNav.tsx @@ -4,6 +4,10 @@ import { ReactComponent as Logo } from 'assets/AllianceDAOLogo.svg'; import { ReactComponent as HamburgerIcon } from 'assets/hamburger.svg'; import { ReactComponent as CloseIcon } from 'assets/close.svg'; import { ReactComponent as CheckIcon } from 'assets/check.svg'; +import { ReactComponent as ExternalLinkIcon } from 'assets/ExternalLink.svg'; +import { ReactComponent as TwitterIcon } from 'assets/socials/Twitter.svg'; +import { ReactComponent as MediumIcon } from 'assets/socials/Medium.svg'; +import { ReactComponent as TelegramIcon } from 'assets/socials/Telegram.svg'; import { useNav } from 'config/routes'; import styles from './MobileNav.module.scss'; @@ -16,6 +20,8 @@ const MobileNav = ({ isMobileNavOpen: boolean, setMobileNavOpen: (isMobileNavOpen: boolean) => void }) => { + const socialSize = 20; + const { pathname } = useLocation(); const { menu } = useNav(); @@ -26,7 +32,7 @@ const MobileNav = ({ return ( <>