├── .prettierignore
├── .eslintignore
├── public
├── bg-tile.png
├── bg-types.png
├── pokemon-egg.png
├── colored-logo.png
├── pokenex-screen.gif
├── typeIcons
│ ├── bug.png
│ ├── dark.png
│ ├── fire.png
│ ├── ice.png
│ ├── rock.png
│ ├── dragon.png
│ ├── fairy.png
│ ├── flying.png
│ ├── ghost.png
│ ├── grass.png
│ ├── ground.png
│ ├── normal.png
│ ├── poison.png
│ ├── steel.png
│ ├── water.png
│ ├── electric.png
│ ├── fighting.png
│ └── psychic.png
├── vercel.svg
└── favicon.svg
├── next-env.d.ts
├── .prettierrc.json
├── next.config.js
├── pages
├── index.tsx
├── _document.tsx
├── 404.tsx
├── user
│ └── [uid].tsx
├── leaderboard.tsx
├── profile.tsx
├── pokemon
│ └── [pid].tsx
├── api
│ ├── users.ts
│ ├── user
│ │ └── [userID].ts
│ ├── signin.ts
│ ├── delete
│ │ └── [deleteID].ts
│ ├── auth
│ │ └── [...nextauth].ts
│ └── update
│ │ └── [updateID].ts
├── explore.tsx
├── play.tsx
├── _error.tsx
└── _app.tsx
├── components
├── Profile
│ ├── ProfilePage.tsx
│ ├── UserPage.tsx
│ ├── Favorites.tsx
│ ├── _profile-page.scss
│ └── ProfileComponent.tsx
├── PokemonPage
│ ├── FlexBetween.tsx
│ ├── PokemonPage.tsx
│ ├── EvoStatCard.tsx
│ ├── _pokemon-page.scss
│ ├── Evolution.tsx
│ └── BioTrainCard.tsx
├── AccessDenied
│ └── AccessDenied.tsx
├── Leaderboard
│ ├── _leaderboard.scss
│ └── LeaderboardPage.tsx
├── Cards
│ ├── EndCard.tsx
│ ├── HiddenCard.tsx
│ ├── FramerCard.tsx
│ ├── Card.tsx
│ ├── _card.scss
│ └── Deck.tsx
├── InputComponents
│ ├── Search.tsx
│ ├── Sort.tsx
│ └── FilterByType.tsx
├── Explore
│ ├── UndoButton.tsx
│ ├── _explore.scss
│ └── ExplorePage.tsx
├── Nav
│ ├── ActiveLink.tsx
│ ├── _navbar.scss
│ └── Nav.tsx
├── LandingPage
│ ├── _landing-page.scss
│ └── LandingPage.tsx
├── HeadTitle
│ └── HeadTitle.tsx
└── Game
│ ├── GameOver.tsx
│ ├── Options.tsx
│ ├── _game.scss
│ └── GamePage.tsx
├── lib
├── reduxHooks.ts
├── pokemonSlice.ts
├── useRefineItems.ts
└── userSlice.ts
├── interfaces
├── next-auth.d.ts
└── Interfaces.ts
├── utils
├── store.ts
└── dataBaseConnection.ts
├── styles
├── styles.scss
├── _pages.scss
└── _config.scss
├── .gitignore
├── models
└── User.ts
├── tsconfig.json
├── helpers
├── getUsers.ts
├── getTypeIconsAndColor.ts
├── getPokemon.ts
└── GlobalFunctions.ts
├── package.json
├── .eslintrc.js
└── README.md
/.prettierignore:
--------------------------------------------------------------------------------
1 |
2 | build
3 | coverage
4 | .vercel
5 | .next
6 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 |
2 | build/*
3 | public/*
4 | docs/*
5 | templates/*
6 |
--------------------------------------------------------------------------------
/public/bg-tile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kitharvey/pokenex/HEAD/public/bg-tile.png
--------------------------------------------------------------------------------
/public/bg-types.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kitharvey/pokenex/HEAD/public/bg-types.png
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/public/pokemon-egg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kitharvey/pokenex/HEAD/public/pokemon-egg.png
--------------------------------------------------------------------------------
/public/colored-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kitharvey/pokenex/HEAD/public/colored-logo.png
--------------------------------------------------------------------------------
/public/pokenex-screen.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kitharvey/pokenex/HEAD/public/pokenex-screen.gif
--------------------------------------------------------------------------------
/public/typeIcons/bug.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kitharvey/pokenex/HEAD/public/typeIcons/bug.png
--------------------------------------------------------------------------------
/public/typeIcons/dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kitharvey/pokenex/HEAD/public/typeIcons/dark.png
--------------------------------------------------------------------------------
/public/typeIcons/fire.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kitharvey/pokenex/HEAD/public/typeIcons/fire.png
--------------------------------------------------------------------------------
/public/typeIcons/ice.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kitharvey/pokenex/HEAD/public/typeIcons/ice.png
--------------------------------------------------------------------------------
/public/typeIcons/rock.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kitharvey/pokenex/HEAD/public/typeIcons/rock.png
--------------------------------------------------------------------------------
/public/typeIcons/dragon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kitharvey/pokenex/HEAD/public/typeIcons/dragon.png
--------------------------------------------------------------------------------
/public/typeIcons/fairy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kitharvey/pokenex/HEAD/public/typeIcons/fairy.png
--------------------------------------------------------------------------------
/public/typeIcons/flying.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kitharvey/pokenex/HEAD/public/typeIcons/flying.png
--------------------------------------------------------------------------------
/public/typeIcons/ghost.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kitharvey/pokenex/HEAD/public/typeIcons/ghost.png
--------------------------------------------------------------------------------
/public/typeIcons/grass.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kitharvey/pokenex/HEAD/public/typeIcons/grass.png
--------------------------------------------------------------------------------
/public/typeIcons/ground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kitharvey/pokenex/HEAD/public/typeIcons/ground.png
--------------------------------------------------------------------------------
/public/typeIcons/normal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kitharvey/pokenex/HEAD/public/typeIcons/normal.png
--------------------------------------------------------------------------------
/public/typeIcons/poison.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kitharvey/pokenex/HEAD/public/typeIcons/poison.png
--------------------------------------------------------------------------------
/public/typeIcons/steel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kitharvey/pokenex/HEAD/public/typeIcons/steel.png
--------------------------------------------------------------------------------
/public/typeIcons/water.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kitharvey/pokenex/HEAD/public/typeIcons/water.png
--------------------------------------------------------------------------------
/public/typeIcons/electric.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kitharvey/pokenex/HEAD/public/typeIcons/electric.png
--------------------------------------------------------------------------------
/public/typeIcons/fighting.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kitharvey/pokenex/HEAD/public/typeIcons/fighting.png
--------------------------------------------------------------------------------
/public/typeIcons/psychic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kitharvey/pokenex/HEAD/public/typeIcons/psychic.png
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "printWidth": 100,
4 | "semi": false,
5 | "tabWidth": 2
6 | }
7 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | images: {
3 | domains: ['raw.githubusercontent.com', 'avatars.githubusercontent.com'],
4 | },
5 | }
--------------------------------------------------------------------------------
/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import HeadTitle from "@components/HeadTitle/HeadTitle"
2 | import LandingPage from "@components/LandingPage/LandingPage"
3 |
4 | const Home = () => {
5 | return (
6 | <>
7 |
8 |
9 | >
10 | )
11 | }
12 |
13 | export default Home
14 |
--------------------------------------------------------------------------------
/components/Profile/ProfilePage.tsx:
--------------------------------------------------------------------------------
1 | import { useAppSelector } from "@lib/reduxHooks"
2 | import ProfileComponent from "./ProfileComponent"
3 |
4 | const ProfilePage = () => {
5 | const { userData } = useAppSelector((state) => state.user)
6 |
7 | return <>{userData && }>
8 | }
9 |
10 | export default ProfilePage
11 |
--------------------------------------------------------------------------------
/lib/reduxHooks.ts:
--------------------------------------------------------------------------------
1 | import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux"
2 | import type { RootState, AppDispatch } from "@utils/store"
3 |
4 | // Use throughout your app instead of plain `useDispatch` and `useSelector`
5 | export const useAppDispatch = () => useDispatch()
6 | export const useAppSelector: TypedUseSelectorHook = useSelector
7 |
--------------------------------------------------------------------------------
/interfaces/next-auth.d.ts:
--------------------------------------------------------------------------------
1 | import NextAuth from "next-auth"
2 |
3 | declare module "next-auth" {
4 | /**
5 | * Returned by `useSession`, `getSession` and received as a prop on the `Provider` React Context
6 | */
7 | export interface Session {
8 | user: {
9 | name?: string | null
10 | picture?: string | null
11 | uid?: string | null
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import NextDocument, { Html, Head, Main, NextScript } from "next/document"
2 |
3 | class Document extends NextDocument {
4 | render() {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | )
14 | }
15 | }
16 |
17 | export default Document
18 |
--------------------------------------------------------------------------------
/pages/404.tsx:
--------------------------------------------------------------------------------
1 | import HeadTitle from "@components/HeadTitle/HeadTitle"
2 | import React from "react"
3 |
4 | const Error404 = () => {
5 | return (
6 |
7 |
8 |
404
9 |
This page could not be found.
10 |
11 | )
12 | }
13 |
14 | export default Error404
15 |
--------------------------------------------------------------------------------
/utils/store.ts:
--------------------------------------------------------------------------------
1 | import { configureStore } from "@reduxjs/toolkit"
2 | import userReducer from "@lib/userSlice"
3 | import pokemonReducer from "@lib/pokemonSlice"
4 |
5 | export const store = configureStore({
6 | reducer: {
7 | user: userReducer,
8 | pokemon: pokemonReducer,
9 | },
10 | })
11 |
12 | export type RootState = ReturnType
13 | export type AppDispatch = typeof store.dispatch
14 |
--------------------------------------------------------------------------------
/utils/dataBaseConnection.ts:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose"
2 |
3 | async function dbConnect() {
4 | if (mongoose.connection.readyState >= 1) {
5 | return false
6 | }
7 |
8 | return mongoose.connect(`${process.env.MONGODB_URI}`, {
9 | useNewUrlParser: true,
10 | useUnifiedTopology: true,
11 | useFindAndModify: false,
12 | useCreateIndex: true,
13 | })
14 | }
15 |
16 | export default dbConnect
17 |
--------------------------------------------------------------------------------
/components/PokemonPage/FlexBetween.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 |
3 | type FlexBetweenProps = {
4 | category: string
5 | details: React.ReactNode
6 | }
7 |
8 | const FlexBetween: React.FC = ({ category, details }) => (
9 |
10 |
{category}
11 |
{details}
12 |
13 | )
14 |
15 | export default FlexBetween
16 |
--------------------------------------------------------------------------------
/pages/user/[uid].tsx:
--------------------------------------------------------------------------------
1 | import AccessDenied from "@components/AccessDenied/AccessDenied"
2 | import UserPage from "@components/Profile/UserPage"
3 | import { useSession } from "next-auth/client"
4 | import { useRouter } from "next/router"
5 | import React from "react"
6 |
7 | const User = () => {
8 | const [session] = useSession()
9 | const router = useRouter()
10 |
11 | if (!session) return
12 |
13 | return <>{router.query.uid && }>
14 | }
15 |
16 | export default User
17 |
--------------------------------------------------------------------------------
/styles/styles.scss:
--------------------------------------------------------------------------------
1 | @import "config";
2 | @import "@components/Nav/navbar";
3 | @import "@components/Leaderboard/leaderboard";
4 | @import "@components/Cards/card";
5 | @import "@components/Explore/explore";
6 | @import "@components/Game/game";
7 | @import "@components/Profile/profile-page";
8 | @import "@components/PokemonPage/pokemon-page";
9 | @import "@components/LandingPage/landing-page";
10 | @import "pages";
11 |
12 | a {
13 | color: inherit;
14 | text-decoration: none;
15 | }
16 |
17 | * {
18 | box-sizing: border-box;
19 | }
20 |
--------------------------------------------------------------------------------
/pages/leaderboard.tsx:
--------------------------------------------------------------------------------
1 | import LeaderboardPage from "@components/Leaderboard/LeaderboardPage"
2 | import HeadTitle from "@components/HeadTitle/HeadTitle"
3 | import { useSession } from "next-auth/client"
4 | import AccessDenied from "@components/AccessDenied/AccessDenied"
5 |
6 | const Leaderboard = () => {
7 | const [session] = useSession()
8 | if (!session) return
9 | return (
10 | <>
11 |
12 |
13 | >
14 | )
15 | }
16 |
17 | export default Leaderboard
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env.local
29 | .env.development.local
30 | .env.test.local
31 | .env.production.local
32 |
33 | # vercel
34 | .vercel
35 |
36 | .vercel
37 |
--------------------------------------------------------------------------------
/pages/profile.tsx:
--------------------------------------------------------------------------------
1 | import AccessDenied from "@components/AccessDenied/AccessDenied"
2 | import HeadTitle from "@components/HeadTitle/HeadTitle"
3 | import ProfilePage from "@components/Profile/ProfilePage"
4 | import { useSession } from "next-auth/client"
5 |
6 | const Profile = () => {
7 | const [session] = useSession()
8 | if (!session) return
9 | return (
10 | <>
11 |
12 |
13 | >
14 | )
15 | }
16 |
17 | export default Profile
18 |
--------------------------------------------------------------------------------
/components/Profile/UserPage.tsx:
--------------------------------------------------------------------------------
1 | import HeadTitle from "@components/HeadTitle/HeadTitle"
2 | import { getUsers } from "@helpers/getUsers"
3 | import useSWR from "swr"
4 | import ProfileComponent from "./ProfileComponent"
5 |
6 | const UserPage: React.FC<{ id: string | string[] }> = ({ id }) => {
7 | const { data } = useSWR(`/api/user/${id}`, getUsers)
8 | return (
9 | <>
10 |
11 | {data && }
12 | >
13 | )
14 | }
15 |
16 | export default UserPage
17 |
--------------------------------------------------------------------------------
/models/User.ts:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose"
2 |
3 | const userSchema = new mongoose.Schema({
4 | uid: {
5 | type: String,
6 | required: true,
7 | },
8 | name: {
9 | type: String,
10 | required: true,
11 | },
12 | picture: {
13 | type: String,
14 | required: true,
15 | },
16 | favorites: [
17 | {
18 | id: Number,
19 | name: String,
20 | types: [String],
21 | sprite: String,
22 | },
23 | ],
24 | score: {
25 | type: Number,
26 | default: 0,
27 | },
28 | })
29 |
30 | export default mongoose.models.User || mongoose.model("User", userSchema)
31 |
--------------------------------------------------------------------------------
/pages/pokemon/[pid].tsx:
--------------------------------------------------------------------------------
1 | import HeadTitle from "@components/HeadTitle/HeadTitle"
2 | import PokemonPage from "@components/PokemonPage/PokemonPage"
3 | import { useAppSelector } from "@lib/reduxHooks"
4 | import Case from "case"
5 |
6 | const Pokemon = () => {
7 | const { pokemonData } = useAppSelector((state) => state.pokemon)
8 |
9 | return (
10 | <>
11 |
16 |
17 | >
18 | )
19 | }
20 |
21 | export default Pokemon
22 |
--------------------------------------------------------------------------------
/pages/api/users.ts:
--------------------------------------------------------------------------------
1 | import type { NextApiRequest, NextApiResponse } from "next"
2 | import User from "@models/User"
3 | import dbConnect from "@utils/dataBaseConnection"
4 |
5 | export default async (req: NextApiRequest, res: NextApiResponse) => {
6 | await dbConnect()
7 | const { method } = req
8 | if (method === "GET") {
9 | try {
10 | const users = await User.find()
11 | return res.status(200).json(users)
12 | } catch (error) {
13 | return res.status(500).json({ message: error.message })
14 | }
15 | } else {
16 | return res.status(400).json({ message: "invalid method" })
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/components/AccessDenied/AccessDenied.tsx:
--------------------------------------------------------------------------------
1 | import HeadTitle from "@components/HeadTitle/HeadTitle"
2 | import { signIn } from "next-auth/client"
3 | import { FaGithub } from "react-icons/fa"
4 |
5 | const AccessDenied = () => {
6 | return (
7 |
8 |
9 |
Access Denied
10 |
You must be signed in to view this page
11 |
signIn("github")}>
12 | Sign in with
13 |
14 |
15 | )
16 | }
17 |
18 | export default AccessDenied
19 |
--------------------------------------------------------------------------------
/components/Leaderboard/_leaderboard.scss:
--------------------------------------------------------------------------------
1 | .leaderboard-page {
2 | margin-top: 40px;
3 |
4 | .table-wrapper {
5 | .table-head,
6 | .table-row {
7 | display: grid;
8 | grid-template-columns: repeat(3, 1fr);
9 | align-items: center;
10 | justify-items: center;
11 | height: 40px;
12 | }
13 | .table-row {
14 | font-weight: 300;
15 | cursor: pointer;
16 | border-radius: 5px;
17 | transition: all 0.1s ease-in-out;
18 | &:hover {
19 | background: #00000025;
20 | }
21 | }
22 | }
23 | }
24 |
25 | .spinner-wrapper {
26 | position: fixed;
27 | bottom: 50px;
28 | right: 50px;
29 | }
30 |
--------------------------------------------------------------------------------
/components/Cards/EndCard.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image"
2 | import React from "react"
3 |
4 | const EndCard: React.FC = () => {
5 | return (
6 |
7 |
13 |
21 |
End of Deck
22 |
23 |
24 | )
25 | }
26 |
27 | export default EndCard
28 |
--------------------------------------------------------------------------------
/components/InputComponents/Search.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { FaSearch } from "react-icons/fa"
3 |
4 | interface SearchProps {
5 | handleSearch: (event: React.ChangeEvent) => void
6 | searchValue: string
7 | }
8 |
9 | const Search: React.FC = ({ handleSearch, searchValue }) => {
10 | return (
11 |
12 |
13 |
22 |
23 | )
24 | }
25 |
26 | export default Search
27 |
--------------------------------------------------------------------------------
/pages/explore.tsx:
--------------------------------------------------------------------------------
1 | import ExplorePage from "@components/Explore/ExplorePage"
2 | import HeadTitle from "@components/HeadTitle/HeadTitle"
3 | import { fetchExploreList } from "@helpers/getPokemon"
4 | import { PokemonDataInterface } from "@interfaces/Interfaces"
5 | import { InferGetStaticPropsType } from "next"
6 |
7 | export const getStaticProps = async () => {
8 | const list = await fetchExploreList()
9 | return {
10 | props: {
11 | pokemons: list,
12 | },
13 | }
14 | }
15 |
16 | const Explore = ({ pokemons }: InferGetStaticPropsType) => {
17 | return (
18 | <>
19 |
20 | {pokemons && }
21 | >
22 | )
23 | }
24 |
25 | export default Explore
26 |
--------------------------------------------------------------------------------
/components/Explore/UndoButton.tsx:
--------------------------------------------------------------------------------
1 | import { motion } from "framer-motion"
2 | import React from "react"
3 | import { FaUndoAlt } from "react-icons/fa"
4 |
5 | interface UndoProps {
6 | handleUndo: () => void
7 | }
8 |
9 | const UndoButton: React.FC = ({ handleUndo }) => {
10 | return (
11 | handleUndo()}
14 | style={{
15 | boxShadow: "rgba(0, 0, 0, 0.314) 0 0px 10px 1px",
16 | }}
17 | whileTap={{
18 | rotate: -180,
19 | }}
20 | whileHover={{
21 | cursor: "pointer",
22 | boxShadow: "rgba(0, 0, 0, 0.314) 0px 0px 15px 1px",
23 | }}
24 | >
25 |
26 |
27 | )
28 | }
29 |
30 | export default UndoButton
31 |
--------------------------------------------------------------------------------
/components/Nav/ActiveLink.tsx:
--------------------------------------------------------------------------------
1 | import { useRouter } from "next/router"
2 | import Link from "next/link"
3 | import React, { Children } from "react"
4 |
5 | interface ActiveLinkProps {
6 | children: JSX.Element
7 | activeClassName: string
8 | href: string
9 | }
10 |
11 | const ActiveLink: React.FC = ({ children, activeClassName, ...props }) => {
12 | const { asPath } = useRouter()
13 | const child = Children.only(children)
14 | const childClassName = child.props.className || ""
15 |
16 | const className =
17 | asPath === props.href ? `${childClassName} ${activeClassName}`.trim() : childClassName
18 |
19 | return (
20 |
21 | {React.cloneElement(child, {
22 | className: className || null,
23 | })}
24 |
25 | )
26 | }
27 |
28 | export default ActiveLink
29 |
--------------------------------------------------------------------------------
/pages/play.tsx:
--------------------------------------------------------------------------------
1 | import GamePage from "@components/Game/GamePage"
2 | import { InferGetServerSidePropsType } from "next"
3 | import { fetchPlayList } from "@helpers/getPokemon"
4 | import HeadTitle from "@components/HeadTitle/HeadTitle"
5 |
6 | export const getStaticProps = async () => {
7 | const pokemons = await fetchPlayList()
8 | if (!pokemons) {
9 | return {
10 | redirect: {
11 | destination: "/",
12 | permanent: false,
13 | },
14 | }
15 | }
16 |
17 | return {
18 | props: {
19 | pokemons,
20 | },
21 | }
22 | }
23 |
24 | const Play = ({ pokemons }: InferGetServerSidePropsType) => {
25 | return (
26 | <>
27 |
28 | {pokemons && }
29 | >
30 | )
31 | }
32 |
33 | export default Play
34 |
--------------------------------------------------------------------------------
/components/InputComponents/Sort.tsx:
--------------------------------------------------------------------------------
1 | import { SortKey } from "@lib/useRefineItems"
2 | import React from "react"
3 | import { FaSort } from "react-icons/fa"
4 |
5 | interface FilterByTypeProps {
6 | handleSort: (event: React.ChangeEvent) => void
7 | sortValue: SortKey | null
8 | }
9 |
10 | const Sort: React.FC = ({ handleSort, sortValue }) => {
11 | return (
12 |
13 |
14 |
20 | sort by ID
21 | sort by name
22 | shuffle
23 |
24 |
25 | )
26 | }
27 |
28 | export default Sort
29 |
--------------------------------------------------------------------------------
/pages/api/user/[userID].ts:
--------------------------------------------------------------------------------
1 | import type { NextApiRequest, NextApiResponse } from "next"
2 | import User from "@models/User"
3 | import dbConnect from "@utils/dataBaseConnection"
4 | import { getSession } from "next-auth/client"
5 |
6 | export default async (req: NextApiRequest, res: NextApiResponse) => {
7 | await dbConnect()
8 | const session = getSession({ req })
9 | const { userID } = req.query
10 | const { method } = req
11 | if (method === "GET" && session) {
12 | try {
13 | const user = await User.findById(userID)
14 | if (user) {
15 | return res.json({ user })
16 | }
17 | return res.status(404).json({ message: "Cannot find user" })
18 | } catch (err) {
19 | return res.status(400).json({ err })
20 | }
21 | } else {
22 | return res.status(400).json({ message: "invalid method or not authorized" })
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/components/LandingPage/_landing-page.scss:
--------------------------------------------------------------------------------
1 | .landing-page {
2 | width: 100%;
3 | min-height: 80vh;
4 | display: flex;
5 | justify-content: space-between;
6 | align-items: center;
7 |
8 | .right {
9 | display: none;
10 | @media screen and (min-width: 640px) {
11 | display: block;
12 | }
13 | .card-wrapper {
14 | height: 315px;
15 | width: 240px;
16 |
17 | button {
18 | display: none;
19 | }
20 | .name {
21 | font-size: medium;
22 | }
23 | }
24 | }
25 |
26 | .left {
27 | h1 {
28 | line-height: 1.2;
29 | }
30 | .button-wrapper {
31 | margin-top: 20px;
32 | button {
33 | margin-right: 10px;
34 | }
35 | }
36 | .signin {
37 | font-size: smaller;
38 | button:hover {
39 | color: $color-accent-dark;
40 | }
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/pages/_error.tsx:
--------------------------------------------------------------------------------
1 | import HeadTitle from "@components/HeadTitle/HeadTitle"
2 | import type { NextPageContext } from "next"
3 |
4 | interface ErrorProps {
5 | statusCode: number
6 | message: string
7 | }
8 |
9 | function Error({ statusCode, message }: ErrorProps) {
10 | return (
11 |
12 |
13 | {statusCode &&
{statusCode}
}
14 | {message &&
{message}
}
15 |
16 | )
17 | }
18 |
19 | Error.getInitialProps = ({ res, err }: NextPageContext) => {
20 | let statusCode
21 | let message
22 | if (res) {
23 | statusCode = res.statusCode
24 | message = res.statusMessage
25 | }
26 | if (err) {
27 | statusCode = err.statusCode
28 | message = err.message
29 | }
30 |
31 | return { statusCode, message }
32 | }
33 |
34 | export default Error
35 |
--------------------------------------------------------------------------------
/pages/api/signin.ts:
--------------------------------------------------------------------------------
1 | import type { NextApiRequest, NextApiResponse } from "next"
2 | import User from "@models/User"
3 | import dbConnect from "@utils/dataBaseConnection"
4 |
5 | export default async (req: NextApiRequest, res: NextApiResponse) => {
6 | await dbConnect()
7 |
8 | let user
9 | const { uid, name, picture } = req.body
10 | const { method } = req
11 | if (method === "POST") {
12 | try {
13 | const isUserAlready = await User.findOne({ uid })
14 | if (isUserAlready) {
15 | user = isUserAlready
16 | return res.status(200).json({ user })
17 | }
18 | const newuser = new User({ uid, name, picture })
19 | user = await newuser.save()
20 | return res.status(201).json({ user })
21 | } catch (err) {
22 | return res.status(400).json({ err })
23 | }
24 | } else {
25 | return res.status(400).json({ message: "invalid method" })
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/components/Cards/HiddenCard.tsx:
--------------------------------------------------------------------------------
1 | import { getPokemonImage } from "@helpers/GlobalFunctions"
2 | import { NameIDInterface } from "@interfaces/Interfaces"
3 | import Image from "next/image"
4 | import React from "react"
5 |
6 | interface ExploreCardProps {
7 | pokemon: NameIDInterface
8 | reveal: boolean
9 | }
10 |
11 | const HiddenCard: React.FC = ({ pokemon, reveal }) => {
12 | const imgsrc = getPokemonImage(+pokemon.id)
13 | return (
14 |
26 | )
27 | }
28 |
29 | export default HiddenCard
30 |
--------------------------------------------------------------------------------
/pages/api/delete/[deleteID].ts:
--------------------------------------------------------------------------------
1 | import type { NextApiRequest, NextApiResponse } from "next"
2 | import User from "@models/User"
3 | import dbConnect from "@utils/dataBaseConnection"
4 | import { getSession } from "next-auth/client"
5 |
6 | export default async (req: NextApiRequest, res: NextApiResponse) => {
7 | await dbConnect()
8 | const session = getSession({ req })
9 | const { deleteID } = req.query
10 | const { method } = req
11 | if (method === "DELETE" && session) {
12 | try {
13 | const isUserAlready = await User.findById(deleteID)
14 | if (isUserAlready) {
15 | isUserAlready.remove()
16 | return res.json({ message: `Account was deleted` })
17 | }
18 | return res.status(404).json({ message: "Cannot find user" })
19 | } catch (err) {
20 | return res.status(400).json({ err })
21 | }
22 | } else {
23 | return res.status(400).json({ message: "invalid method or not authorized" })
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/pages/api/auth/[...nextauth].ts:
--------------------------------------------------------------------------------
1 | import NextAuth from "next-auth"
2 | import Providers from "next-auth/providers"
3 | import { NextApiRequest, NextApiResponse } from "next"
4 |
5 | export default (req: NextApiRequest, res: NextApiResponse) =>
6 | NextAuth(req, res, {
7 | providers: [
8 | Providers.GitHub({
9 | clientId: process.env.GITHUB_CLIENT_ID,
10 | clientSecret: process.env.GITHUB_CLIENT_SECRET,
11 | scope: "read:user",
12 | }),
13 | ],
14 | session: {
15 | maxAge: 60 * 60 * 24,
16 | },
17 | secret: process.env.AUTH_SECRET,
18 | jwt: {
19 | secret: process.env.JWT_SECRET,
20 | },
21 | callbacks: {
22 | session: async (session, user) => {
23 | session.user = user
24 | session.user = {
25 | name: user.name,
26 | picture: user.picture as string,
27 | uid: user.sub as string,
28 | }
29 | return Promise.resolve(session)
30 | },
31 | },
32 | })
33 |
--------------------------------------------------------------------------------
/styles/_pages.scss:
--------------------------------------------------------------------------------
1 | .page {
2 | width: 100%;
3 | min-height: 80vh;
4 | height: max-content;
5 | display: flex;
6 | flex-direction: column;
7 | align-items: center;
8 | justify-content: center;
9 | user-select: none;
10 |
11 | .black-button {
12 | border-radius: 5px;
13 | margin-top: 10px;
14 | font-size: 1rem;
15 | padding: 5px 10px;
16 | }
17 | }
18 |
19 | .error-page {
20 | width: 100%;
21 | min-height: 80vh;
22 | height: max-content;
23 | display: flex;
24 | align-items: center;
25 | justify-content: center;
26 |
27 | .status-code {
28 | display: inline-block;
29 | border-right: 1px solid rgba(0, 0, 0, 0.3);
30 | margin: 0;
31 | margin-right: 20px;
32 | padding: 10px 23px 10px 0;
33 | font-size: 24px;
34 | font-weight: 500;
35 | vertical-align: top;
36 | }
37 | .message {
38 | font-size: 14px;
39 | font-weight: normal;
40 | line-height: inherit;
41 | margin: 0;
42 | padding: 0;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowJs": true,
4 | "alwaysStrict": true,
5 | "esModuleInterop": true,
6 | "forceConsistentCasingInFileNames": true,
7 | "isolatedModules": true,
8 | "jsx": "preserve",
9 | "lib": ["dom", "es2019"],
10 | "module": "esnext",
11 | "moduleResolution": "node",
12 | "noEmit": true,
13 | "noFallthroughCasesInSwitch": true,
14 | "noUnusedLocals": true,
15 | "noUnusedParameters": true,
16 | "resolveJsonModule": true,
17 | "skipLibCheck": true,
18 | "strict": true,
19 | "target": "esnext",
20 | "baseUrl": "./",
21 | "paths": {
22 | "@models/*": ["models/*"],
23 | "@utils/*": ["utils/*"],
24 | "@components/*": ["components/*"],
25 | "@lib/*": ["lib/*"],
26 | "@helpers/*": ["helpers/*"],
27 | "@interfaces/*": ["interfaces/*"],
28 | "@styles/*": ["styles/*"]
29 | }
30 | },
31 |
32 | "exclude": ["node_modules"],
33 | "include": ["**/*.ts", "**/*.tsx"]
34 | }
35 |
--------------------------------------------------------------------------------
/pages/api/update/[updateID].ts:
--------------------------------------------------------------------------------
1 | import type { NextApiRequest, NextApiResponse } from "next"
2 | import User from "@models/User"
3 | import dbConnect from "@utils/dataBaseConnection"
4 | import { getSession } from "next-auth/client"
5 |
6 | export default async (req: NextApiRequest, res: NextApiResponse) => {
7 | await dbConnect()
8 | const session = getSession({ req })
9 | const {
10 | method,
11 | query: { updateID },
12 | body,
13 | } = req
14 | if (method === "PATCH" && session) {
15 | try {
16 | const isUserAlready = await User.findById(updateID)
17 | if (isUserAlready) {
18 | isUserAlready[body.key] = body[body.key]
19 | const user = await isUserAlready.save()
20 | return res.status(202).json({ user })
21 | }
22 | return res.status(404).json({ message: "Cannot find user" })
23 | } catch (err) {
24 | return res.status(400).json({ err })
25 | }
26 | } else {
27 | return res.status(400).json({ message: "invalid method or not authorized" })
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
3 |
4 |
--------------------------------------------------------------------------------
/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import Router from "next/router"
2 | import type { AppProps } from "next/app"
3 | import { Provider as ReduxProvider } from "react-redux"
4 | import { Provider as NextAuthProvider } from "next-auth/client"
5 | import ProgressBar from "@badrap/bar-of-progress"
6 | import Nav from "@components/Nav/Nav"
7 | import "@styles/styles.scss"
8 | import { store } from "@utils/store"
9 |
10 | const progress = new ProgressBar({
11 | size: 2,
12 | color: "#ff5a5f",
13 | className: "bar-of-progress",
14 | delay: 100,
15 | })
16 |
17 | Router.events.on("routeChangeStart", progress.start)
18 | Router.events.on("routeChangeComplete", progress.finish)
19 | Router.events.on("routeChangeError", progress.finish)
20 |
21 | const MyApp = ({ Component, pageProps }: AppProps) => {
22 | return (
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | )
32 | }
33 |
34 | export default MyApp
35 |
--------------------------------------------------------------------------------
/components/InputComponents/FilterByType.tsx:
--------------------------------------------------------------------------------
1 | import { UserFavoritesProps } from "@interfaces/Interfaces"
2 | import React from "react"
3 | import { FiFilter } from "react-icons/fi"
4 |
5 | interface FilterByTypeProps {
6 | list: UserFavoritesProps[]
7 | handleFilterByType: (event: React.ChangeEvent) => void
8 | filterValue: string
9 | }
10 |
11 | const FilterByType: React.FC = ({ list, handleFilterByType, filterValue }) => {
12 | const flat = list.map((item) => item.types).flat()
13 | const arrUnique = Array.from(new Set(flat))
14 | return (
15 |
16 |
17 |
23 | filter by type
24 | {arrUnique.map((option) => (
25 |
26 | {option}
27 |
28 | ))}
29 |
30 |
31 | )
32 | }
33 |
34 | export default FilterByType
35 |
--------------------------------------------------------------------------------
/components/Explore/_explore.scss:
--------------------------------------------------------------------------------
1 | .item-wrapper {
2 | padding: 10px 20px;
3 | display: flex;
4 | align-items: flex-start;
5 | justify-content: center;
6 | flex-direction: column;
7 | @media screen and (min-width: 640px) {
8 | flex-direction: row;
9 | align-items: center;
10 | }
11 |
12 | .select-wrapper {
13 | position: relative;
14 | color: #666;
15 | cursor: pointer;
16 |
17 | &:focus-within,
18 | &:hover {
19 | color: #000;
20 | }
21 |
22 | select,
23 | input {
24 | appearance: none;
25 | font-family: inherit;
26 | border: none;
27 | outline: none;
28 | font-family: inherit;
29 | background: none;
30 | font-size: 0.75rem;
31 | padding: 5px 10px 5px 20px;
32 | color: inherit;
33 | max-width: 180px;
34 | }
35 |
36 | svg {
37 | position: absolute;
38 | left: 0;
39 | top: 50%;
40 | transform: translateY(-50%);
41 | pointer-events: none;
42 | }
43 | }
44 | }
45 |
46 | .undo-wrapper {
47 | border-radius: 50%;
48 | padding: 10px;
49 | background: #fff;
50 | display: flex;
51 | align-items: center;
52 | justify-content: center;
53 | }
54 |
--------------------------------------------------------------------------------
/components/HeadTitle/HeadTitle.tsx:
--------------------------------------------------------------------------------
1 | import Head from "next/head"
2 | import React from "react"
3 |
4 | interface HeadTitleProps {
5 | title: string
6 | }
7 |
8 | const metaDescription =
9 | 'A pokédex in card style. Explore and swipe through pokemon cards or play "who\'s that pokemon?".'
10 | const metaIMG = "./screen.gif"
11 |
12 | const HeadTitle: React.FC = ({ title }) => {
13 | return (
14 |
15 | {title}
16 |
17 |
18 |
19 | {title}
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | )
31 | }
32 |
33 | export default HeadTitle
34 |
--------------------------------------------------------------------------------
/components/Leaderboard/LeaderboardPage.tsx:
--------------------------------------------------------------------------------
1 | import { getUsers } from "@helpers/getUsers"
2 | import { UserProps } from "@interfaces/Interfaces"
3 | import Link from "next/link"
4 | import useSWR from "swr"
5 |
6 | const LeaderboardPage = () => {
7 | const { data: list } = useSWR(`/api/users`, getUsers)
8 |
9 | return (
10 |
11 | {list && (
12 |
32 | )}
33 |
34 | )
35 | }
36 |
37 | export default LeaderboardPage
38 |
--------------------------------------------------------------------------------
/helpers/getUsers.ts:
--------------------------------------------------------------------------------
1 | import { UserSessionProps, UserFavoritesProps } from "@interfaces/Interfaces"
2 | import axios from "axios"
3 |
4 | export async function getUsers(link: string) {
5 | const { data } = await axios.get(`${link}`)
6 | return data
7 | }
8 |
9 | export const getUserSignIn = async (body: UserSessionProps) => {
10 | const { data } = await axios.post(`/api/signin`, body)
11 | return data
12 | }
13 |
14 | export const deleteUser = async (id: string) => {
15 | const { data } = await axios.delete(`/api/delete/${id}`)
16 | return data
17 | }
18 |
19 | type UpdateKey = "score" | "favorites" | "name"
20 |
21 | export const updateUserScore = async (id: string, key: UpdateKey, score: number) => {
22 | const { data } = await axios.patch(`/api/update/${id}`, { key, score })
23 | return data
24 | }
25 |
26 | export const updateUserFavorites = async (
27 | id: string,
28 | key: UpdateKey,
29 | favorites: UserFavoritesProps[]
30 | ) => {
31 | const { data } = await axios.patch(`/api/update/${id}`, { key, favorites })
32 | return data
33 | }
34 |
35 | export const updateUserName = async (id: string, key: UpdateKey, name: string) => {
36 | const { data } = await axios.patch(`/api/update/${id}`, { key, name })
37 | return data
38 | }
39 |
--------------------------------------------------------------------------------
/components/Game/GameOver.tsx:
--------------------------------------------------------------------------------
1 | import { useAppDispatch, useAppSelector } from "@lib/reduxHooks"
2 | import { updateScore } from "@lib/userSlice"
3 | import { useSession } from "next-auth/client"
4 | import Link from "next/link"
5 | import React, { useEffect } from "react"
6 | import { IoClose } from "react-icons/io5"
7 |
8 | interface GameOverProps {
9 | score: number
10 | handleCloseModal: () => void
11 | }
12 |
13 | const GameOver: React.FC = ({ score, handleCloseModal }) => {
14 | const { userData } = useAppSelector((state) => state.user)
15 | const dispath = useAppDispatch()
16 | const [session] = useSession()
17 |
18 | useEffect(() => {
19 | if (session) dispath(updateScore(score))
20 | }, [score, dispath, session])
21 |
22 | return (
23 |
24 |
25 |
26 | handleCloseModal()} />
27 |
28 |
Game Over
29 |
score: {score}
30 | {session &&
best score: {userData?.score}
}
31 | {session && (
32 |
33 |
34 | View Leaderboard
35 |
36 |
37 | )}
38 |
39 |
40 | )
41 | }
42 |
43 | export default GameOver
44 |
--------------------------------------------------------------------------------
/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 |
11 |
14 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/components/Game/Options.tsx:
--------------------------------------------------------------------------------
1 | import { motion } from "framer-motion"
2 | import React from "react"
3 |
4 | interface OptionsProps {
5 | options: string[]
6 | handleSelect: (option: string) => void
7 | reveal: boolean
8 | chosen: string | null
9 | answer: string
10 | }
11 |
12 | const Options: React.FC = ({ options, handleSelect, reveal, chosen, answer }) => {
13 | const buttonBGColor = (opt: string) => {
14 | if (reveal) {
15 | if (opt === chosen) {
16 | if (chosen === answer) {
17 | return "#6EE7B7"
18 | }
19 | return "#FCA5A5"
20 | }
21 | if (answer === opt) {
22 | return "#6EE7B7"
23 | }
24 | return "#FFF"
25 | }
26 | return "#FFF"
27 | }
28 |
29 | return (
30 |
31 | {options.map((option) => (
32 |
handleSelect(option)}
40 | whileTap={{
41 | scale: 0.95,
42 | }}
43 | whileHover={{
44 | cursor: "pointer",
45 | boxShadow: "rgba(0, 0, 0, 0.314) 0px 0px 15px 1px",
46 | }}
47 | >
48 | {option}
49 |
50 | ))}
51 |
52 | )
53 | }
54 |
55 | export default Options
56 |
--------------------------------------------------------------------------------
/styles/_config.scss:
--------------------------------------------------------------------------------
1 | @import url("https://fonts.googleapis.com/css2?family=Poppins:wght@300;500;700&display=swap");
2 |
3 | $color-accent-light: #ff5a5f;
4 | $color-accent-dark: #c81d25;
5 |
6 | *,
7 | *::before,
8 | *::after {
9 | margin: 0;
10 | padding: 0;
11 | box-sizing: border-box;
12 | }
13 |
14 | body {
15 | width: 100%;
16 | font-family: "Poppins", sans-serif;
17 | overflow-x: hidden;
18 | background-image: url("/bg-tile.png");
19 | background-repeat: repeat;
20 | }
21 |
22 | .container {
23 | width: 100%;
24 | margin: 0 auto;
25 | padding: 0 25px;
26 | @media screen and (min-width: 640px) {
27 | max-width: 640px;
28 | }
29 | @media screen and (min-width: 768px) {
30 | max-width: 768px;
31 | }
32 | @media screen and (min-width: 1024px) {
33 | max-width: 1024px;
34 | }
35 | @media screen and (min-width: 1280px) {
36 | max-width: 1280px;
37 | }
38 | }
39 |
40 | .mt-40 {
41 | margin-top: 20px;
42 | margin-bottom: 20px;
43 |
44 | @media screen and (min-width: 640px) {
45 | margin-top: 40px;
46 | margin-bottom: 40px;
47 | }
48 | }
49 |
50 | button {
51 | appearance: none;
52 | background: none;
53 | border: none;
54 | outline: none;
55 | color: inherit;
56 | font-family: inherit;
57 | cursor: pointer;
58 | }
59 |
60 | .card-wrapper {
61 | height: 315px;
62 | width: 240px;
63 | user-select: none;
64 | margin-bottom: 50px;
65 | margin-top: 75px;
66 | position: relative;
67 |
68 | @media screen and (min-width: 640px) {
69 | height: 420px;
70 | width: 320px;
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pokenext",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "format": "prettier --write **/*.{ts,tsx,scss,css,json,md}",
10 | "lint": "tsc --noEmit && eslint --fix **/*.ts?(x)"
11 | },
12 | "dependencies": {
13 | "@badrap/bar-of-progress": "^0.1.2",
14 | "@reduxjs/toolkit": "^1.5.1",
15 | "axios": "^0.21.1",
16 | "case": "^1.6.3",
17 | "framer-motion": "^4.1.4",
18 | "mongoose": "^5.12.3",
19 | "next": "^10.2.0",
20 | "next-absolute-url": "^1.2.2",
21 | "next-auth": "^3.17.2",
22 | "popmotion": "^9.3.5",
23 | "react": "17.0.2",
24 | "react-dom": "17.0.2",
25 | "react-icons": "^4.2.0",
26 | "react-redux": "^7.2.3",
27 | "react-spinners": "^0.10.6",
28 | "redux": "^4.0.5",
29 | "sass": "^1.32.8",
30 | "swr": "^0.5.6"
31 | },
32 | "devDependencies": {
33 | "@types/node": "^14.14.37",
34 | "@types/react": "^17.0.3",
35 | "@typescript-eslint/eslint-plugin": "^4.21.0",
36 | "@typescript-eslint/parser": "^4.21.0",
37 | "eslint": "^7.24.0",
38 | "eslint-config-airbnb-typescript": "^12.3.1",
39 | "eslint-config-prettier": "^8.1.0",
40 | "eslint-plugin-import": "^2.22.1",
41 | "eslint-plugin-jsx-a11y": "^6.4.1",
42 | "eslint-plugin-prettier": "^3.3.1",
43 | "eslint-plugin-react": "^7.23.2",
44 | "eslint-plugin-react-hooks": "^4.2.0",
45 | "prettier": "^2.2.1",
46 | "prettier-eslint-cli": "^5.0.1",
47 | "typescript": "^4.2.4"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/components/PokemonPage/PokemonPage.tsx:
--------------------------------------------------------------------------------
1 | import Card from "@components/Cards/Card"
2 | import { getEvolutionData, getPokemonData, getPokemonSpeciesData } from "@lib/pokemonSlice"
3 | import { useAppDispatch, useAppSelector } from "@lib/reduxHooks"
4 | import { useRouter } from "next/router"
5 | import { useEffect } from "react"
6 | import BioTrainCard from "./BioTrainCard"
7 | import EvoStatCard from "./EvoStatCard"
8 |
9 | const PokemonPage = () => {
10 | const { pokemonSpeciesData, pokemonData } = useAppSelector((state) => state.pokemon)
11 | const router = useRouter()
12 | const { pid } = router.query
13 | const dispatch = useAppDispatch()
14 |
15 | useEffect(() => {
16 | if (pid) {
17 | dispatch(getPokemonSpeciesData(+pid))
18 | dispatch(getPokemonData(+pid))
19 | }
20 | }, [pid, dispatch])
21 |
22 | useEffect(() => {
23 | if (pokemonSpeciesData) {
24 | dispatch(getEvolutionData(pokemonSpeciesData.evolution_chain.url))
25 | }
26 | }, [pokemonSpeciesData, dispatch])
27 |
28 | return (
29 |
30 |
router.back()}>
31 | Go Back
32 |
33 |
34 |
{pokemonData && }
35 |
36 | {pokemonSpeciesData && pokemonData && (
37 |
38 | )}
39 | {pokemonData && }
40 |
41 |
42 |
43 | )
44 | }
45 |
46 | export default PokemonPage
47 |
--------------------------------------------------------------------------------
/components/PokemonPage/EvoStatCard.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { PokemonDataInterface } from "@interfaces/Interfaces"
3 | import Evolution from "./Evolution"
4 |
5 | interface RightCardProps {
6 | pokemondata: PokemonDataInterface
7 | }
8 |
9 | const EvoStatCard: React.FC = ({ pokemondata }) => (
10 |
11 |
15 |
16 |
Stats
17 |
18 |
19 |
HP
20 |
{pokemondata.stats[0].base_stat}
21 |
22 |
23 |
Atk
24 |
{pokemondata.stats[1].base_stat}
25 |
26 |
27 |
Def
28 |
{pokemondata.stats[2].base_stat}
29 |
30 |
31 |
Sp. Atk
32 |
{pokemondata.stats[3].base_stat}
33 |
34 |
35 |
Sp. Def
36 |
{pokemondata.stats[4].base_stat}
37 |
38 |
39 |
Speed
40 |
{pokemondata.stats[5].base_stat}
41 |
42 |
43 |
44 |
45 | )
46 |
47 | export default EvoStatCard
48 |
--------------------------------------------------------------------------------
/lib/pokemonSlice.ts:
--------------------------------------------------------------------------------
1 | import {
2 | PokemonDataInterface,
3 | PokemonSpeciesDataInterface,
4 | NameURLInterface,
5 | } from "@interfaces/Interfaces"
6 | import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"
7 | import { fetchEvolutionData, fetchPokemonData, fetchSpeciesData } from "@helpers/getPokemon"
8 | import { extractEvolutionChain } from "@helpers/GlobalFunctions"
9 |
10 | export const getPokemonData = createAsyncThunk("pokemon/data", async (id: number) => {
11 | const data = await fetchPokemonData(id)
12 | return data
13 | })
14 |
15 | export const getPokemonSpeciesData = createAsyncThunk("pokemon/species", async (id: number) => {
16 | const data = await fetchSpeciesData(id)
17 | return data
18 | })
19 |
20 | export const getEvolutionData = createAsyncThunk("pokemon/evolution", async (link: string) => {
21 | const data = await fetchEvolutionData(link)
22 | const evolutionChain = extractEvolutionChain(data)
23 | return evolutionChain
24 | })
25 |
26 | interface InitialStateProps {
27 | pokemonData: PokemonDataInterface | null
28 | pokemonSpeciesData: PokemonSpeciesDataInterface | null
29 | evolutionData: NameURLInterface[] | null
30 | }
31 |
32 | const initialState: InitialStateProps = {
33 | pokemonData: null,
34 | pokemonSpeciesData: null,
35 | evolutionData: null,
36 | }
37 |
38 | const pokemon = createSlice({
39 | name: "pokemon",
40 | initialState,
41 | reducers: {},
42 | extraReducers: (builder) => {
43 | builder.addCase(getPokemonSpeciesData.fulfilled, (state, { payload }) => {
44 | state.pokemonSpeciesData = payload
45 | })
46 | builder.addCase(getEvolutionData.fulfilled, (state, { payload }) => {
47 | state.evolutionData = payload
48 | })
49 | builder.addCase(getPokemonData.fulfilled, (state, { payload }) => {
50 | state.pokemonData = payload
51 | })
52 | },
53 | })
54 |
55 | export default pokemon.reducer
56 |
--------------------------------------------------------------------------------
/helpers/getTypeIconsAndColor.ts:
--------------------------------------------------------------------------------
1 | const normal = "/typeIcons/normal.png"
2 | const fire = "/typeIcons/fire.png"
3 | const fighting = "/typeIcons/fighting.png"
4 | const water = "/typeIcons/water.png"
5 | const flying = "/typeIcons/flying.png"
6 | const grass = "/typeIcons/grass.png"
7 | const poison = "/typeIcons/poison.png"
8 | const electric = "/typeIcons/electric.png"
9 | const ground = "/typeIcons/ground.png"
10 | const psychic = "/typeIcons/psychic.png"
11 | const rock = "/typeIcons/rock.png"
12 | const ice = "/typeIcons/ice.png"
13 | const bug = "/typeIcons/bug.png"
14 | const dragon = "/typeIcons/dragon.png"
15 | const ghost = "/typeIcons/ghost.png"
16 | const dark = "/typeIcons/dark.png"
17 | const steel = "/typeIcons/steel.png"
18 | const fairy = "/typeIcons/fairy.png"
19 |
20 | export const getTypeIcon = (type: string) => {
21 | const colors = {
22 | normal,
23 | fire,
24 | fighting,
25 | water,
26 | flying,
27 | grass,
28 | poison,
29 | electric,
30 | ground,
31 | psychic,
32 | rock,
33 | ice,
34 | bug,
35 | dragon,
36 | ghost,
37 | dark,
38 | steel,
39 | fairy,
40 | }
41 | const getIcon = Object.entries(colors).filter(([key]) => key === type)
42 | return getIcon[0]
43 | }
44 |
45 | export const findColor = (color: string) => {
46 | const colors = {
47 | normal: "#C4C4A4",
48 | fire: "#F08030",
49 | fighting: "#C03028",
50 | water: "#6890F0",
51 | flying: "#A890F0",
52 | grass: "#78C850",
53 | poison: "#A040A0",
54 | electric: "#F8D030",
55 | ground: "#E0C068",
56 | psychic: "#F85888",
57 | rock: "#B8A038",
58 | ice: "#98D8D8",
59 | bug: "#A8B820",
60 | dragon: "#7038F8",
61 | ghost: "#705898",
62 | dark: "#705848",
63 | steel: "#B8B8D0",
64 | fairy: "#EE99AC",
65 | }
66 | const getColor = Object.entries(colors).filter(([key]) => key === color)
67 | return getColor[0]
68 | }
69 |
--------------------------------------------------------------------------------
/components/PokemonPage/_pokemon-page.scss:
--------------------------------------------------------------------------------
1 | .flex-wrapper {
2 | width: 100%;
3 | display: grid;
4 | grid-template-columns: 1fr 1fr;
5 | }
6 |
7 | .evolution-wrapper {
8 | display: flex;
9 | align-items: center;
10 | justify-content: space-evenly;
11 |
12 | .evolution-card {
13 | display: flex;
14 | flex-direction: column;
15 | align-items: center;
16 |
17 | .evolution-img {
18 | display: flex;
19 | align-items: center;
20 | justify-content: center;
21 | border-radius: 50%;
22 | height: 100px;
23 | width: 100px;
24 | cursor: pointer;
25 | }
26 | }
27 | }
28 |
29 | .cards-container {
30 | margin-top: 40px;
31 | display: grid;
32 | grid-template-rows: 420px 1fr;
33 | grid-template-columns: unset;
34 | grid-gap: 40px;
35 | align-items: center;
36 | justify-items: center;
37 | height: max-content;
38 | font-size: small;
39 | width: 100%;
40 |
41 | .details-wrapper {
42 | width: 100%;
43 | }
44 |
45 | @media screen and (min-width: 768px) {
46 | grid-template-rows: unset;
47 | grid-template-columns: 320px 1fr;
48 | }
49 |
50 | .details {
51 | margin: 20px 0;
52 | height: max-content;
53 | padding: 0 20px;
54 | }
55 | .card-wrapper {
56 | margin: 0;
57 | }
58 | .biotrain-content {
59 | display: flex;
60 | flex-direction: column;
61 |
62 | @media screen and (min-width: 768px) {
63 | display: grid;
64 | grid-template-columns: 1fr 1fr;
65 | grid-gap: 20px;
66 | }
67 |
68 | .flavor {
69 | word-wrap: break-word;
70 | margin-bottom: 10px;
71 | }
72 | }
73 | .stats-container {
74 | display: grid;
75 | grid-template-columns: repeat(6, 1fr);
76 |
77 | .stats {
78 | display: flex;
79 | align-items: center;
80 | justify-content: center;
81 | flex-direction: column;
82 | }
83 | }
84 | }
85 |
86 | .title {
87 | font-weight: bold;
88 | font-size: medium;
89 | }
90 |
--------------------------------------------------------------------------------
/helpers/getPokemon.ts:
--------------------------------------------------------------------------------
1 | import { NameURLInterface, PokemonTypes } from "@interfaces/Interfaces"
2 | import axios from "axios"
3 | import { getIDfromURL, getPokemonImage } from "./GlobalFunctions"
4 |
5 | const headURL = "https://pokeapi.co/"
6 | const limit = process.env.NODE_ENV === "production" ? 807 : 8
7 | export const fetchExploreList = async () => {
8 | const { data: pokemonlist } = await axios.get(`${headURL}api/v2/pokemon/?limit=${limit}&offset=0`)
9 | const list = Promise.all(
10 | pokemonlist.results.map(async (pokemon: NameURLInterface) => {
11 | const { data } = await axios.get(pokemon.url)
12 | return {
13 | id: data.id,
14 | name: data.species.name,
15 | types: data.types.map((type: PokemonTypes) => type.type.name),
16 | sprite: getPokemonImage(data.id),
17 | }
18 | })
19 | )
20 | return list
21 | }
22 |
23 | export const fetchPlayList = async () => {
24 | const { data } = await axios.get(`${headURL}api/v2/pokemon/?limit=${limit}&offset=0`)
25 | const list = data.results.map(({ name, url }: NameURLInterface) => ({
26 | name,
27 | id: getIDfromURL(url),
28 | }))
29 | const pokemonlist = list
30 | return pokemonlist
31 | }
32 |
33 | export const fetchPokemonData = async (id: number) => {
34 | const { data } = await axios.get(`${headURL}api/v2/pokemon/${id}`)
35 | return {
36 | abilities: data.abilities,
37 | base_experience: data.base_experience,
38 | height: data.height,
39 | id: data.id,
40 | name: data.species.name,
41 | species: data.species,
42 | types: data.types.map((type: PokemonTypes) => type.type.name),
43 | weight: data.weight,
44 | stats: data.stats,
45 | sprite: getPokemonImage(data.id),
46 | }
47 | }
48 |
49 | export const fetchEvolutionData = async (link: string) => {
50 | const { data } = await axios.get(link)
51 | return data
52 | }
53 | export const fetchSpeciesData = async (id: number) => {
54 | const { data } = await axios.get(`${headURL}api/v2/pokemon-species/${id}/`)
55 | return data
56 | }
57 |
--------------------------------------------------------------------------------
/components/Game/_game.scss:
--------------------------------------------------------------------------------
1 | .modal {
2 | position: fixed;
3 | top: 0;
4 | left: 0;
5 | height: 100%;
6 | width: 100%;
7 | background: #00000080;
8 | display: flex;
9 | align-items: center;
10 | justify-content: center;
11 |
12 | .wrapper {
13 | padding: 50px;
14 | display: flex;
15 | align-items: center;
16 | justify-content: center;
17 | flex-direction: column;
18 | position: relative;
19 | background: #fff;
20 | border-radius: 20px;
21 |
22 | span {
23 | margin-top: 20px;
24 | }
25 |
26 | .close-modal {
27 | position: absolute;
28 | top: 10px;
29 | right: 10px;
30 | cursor: pointer;
31 | }
32 | }
33 | }
34 |
35 | .play-wrapper {
36 | h1 {
37 | text-align: center;
38 | line-height: 1;
39 | font-size: 1rem;
40 | @media screen and (min-width: 640px) {
41 | font-size: 2rem;
42 | }
43 | }
44 |
45 | .dash {
46 | margin-top: 20px;
47 | display: flex;
48 | align-items: center;
49 | justify-content: space-between;
50 | width: 100%;
51 |
52 | @media screen and (min-width: 640px) {
53 | width: 320px;
54 | }
55 |
56 | span {
57 | margin-left: 5px;
58 | color: #ef2b2b;
59 | }
60 | }
61 |
62 | .options-wrapper {
63 | display: grid;
64 | grid-template-columns: 1fr 1fr;
65 | grid-template-rows: 1fr 1fr;
66 | grid-gap: 5px;
67 | align-items: center;
68 | justify-items: center;
69 |
70 | @media screen and (min-width: 640px) {
71 | display: flex;
72 | align-items: center;
73 | justify-content: center;
74 | flex-wrap: wrap;
75 | }
76 |
77 | .option {
78 | padding: 5px 10px;
79 | border-radius: 999px;
80 | width: 100%;
81 | text-align: center;
82 | p {
83 | font-size: small;
84 | }
85 | @media screen and (min-width: 640px) {
86 | padding: 10px 20px;
87 | margin: 10px;
88 | width: auto;
89 | p {
90 | font-size: medium;
91 | }
92 | }
93 | }
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/components/Profile/Favorites.tsx:
--------------------------------------------------------------------------------
1 | import Card from "@components/Cards/Card"
2 | import FilterByType from "@components/InputComponents/FilterByType"
3 | import Search from "@components/InputComponents/Search"
4 | import Sort from "@components/InputComponents/Sort"
5 | import { UserFavoritesProps } from "@interfaces/Interfaces"
6 | import useRefineItems from "@lib/useRefineItems"
7 | import React from "react"
8 |
9 | interface FavoritesProps {
10 | favorites: UserFavoritesProps[]
11 | }
12 |
13 | const Favorites: React.FC = ({ favorites }) => {
14 | const {
15 | refinedItems,
16 | requestSort,
17 | requestSearch,
18 | requestFilter,
19 | sortKey,
20 | search,
21 | filterByType,
22 | } = useRefineItems(favorites)
23 |
24 | const handleSearch = (event: React.ChangeEvent) => {
25 | const text = event.target.value
26 | requestSearch(text)
27 | }
28 |
29 | const handleFilterByType = (event: React.ChangeEvent) => {
30 | const text = event.target.value
31 | requestFilter(text)
32 | }
33 |
34 | const handleSort = (event: React.ChangeEvent) => {
35 | const text = event.target.value
36 | if (text === "id") requestSort("id")
37 | else if (text === "name") requestSort("name")
38 | else requestSort(null)
39 | }
40 |
41 | return (
42 |
43 |
Favorites
44 |
45 |
46 |
47 |
52 |
53 |
54 | {refinedItems.map((favorite) => (
55 |
56 |
57 |
58 | ))}
59 |
60 |
61 | )
62 | }
63 |
64 | export default Favorites
65 |
--------------------------------------------------------------------------------
/lib/useRefineItems.ts:
--------------------------------------------------------------------------------
1 | import { shuffle } from "@helpers/GlobalFunctions"
2 | import { UserFavoritesProps } from "@interfaces/Interfaces"
3 | import { useMemo, useState } from "react"
4 |
5 | export type SortKey = "id" | "name" | null
6 |
7 | const useRefineItems = (items: UserFavoritesProps[]) => {
8 | const [sortKey, setSortKey] = useState("id")
9 | const [search, setSearch] = useState("")
10 | const [filterByType, setFilterByType] = useState("")
11 |
12 | const refinedItems = useMemo(() => {
13 | const itemsCopy = [...items]
14 | let refinableItems = [...items]
15 |
16 | if (sortKey) {
17 | refinableItems = itemsCopy.sort((a, b) => {
18 | if (a[sortKey] < b[sortKey]) {
19 | return -1
20 | }
21 | if (a[sortKey] > b[sortKey]) {
22 | return 1
23 | }
24 | return 0
25 | })
26 | }
27 |
28 | if (!sortKey) {
29 | refinableItems = shuffle(itemsCopy)
30 | }
31 |
32 | if (search || filterByType) {
33 | refinableItems = itemsCopy.filter((item) => {
34 | if (
35 | search &&
36 | filterByType &&
37 | item.name.includes(search) &&
38 | item.types.includes(filterByType)
39 | ) {
40 | return item
41 | }
42 | if (search && !filterByType && item.name.includes(search)) {
43 | return item
44 | }
45 | if (!search && filterByType && item.types.includes(filterByType)) {
46 | return item
47 | }
48 | return null
49 | })
50 | }
51 |
52 | return refinableItems
53 | }, [items, sortKey, search, filterByType])
54 |
55 | const requestSort = (key: SortKey) => {
56 | setSortKey(key)
57 | }
58 |
59 | const requestSearch = (name: string) => {
60 | setSearch(name.toLowerCase())
61 | }
62 | const requestFilter = (type: string) => {
63 | setFilterByType(type)
64 | }
65 |
66 | return {
67 | refinedItems,
68 | requestSort,
69 | requestSearch,
70 | requestFilter,
71 | sortKey,
72 | search,
73 | filterByType,
74 | }
75 | }
76 |
77 | export default useRefineItems
78 |
--------------------------------------------------------------------------------
/components/Cards/FramerCard.tsx:
--------------------------------------------------------------------------------
1 | import { motion, PanInfo, useMotionValue, useTransform } from "framer-motion"
2 | import React from "react"
3 |
4 | interface FramerCardProps {
5 | initial?: {
6 | scale: number
7 | y: number
8 | opacity: number
9 | }
10 | animate?: {
11 | scale: number
12 | y: number
13 | opacity: number
14 | }
15 | transition?: {
16 | scale?: { duration: number }
17 | opacity?: { duration: number }
18 | type?: "inertia" | "spring" | "tween"
19 | stiffness?: number
20 | damping?: number
21 | }
22 | drag?: boolean | "x" | "y"
23 | exitX?: number
24 | setExitX?: (x: number) => void
25 | index: number
26 | setIndex?: (x: number) => void
27 | handleDragEnd?: (_event: MouseEvent | TouchEvent | PointerEvent, info: PanInfo) => void
28 | whileHover?: {
29 | scale?: number
30 | cursor?: string
31 | }
32 | whileTap?: {
33 | cursor?: string
34 | scale?: number
35 | }
36 | }
37 |
38 | const FramerCard: React.FC = (props) => {
39 | const maximumX = 200
40 | const x = useMotionValue(0)
41 | const rotate = useTransform(x, [-maximumX, 0, maximumX], [-10, 0, 10], {
42 | clamp: false,
43 | })
44 |
45 | const {
46 | initial,
47 | animate,
48 | transition,
49 | exitX,
50 | drag,
51 | whileHover,
52 | whileTap,
53 | children,
54 | handleDragEnd,
55 | } = props
56 |
57 | return (
58 |
87 | {children}
88 |
89 | )
90 | }
91 |
92 | export default FramerCard
93 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | node: true,
5 | es6: true,
6 | },
7 | parserOptions: { ecmaVersion: 8 }, // to enable features such as async/await
8 | ignorePatterns: ['node_modules/*', '.next/*', '.vercel/*', '.out/*', '!.prettierrc.json'], // We don't want to lint generated files nor node_modules, but we want to lint .prettierrc.js (ignored by default by eslint)
9 | extends: ['eslint:recommended', 'plugin:prettier/recommended'],
10 | overrides: [
11 | // This configuration will apply only to TypeScript files
12 | {
13 | files: ['**/*.ts', '**/*.tsx'],
14 | parser: '@typescript-eslint/parser',
15 | parserOptions: {
16 | project: "./tsconfig.json"
17 | },
18 | settings: { react: { version: 'detect' } },
19 | env: {
20 | browser: true,
21 | node: true,
22 | es6: true,
23 | },
24 | plugins: ["prettier", "@typescript-eslint"],
25 | extends: [
26 | 'eslint:recommended',
27 | 'plugin:@typescript-eslint/recommended', // TypeScript rules
28 | 'plugin:react/recommended', // React rules
29 | 'plugin:react-hooks/recommended', // React hooks rules
30 | 'plugin:jsx-a11y/recommended', // Accessibility rules
31 | "airbnb-typescript", // Airbnb rules
32 | 'plugin:prettier/recommended'
33 | ],
34 | rules: {
35 | 'prettier/prettier': ['error', {}, { usePrettierrc: true }],
36 | 'react/prop-types': 'off',
37 | 'react/react-in-jsx-scope': 'off',
38 | 'react/jsx-props-no-spreading': 'off',
39 | 'jsx-a11y/anchor-is-valid': 'off',
40 | '@typescript-eslint/no-unused-vars': ["error"],
41 | "@typescript-eslint/explicit-function-return-type": "off",
42 | "@typescript-eslint/explicit-module-boundary-types": "off",
43 | "no-param-reassign": ["error", { "props": false }],
44 | 'import/no-cycle': ['error', { maxDepth: '∞' }],
45 | "jsx-a11y/label-has-associated-control": "off",
46 | "@typescript-eslint/no-explicit-any": "off",
47 | "@typescript-eslint/no-unused-vars": "off",
48 | "no-underscore-dangle": "off",
49 | "react/no-array-index-key": "off",
50 | },
51 | },
52 | ],
53 | }
54 |
--------------------------------------------------------------------------------
/components/PokemonPage/Evolution.tsx:
--------------------------------------------------------------------------------
1 | import { getIDfromURL, getPokemonImage } from "@helpers/GlobalFunctions"
2 | import { findColor } from "@helpers/getTypeIconsAndColor"
3 | import { useAppSelector } from "@lib/reduxHooks"
4 | import { motion } from "framer-motion"
5 | import React from "react"
6 | import { useRouter } from "next/router"
7 | import Image from "next/image"
8 |
9 | const Evolution = () => {
10 | const { evolutionData, pokemonData } = useAppSelector((state) => state.pokemon)
11 | const router = useRouter()
12 | return (
13 |
26 | {evolutionData &&
27 | pokemonData &&
28 | evolutionData.map(({ name, url }) => (
29 |
30 |
#{getIDfromURL(url)}
31 |
router.push(`/pokemon/${+getIDfromURL(url)}`)}
34 | style={{
35 | background: `linear-gradient(0deg, rgb(255,255,255) 0%, ${
36 | findColor(pokemonData.types[0])[1]
37 | } 80%)`,
38 | }}
39 | initial={{
40 | scale: 0,
41 | y: 0,
42 | opacity: 0,
43 | }}
44 | animate={{
45 | scale: 1,
46 | y: 0,
47 | opacity: 1,
48 | }}
49 | whileHover={{
50 | scale: 1.05,
51 | boxShadow: "0 0px 15px 0px rgba(0,0,0,.25)",
52 | }}
53 | whileTap={{
54 | scale: 1,
55 | boxShadow: "0 0px 0px 0px rgba(0,0,0,.25)",
56 | }}
57 | >
58 |
66 |
67 |
{name}
68 |
69 | ))}
70 |
71 | )
72 | }
73 |
74 | export default Evolution
75 |
--------------------------------------------------------------------------------
/components/Nav/_navbar.scss:
--------------------------------------------------------------------------------
1 | .navbar {
2 | display: none;
3 |
4 | @media screen and (min-width: 640px) {
5 | display: flex;
6 | align-items: center;
7 | justify-content: space-between;
8 | }
9 |
10 | .active {
11 | color: $color-accent-light;
12 | }
13 |
14 | span {
15 | font-size: 0.825rem;
16 | cursor: pointer;
17 | &:hover {
18 | color: $color-accent-light;
19 | }
20 | }
21 |
22 | .title-wrapper {
23 | display: flex;
24 | align-items: center;
25 |
26 | h2 {
27 | transition: all 0.15s ease-in-out;
28 | line-height: 1;
29 | margin-right: 20px;
30 | cursor: pointer;
31 | &:hover {
32 | color: $color-accent-light;
33 | }
34 | }
35 | }
36 |
37 | .web-menu {
38 | display: flex;
39 | align-items: center;
40 | .nav-items {
41 | margin-left: 20px;
42 | }
43 |
44 | .dropdown-wrapper {
45 | margin-left: 10px;
46 | border-left: 1px solid #fff;
47 | padding-left: 10px;
48 | .account-button {
49 | position: relative;
50 | svg {
51 | transform: rotate(0deg);
52 | transition: all 0.2s ease-in-out;
53 | }
54 | &:hover,
55 | &:focus,
56 | &:focus-within {
57 | .dropdown {
58 | opacity: 1;
59 | height: 30px;
60 | }
61 |
62 | svg {
63 | transform: rotate(180deg);
64 | }
65 | }
66 | .dropdown {
67 | display: flex;
68 | align-items: center;
69 | padding: 5px 15px;
70 | border-radius: 10px;
71 | background: #000;
72 | position: absolute;
73 | top: 100%;
74 | left: 0;
75 | z-index: 2;
76 | height: 0px;
77 | transition: all 0.2s ease-in-out;
78 | opacity: 0;
79 | box-shadow: 0 0 10px 5px #00000050;
80 | }
81 | }
82 | }
83 | }
84 | }
85 |
86 | .navbar-mobile {
87 | display: flex;
88 | align-items: center;
89 | justify-content: space-between;
90 | @media screen and (min-width: 640px) {
91 | display: none;
92 | }
93 |
94 | span {
95 | font-size: 1.5rem;
96 | }
97 | }
98 |
99 | header {
100 | width: 100%;
101 | padding: 10px 0;
102 | background-color: #000;
103 | color: #fff;
104 | box-shadow: 0 0 10px 5px #00000050;
105 | }
106 |
--------------------------------------------------------------------------------
/helpers/GlobalFunctions.ts:
--------------------------------------------------------------------------------
1 | import {
2 | NameIDInterface,
3 | PokemonDataInterface,
4 | PokemonEvolutionChainInterface,
5 | PokemonSpeciesDataInterface,
6 | } from "@interfaces/Interfaces"
7 |
8 | export const shuffle = (array: any[]) => {
9 | let counter = array.length
10 |
11 | while (counter > 0) {
12 | const index = Math.floor(Math.random() * counter)
13 | counter -= 1
14 | const temp = array[counter]
15 | array[counter] = array[index]
16 | array[index] = temp
17 | }
18 |
19 | return array
20 | }
21 |
22 | export const getIDfromURL = (url: string) => {
23 | const tempURL = url.split("/")
24 | const id = +tempURL[tempURL.length - 2]
25 | if (id >= 10 && id < 100) return `0${id}`
26 | if (id >= 100) return `${id}`
27 | return `00${id}`
28 | }
29 |
30 | export const getStringIDfromID = (id: number) => {
31 | if (id >= 10 && id < 100) return `0${id}`
32 | if (id >= 100) return `${id}`
33 | return `00${id}`
34 | }
35 |
36 | export const getOptions = (pokemons: NameIDInterface[], correctAnswer: string) => {
37 | const nums = new Set([correctAnswer])
38 | while (nums.size < 4) {
39 | const random = Math.floor(Math.random() * pokemons.length)
40 | const pokemon = pokemons[random].name
41 | nums.add(pokemon)
42 | }
43 | return [...nums]
44 | }
45 |
46 | export const getPokemonImage = (id: number) => {
47 | return `https://raw.githubusercontent.com/HybridShivam/Pokemon/master/assets/images/${getStringIDfromID(
48 | id
49 | )}.png`
50 | }
51 |
52 | export const getFlavorSpeech = (
53 | pokemonSpeciesData: PokemonSpeciesDataInterface,
54 | pokemonData: PokemonDataInterface
55 | ) => {
56 | const enLang = pokemonSpeciesData.flavor_text_entries.filter(
57 | (entry) => entry.language.name === "en"
58 | )[0]
59 | const types = pokemonData.types.join(" and ")
60 | const legend = pokemonSpeciesData.is_legendary ? " legendary, " : ""
61 | const mythic = pokemonSpeciesData.is_mythical ? " mythical, " : ""
62 | const text = `${
63 | pokemonData.name
64 | }, ${legend}${mythic}${types} type pokemon. ${enLang.flavor_text.replace(/\r?\n|\r/g, " ")}`
65 | return text
66 | }
67 |
68 | export const extractEvolutionChain = (response: PokemonEvolutionChainInterface) => {
69 | const evoChain = []
70 | let evoData = response.chain
71 |
72 | do {
73 | evoChain.push({
74 | name: evoData.species.name,
75 | url: evoData.species.url,
76 | })
77 | ;[evoData] = evoData.evolves_to
78 | } while (evoData && Object.prototype.hasOwnProperty.call(evoData, "evolves_to"))
79 |
80 | return evoChain
81 | }
82 |
--------------------------------------------------------------------------------
/lib/userSlice.ts:
--------------------------------------------------------------------------------
1 | import { getUserSignIn, updateUserScore, updateUserFavorites } from "@helpers/getUsers"
2 | import { UserFavoritesProps, UserProps, UserSessionProps } from "@interfaces/Interfaces"
3 | import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit"
4 |
5 | export const userSignIn = createAsyncThunk("user/signin", async (data: UserSessionProps) => {
6 | const { user } = await getUserSignIn(data)
7 | return user
8 | })
9 |
10 | interface InitialStateProps {
11 | userData: UserProps | null
12 | status: "loading" | "success" | "failed" | null
13 | }
14 |
15 | const initialState: InitialStateProps = {
16 | userData: null,
17 | status: null,
18 | }
19 |
20 | const userSlice = createSlice({
21 | name: "user",
22 | initialState,
23 | reducers: {
24 | updateFavorites: (state, action: PayloadAction) => {
25 | const { payload } = action
26 | if (state.userData) {
27 | const favorites = state.userData.favorites.map((fav) => ({
28 | id: fav.id,
29 | name: fav.name,
30 | types: fav.types,
31 | sprite: fav.sprite,
32 | }))
33 | if (favorites.filter((fav) => fav.id === payload.id).length > 0) {
34 | favorites.splice(
35 | favorites.findIndex((fav) => fav.id === payload.id),
36 | 1
37 | )
38 | updateUserFavorites(state.userData._id, "favorites", favorites)
39 | state.userData.favorites = favorites
40 | } else {
41 | favorites.push(payload)
42 | updateUserFavorites(state.userData._id, "favorites", favorites)
43 | state.userData.favorites = favorites
44 | }
45 | }
46 | },
47 | updateScore: (state, action: PayloadAction) => {
48 | const score = action.payload
49 | if (state.userData) {
50 | if (state.userData.score < score) {
51 | updateUserScore(state.userData._id, "score", score)
52 | state.userData.score = score
53 | }
54 | }
55 | },
56 | signout: (state) => {
57 | state.userData = null
58 | state.status = null
59 | },
60 | },
61 | extraReducers: (builder) => {
62 | builder.addCase(userSignIn.pending, (state) => {
63 | state.status = "loading"
64 | })
65 | builder.addCase(userSignIn.rejected, (state) => {
66 | state.status = "failed"
67 | })
68 | builder.addCase(userSignIn.fulfilled, (state, { payload }) => {
69 | state.userData = payload
70 | state.status = "success"
71 | })
72 | },
73 | })
74 |
75 | export const { signout, updateScore, updateFavorites } = userSlice.actions
76 | export default userSlice.reducer
77 |
--------------------------------------------------------------------------------
/components/Explore/ExplorePage.tsx:
--------------------------------------------------------------------------------
1 | import Card from "@components/Cards/Card"
2 | import Deck from "@components/Cards/Deck"
3 | import useRefineItems from "@lib/useRefineItems"
4 | // import { useAppDispatch, useAppSelector } from "@lib/reduxHooks"
5 | import { UserFavoritesProps } from "interfaces/Interfaces"
6 | import { wrap } from "popmotion"
7 | import { useState } from "react"
8 | // import { refineList, setSortKey, setSearch, setfilterByType } from "@lib/exploreSlice"
9 | import FilterByType from "../InputComponents/FilterByType"
10 | import Search from "../InputComponents/Search"
11 | import Sort from "../InputComponents/Sort"
12 | import UndoButton from "./UndoButton"
13 |
14 | interface ExploreProps {
15 | pokemonList: UserFavoritesProps[]
16 | }
17 |
18 | const ExplorePage: React.FC = ({ pokemonList }) => {
19 | const {
20 | refinedItems,
21 | requestSort,
22 | requestSearch,
23 | requestFilter,
24 | sortKey,
25 | search,
26 | filterByType,
27 | } = useRefineItems(pokemonList)
28 | const [index, setIndex] = useState(0)
29 | const [exitX, setExitX] = useState(0)
30 | const cardIndex = wrap(0, refinedItems.length + 1, index)
31 |
32 | const handleSearch = (event: React.ChangeEvent) => {
33 | const text = event.target.value
34 | requestSearch(text)
35 | setIndex(0)
36 | }
37 |
38 | const handleFilterByType = (event: React.ChangeEvent) => {
39 | const text = event.target.value
40 | requestFilter(text)
41 | setIndex(0)
42 | }
43 |
44 | const handleSort = (event: React.ChangeEvent) => {
45 | const text = event.target.value
46 | setIndex(0)
47 | if (text === "id") requestSort("id")
48 | else if (text === "name") requestSort("name")
49 | else requestSort(null)
50 | }
51 |
52 | const handleUndo = () => {
53 | if (index > 0) setIndex(index - 1)
54 | }
55 |
56 | return (
57 |
58 |
59 |
60 |
61 |
66 |
67 | {refinedItems && (
68 |
78 | )}
79 |
80 |
81 | )
82 | }
83 |
84 | export default ExplorePage
85 |
--------------------------------------------------------------------------------
/components/Profile/_profile-page.scss:
--------------------------------------------------------------------------------
1 | .delete-button {
2 | padding: 5px 10px;
3 | border-radius: 5px;
4 | cursor: pointer;
5 | border: 1px solid red;
6 | color: red;
7 |
8 | &:hover {
9 | background: red;
10 | color: #fff;
11 | }
12 | }
13 |
14 | .buttons-wrapper {
15 | display: flex;
16 | align-items: center;
17 |
18 | .delete-button,
19 | .black-button {
20 | margin: 0 5px;
21 | margin-top: 10px;
22 | }
23 | }
24 |
25 | .black-button {
26 | padding: 5px 10px;
27 | border-radius: 5px;
28 | cursor: pointer;
29 | border: 1px solid black;
30 | color: black;
31 |
32 | &:hover {
33 | background: black;
34 | color: #fff;
35 | }
36 | }
37 |
38 | .favorites-wrapper {
39 | display: flex;
40 | align-items: center;
41 | justify-content: center;
42 | flex-wrap: wrap;
43 | margin-top: 20px;
44 | .card-wrapper {
45 | margin: 5px;
46 | height: 315px;
47 | width: 240px;
48 | .name {
49 | font-size: medium;
50 | }
51 | .card-container {
52 | box-shadow: 0 5px 10px 0px rgba(0, 0, 0, 0.5);
53 | }
54 | }
55 | }
56 |
57 | .profile-wrapper {
58 | display: grid;
59 | grid-template-rows: 150px 1fr;
60 | grid-template-columns: unset;
61 | grid-gap: 20px;
62 | width: 100%;
63 | height: 100%;
64 |
65 | @media screen and (min-width: 640px) {
66 | grid-template-columns: 150px 1fr;
67 | grid-template-rows: unset;
68 | }
69 |
70 | .profile {
71 | display: flex;
72 | flex-direction: row;
73 | align-items: center;
74 | justify-content: space-between;
75 | @media screen and (min-width: 640px) {
76 | flex-direction: column;
77 | justify-content: flex-start;
78 | }
79 | .profile-card {
80 | display: flex;
81 | flex-direction: column;
82 | align-items: center;
83 | .name {
84 | font-size: large;
85 | }
86 | .score {
87 | font-size: small;
88 | }
89 |
90 | .avatar {
91 | border-radius: 50%;
92 | border: 2px solid #000;
93 | width: 100px;
94 | height: 100px;
95 |
96 | img {
97 | border-radius: 50%;
98 | }
99 | }
100 | }
101 |
102 | .button-wrapper {
103 | display: flex;
104 | flex-direction: column;
105 | align-items: center;
106 | margin-top: 20px;
107 | .delete-button,
108 | .black-button {
109 | margin-top: 10px;
110 | padding: 2.5px 5px;
111 | @media screen and (min-width: 640px) {
112 | padding: 5px 10px;
113 | }
114 | }
115 | }
116 | }
117 |
118 | .favorites {
119 | width: 100%;
120 | }
121 |
122 | .profile,
123 | .favorites {
124 | padding: 0 10px;
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/components/LandingPage/LandingPage.tsx:
--------------------------------------------------------------------------------
1 | import Card from "@components/Cards/Card"
2 | import Deck from "@components/Cards/Deck"
3 | import { signIn } from "next-auth/client"
4 | import { useRouter } from "next/router"
5 | import { wrap } from "popmotion"
6 | import React, { useEffect, useState } from "react"
7 | import { FaGithub } from "react-icons/fa"
8 |
9 | export const landingPokemons = [
10 | {
11 | id: 1,
12 | name: "bulbasaur",
13 | types: ["grass", "poison"],
14 | sprite: "https://raw.githubusercontent.com/HybridShivam/Pokemon/master/assets/images/001.png",
15 | },
16 | {
17 | id: 4,
18 | name: "charmander",
19 | types: ["fire"],
20 | sprite: "https://raw.githubusercontent.com/HybridShivam/Pokemon/master/assets/images/004.png",
21 | },
22 | {
23 | id: 7,
24 | name: "squirtle",
25 | types: ["water"],
26 | sprite: "https://raw.githubusercontent.com/HybridShivam/Pokemon/master/assets/images/007.png",
27 | },
28 | ]
29 |
30 | let timer: ReturnType = setTimeout(() => "", 1000)
31 |
32 | const LandingPage = () => {
33 | const router = useRouter()
34 | const [index, setIndex] = useState(0)
35 | const [exitX, setExitX] = useState(1000)
36 | const cardIndex = wrap(0, landingPokemons.length + 1, index)
37 |
38 | useEffect(() => {
39 | timer = setTimeout(() => {
40 | setIndex(index + 1)
41 | setExitX(Math.random() < 0.5 ? 1000 : -1000)
42 | }, 3000)
43 |
44 | return () => {
45 | clearTimeout(timer)
46 | }
47 | }, [index])
48 |
49 | return (
50 |
51 |
52 |
Pokédex app in card style
53 |
Swipe through pokemon cards or guess who's that pokemon?
54 |
55 | Record your score and collect your favorite pokemons by{" "}
56 |
57 | {" "}
58 | signIn("github")}>
59 | signing in with
60 |
61 | {" "}
62 |
63 |
64 |
65 | router.push("/explore")}>
66 | Explore
67 |
68 | router.push("/play")}>
69 | Play
70 |
71 |
72 |
73 |
74 |
84 |
85 |
86 | )
87 | }
88 |
89 | export default LandingPage
90 |
--------------------------------------------------------------------------------
/interfaces/Interfaces.ts:
--------------------------------------------------------------------------------
1 | export interface UserFavoritesProps {
2 | id: number
3 | name: string
4 | types: string[]
5 | sprite: string
6 | _id?: string
7 | }
8 |
9 | export interface UserProps {
10 | favorites: UserFavoritesProps[]
11 | _id: string
12 | uid: string
13 | name: string
14 | picture: string
15 | score: number
16 | __v: number
17 | }
18 |
19 | export interface NameURLInterface {
20 | name: string
21 | url: string
22 | }
23 | export interface NameIDInterface {
24 | name: string
25 | id: string
26 | }
27 |
28 | interface PokemonAbilities {
29 | ability: NameURLInterface
30 | is_hidden: boolean
31 | }
32 | export interface PokemonTypes {
33 | slot: number
34 | type: NameURLInterface
35 | }
36 | interface PokemonStats {
37 | base_stat: number
38 | effort: number
39 | stat: NameURLInterface
40 | }
41 |
42 | export interface PokemonDataInterface {
43 | abilities: PokemonAbilities[]
44 | base_experience: number
45 | height: number
46 | id: number
47 | name: string
48 | species: NameURLInterface
49 | types: string[]
50 | weight: number
51 | stats: PokemonStats[]
52 | sprite: string
53 | }
54 |
55 | interface FlavorTextEntry {
56 | flavor_text: string
57 | language: { name: string; url: string }
58 | }
59 |
60 | interface Genera {
61 | genus: string
62 | language: {
63 | name: string
64 | url: string
65 | }
66 | }
67 |
68 | export interface PokemonSpeciesDataInterface {
69 | base_happiness: number
70 | capture_rate: number
71 | evolution_chain: { url: string }
72 | flavor_text_entries: FlavorTextEntry[]
73 | gender_rate: number
74 | generation: { name: string; url: string }
75 | growth_rate: { name: string; url: string }
76 | is_legendary: boolean
77 | is_mythical: boolean
78 | shape: { name: string; url: string }
79 | genera: Genera[]
80 | }
81 |
82 | interface Chain {
83 | is_baby: boolean
84 | species: NameURLInterface
85 | evolution_details: {
86 | item: NameURLInterface
87 | trigger: NameURLInterface
88 | gender: number
89 | held_item: NameURLInterface
90 | known_move: NameURLInterface
91 | known_move_type: NameURLInterface
92 | location: NameURLInterface
93 | min_level: NameURLInterface
94 | min_happiness: NameURLInterface
95 | min_beauty: NameURLInterface
96 | min_affection: NameURLInterface
97 | needs_overworldRain: boolean
98 | party_species: NameURLInterface
99 | party_type: NameURLInterface
100 | relative_physicalStats: number
101 | time_of_day: string
102 | trade_species: NameURLInterface
103 | turn_upside_down: boolean
104 | }[]
105 | evolves_to: Chain[]
106 | }
107 |
108 | export interface PokemonEvolutionChainInterface {
109 | baby_trigger_item: null
110 | chain: Chain
111 | }
112 |
113 | export interface UserSessionProps {
114 | name?: string | null
115 | picture?: string | null
116 | uid?: string | null
117 | }
118 |
--------------------------------------------------------------------------------
/components/Profile/ProfileComponent.tsx:
--------------------------------------------------------------------------------
1 | import { deleteUser } from "@helpers/getUsers"
2 | import { UserProps } from "@interfaces/Interfaces"
3 | import { signOut } from "next-auth/client"
4 | import Image from "next/image"
5 | import { useRouter } from "next/router"
6 | import React, { useState } from "react"
7 | import { IoClose } from "react-icons/io5"
8 | import Favorites from "./Favorites"
9 |
10 | interface ProfileComponentProps {
11 | userData: UserProps
12 | }
13 |
14 | const ProfileComponent: React.FC = ({ userData }) => {
15 | const [confirm, setConfirm] = useState(false)
16 | const router = useRouter()
17 |
18 | const handleDelete = () => {
19 | if (userData) {
20 | deleteUser(userData._id)
21 | signOut()
22 | }
23 | }
24 |
25 | const handleSignOut = () => {
26 | signOut()
27 | router.push("/")
28 | }
29 | return (
30 |
31 |
32 |
33 |
34 |
42 |
43 |
44 |
{userData.name}
45 |
High score: {userData.score}
46 |
47 |
48 | {!router.query.uid && (
49 |
50 |
51 | Sign Out
52 |
53 | setConfirm(true)} className="delete-button">
54 | Delete Account
55 |
56 |
57 | )}
58 |
59 | {userData.favorites.length > 0 ? (
60 |
61 | ) : (
62 |
No Favorites
63 | )}
64 | {confirm && (
65 |
66 |
67 |
68 | setConfirm(false)} />
69 |
70 |
Confirm Deletion
71 |
72 | Are you sure you want to delete this account? You will not able to revert this
73 | process.
74 |
75 |
76 |
77 | confirm
78 |
79 | setConfirm(false)}>
80 | cancel
81 |
82 |
83 |
84 |
85 | )}
86 |
87 | )
88 | }
89 |
90 | export default ProfileComponent
91 |
--------------------------------------------------------------------------------
/components/Cards/Card.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { UserFavoritesProps } from "@interfaces/Interfaces"
3 | import Image from "next/image"
4 | import { findColor, getTypeIcon } from "@helpers/getTypeIconsAndColor"
5 | import { getStringIDfromID } from "@helpers/GlobalFunctions"
6 | import { useAppDispatch, useAppSelector } from "@lib/reduxHooks"
7 | import { updateFavorites } from "@lib/userSlice"
8 | import { FaInfoCircle, FaRegStar, FaStar } from "react-icons/fa"
9 | import { useSession } from "next-auth/client"
10 | import { useRouter } from "next/router"
11 |
12 | export interface CardProps {
13 | pokemon: UserFavoritesProps
14 | }
15 |
16 | const Card: React.FC = ({ pokemon }) => {
17 | const { id, types, sprite, name } = pokemon
18 | const router = useRouter()
19 | const dispatch = useAppDispatch()
20 | const [session] = useSession()
21 | const { userData } = useAppSelector((state) => state.user)
22 | return (
23 |
24 |
32 | {session && !router.query.uid && (
33 |
dispatch(updateFavorites({ id, name, types, sprite }))}
37 | >
38 | {userData && userData.favorites.filter((fav) => fav.id === id).length > 0 ? (
39 |
40 |
41 | remove from favorites
42 |
43 | ) : (
44 |
45 |
46 | add to favorites
47 |
48 | )}
49 |
50 | )}
51 |
52 | {!router.query.pid && (
53 |
router.push(`/pokemon/${id}`)}
57 | >
58 |
59 |
60 | view more info
61 |
62 |
63 | )}
64 |
65 |
#{getStringIDfromID(id)}
66 |
67 |
68 | {types.map((type) => (
69 |
1 ? "-2.5px" : "0",
76 | }}
77 | key={type}
78 | alt={getTypeIcon(type)[0]}
79 | />
80 | ))}
81 |
82 |
{name}
83 |
84 |
85 | )
86 | }
87 |
88 | export default Card
89 |
--------------------------------------------------------------------------------
/components/Cards/_card.scss:
--------------------------------------------------------------------------------
1 | .card-container {
2 | width: 100%;
3 | height: 100%;
4 | padding: 10px;
5 | background: #fff;
6 | border-radius: 20px;
7 | box-shadow: 0 5px 25px 1px rgba(0, 0, 0, 0.5);
8 | .card {
9 | height: 100%;
10 | width: 100%;
11 | padding: 10px;
12 | border-radius: 15px;
13 | position: relative;
14 | display: flex;
15 | align-items: center;
16 | justify-content: center;
17 | flex-direction: column;
18 |
19 | &:hover {
20 | .star-wrapper,
21 | .info-wrapper {
22 | transform: translateY(0px);
23 | opacity: 1;
24 | }
25 | }
26 |
27 | .icon-wrapper {
28 | position: relative;
29 | &:hover {
30 | .tooltip {
31 | opacity: 1;
32 | transform: translateX(-50%) translateY(0px);
33 | }
34 | }
35 | .tooltip {
36 | position: absolute;
37 | top: -30px;
38 | left: 50%;
39 | color: #fff;
40 | font-size: x-small;
41 | white-space: nowrap;
42 | padding: 5px 10px;
43 | border-radius: 2.5px;
44 | transform: translateX(-50%) translateY(5px);
45 | opacity: 0;
46 | pointer-events: none;
47 | transition: all 0.15s ease-in-out;
48 | background: #000;
49 |
50 | &::before {
51 | content: "";
52 | position: absolute;
53 | top: 100%;
54 | left: 50%;
55 | width: 0;
56 | height: 0;
57 | border: 5px solid transparent;
58 | border-top-color: #000;
59 | transform: translate(-50%, 0);
60 | }
61 | }
62 | }
63 |
64 | .star-wrapper {
65 | position: absolute;
66 | z-index: 1000;
67 | top: 8px;
68 | right: 10px;
69 | transition: all 0.15s ease-in-out;
70 | color: #fff;
71 | font-size: 1.5rem;
72 | transform: translateY(0px);
73 | opacity: 1;
74 | @media screen and (min-width: 640px) {
75 | transform: translateY(-10px);
76 | opacity: 0;
77 | }
78 |
79 | &:hover {
80 | color: #ffc300;
81 | }
82 | }
83 |
84 | .info-wrapper {
85 | position: absolute;
86 | z-index: 1000;
87 | top: 8px;
88 | left: 10px;
89 | transition: all 0.15s ease-in-out;
90 | transform: translateY(0px);
91 | opacity: 1;
92 | color: #fff;
93 | font-size: 1.5rem;
94 | @media screen and (min-width: 640px) {
95 | transform: translateY(-10px);
96 | opacity: 0;
97 | }
98 | &:hover {
99 | color: #eee;
100 | }
101 | }
102 |
103 | .types-container {
104 | display: flex;
105 | position: absolute;
106 | bottom: 25px;
107 | left: 50%;
108 | transform: translateX(-50%);
109 | }
110 | .id-number {
111 | position: absolute;
112 | top: 10px;
113 | left: 50%;
114 | transform: translateX(-50%);
115 | font-size: 2.5rem;
116 | font-weight: 700;
117 | opacity: 0.5;
118 | }
119 | .name {
120 | position: absolute;
121 | bottom: 0;
122 | left: 50%;
123 | transform: translateX(-50%);
124 | font-size: 1rem;
125 | font-weight: 700;
126 | line-height: 1;
127 | text-transform: capitalize;
128 |
129 | @media screen and (min-width: 640px) {
130 | font-size: 1.25rem;
131 | }
132 | }
133 | }
134 |
135 | img {
136 | user-select: none;
137 | pointer-events: none;
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/components/PokemonPage/BioTrainCard.tsx:
--------------------------------------------------------------------------------
1 | import { PokemonDataInterface, PokemonSpeciesDataInterface } from "@interfaces/Interfaces"
2 | import React from "react"
3 | import Case from "case"
4 | import { getFlavorSpeech } from "@helpers/GlobalFunctions"
5 | import FlexBetween from "./FlexBetween"
6 |
7 | interface LeftCardProps {
8 | pokemondata: PokemonDataInterface
9 | speciesdata: PokemonSpeciesDataInterface
10 | }
11 |
12 | const BioTrainCard: React.FC = ({ pokemondata, speciesdata }) => {
13 | return (
14 |
15 | {speciesdata && pokemondata && (
16 |
17 |
18 |
Bio
19 |
{Case.sentence(getFlavorSpeech(speciesdata, pokemondata))}
20 |
21 |
22 |
26 | {Case.capital(
27 | speciesdata.genera.filter((entry) => entry.language.name === "en")[0].genus
28 | )}
29 |
30 | }
31 | />
32 |
36 | {pokemondata.height / 10}m{" "}
37 |
38 | ({Math.floor(((pokemondata.height / 10) * 39.37) / 12)}'
39 | {(((pokemondata.height / 10) * 39.37) % 12).toFixed(1)}")
40 |
41 |
42 | }
43 | />
44 |
48 | {pokemondata.weight / 10}kg{" "}
49 | ({((pokemondata.weight / 10) * 2.2).toFixed(1)} lbs)
50 |
51 | }
52 | />
53 |
57 | {pokemondata.abilities.map((ability) => (
58 |
59 | {ability.ability.name
60 | .split("-")
61 | .map((txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase())
62 | .join(" ")}{" "}
63 | {ability.is_hidden && "(Hidden Ability)"} {" "}
64 |
65 | ))}
66 |
67 | }
68 | />
69 |
70 |
71 |
72 |
73 |
Training
74 |
{pokemondata.base_experience}} />
75 | {speciesdata.base_happiness}} />
76 |
80 | {speciesdata.capture_rate}{" "}
81 |
82 | ({((speciesdata.capture_rate / 255) * 100).toFixed(1)}%)
83 |
84 |
85 | }
86 | />
87 | {speciesdata.growth_rate.name}} />
88 |
89 |
90 | )}
91 |
92 | )
93 | }
94 |
95 | export default BioTrainCard
96 |
--------------------------------------------------------------------------------
/components/Game/GamePage.tsx:
--------------------------------------------------------------------------------
1 | import Deck from "@components/Cards/Deck"
2 | import HiddenCard from "@components/Cards/HiddenCard"
3 | import { getOptions, shuffle } from "@helpers/GlobalFunctions"
4 | import { NameIDInterface } from "interfaces/Interfaces"
5 | import { useEffect, useState } from "react"
6 | import { ImHeart } from "react-icons/im"
7 | import Options from "./Options"
8 | import GameOver from "./GameOver"
9 |
10 | interface ExploreProps {
11 | pokemonsList: NameIDInterface[]
12 | }
13 |
14 | const GamePage: React.FC = ({ pokemonsList }) => {
15 | const [pokemons, setPokemons] = useState(pokemonsList)
16 | const [index, setIndex] = useState(0)
17 | const [exitX, setExitX] = useState(1000)
18 | const [options, setOptions] = useState(null)
19 | const [reveal, setReveal] = useState(false)
20 | const [chosen, setChosen] = useState(null)
21 | const [score, setScore] = useState(0)
22 | const [lives, setLives] = useState(3)
23 | const [isGameOver, setIsGameOver] = useState(false)
24 | const [answer, setAnswer] = useState("")
25 |
26 | useEffect(() => {
27 | const shuffledList = shuffle(pokemons)
28 | setPokemons(shuffledList)
29 | }, [pokemons])
30 |
31 | useEffect(() => {
32 | if (index < pokemons.length) {
33 | setAnswer(pokemons[index].name)
34 | const tempOptions = getOptions(pokemons, pokemons[index].name)
35 | const shuffledOptions = shuffle(tempOptions)
36 | setOptions(shuffledOptions)
37 | }
38 |
39 | return () => {
40 | setOptions(null)
41 | }
42 | }, [pokemons, index])
43 |
44 | useEffect(() => {
45 | if (lives <= 0 || index === pokemons.length) {
46 | setTimeout(() => {
47 | setIsGameOver(true)
48 | }, 3000)
49 | }
50 | }, [index, lives, pokemons])
51 |
52 | const handleSelect = (selected: string) => {
53 | if (index < pokemons.length) {
54 | setChosen(selected)
55 | setReveal(true)
56 | setExitX(Math.random() < 0.5 ? 1000 : -1000)
57 |
58 | setTimeout(() => {
59 | setIndex(index + 1)
60 | setReveal(false)
61 | setChosen(null)
62 | }, 3000)
63 |
64 | if (answer === selected) setScore(score + 1)
65 | if (answer !== selected) setLives(lives - 1)
66 | }
67 | }
68 |
69 | const handleCloseModal = () => {
70 | const shuffledList = shuffle(pokemons)
71 | setScore(0)
72 | setIsGameOver(false)
73 | setPokemons(shuffledList)
74 | setIndex(0)
75 | setLives(3)
76 | }
77 |
78 | return (
79 |
80 |
Who's that pokemon?
81 |
82 |
score: {score}
83 |
84 | {Array(lives)
85 | .fill( )
86 | .map((_a, idx) => (
87 |
88 |
89 |
90 | ))}
91 |
92 |
93 | {pokemons && (
94 |
105 | )}
106 |
107 | {options && (
108 |
115 | )}
116 |
117 | {isGameOver &&
}
118 |
119 | )
120 | }
121 |
122 | export default GamePage
123 |
--------------------------------------------------------------------------------
/components/Cards/Deck.tsx:
--------------------------------------------------------------------------------
1 | // import { NameIDInterface, PokemonDataInterface } from "@interfaces/Interfaces"
2 | import { AnimatePresence, PanInfo } from "framer-motion"
3 | import React from "react"
4 | import EndCard from "./EndCard"
5 | // import { CardProps } from "./Card"
6 | import FramerCard from "./FramerCard"
7 |
8 | interface DeckProps {
9 | pokemons: any[]
10 | cardIndex: number
11 | exitX: number
12 | setExitX?: (x: number) => void
13 | index: number
14 | setIndex?: (x: number) => void
15 | dragX: boolean | "x" | "y"
16 | CardComponent: React.FC
17 | reveal?: boolean
18 | }
19 |
20 | const Deck: React.FC = ({
21 | pokemons,
22 | cardIndex,
23 | exitX,
24 | setExitX,
25 | index,
26 | setIndex,
27 | dragX,
28 | CardComponent,
29 | reveal,
30 | }) => {
31 | const maximumX = 100
32 |
33 | function handleDragEnd(_event: MouseEvent | TouchEvent | PointerEvent, info: PanInfo) {
34 | if (info.offset.x < -maximumX) {
35 | if (setExitX) setExitX(-maximumX * 5)
36 | if (setIndex) setIndex(index + 1)
37 | }
38 | if (info.offset.x > maximumX) {
39 | if (setExitX) setExitX(maximumX * 5)
40 | if (setIndex) setIndex(index + 1)
41 | }
42 | }
43 |
44 | return (
45 |
46 |
47 | {pokemons.length >= 3 && cardIndex + 2 < pokemons.length + 1 && (
48 |
65 | {pokemons && cardIndex + 2 < pokemons.length ? (
66 |
67 | ) : (
68 |
69 | )}
70 |
71 | )}
72 | {pokemons.length >= 2 && cardIndex + 1 < pokemons.length + 1 && (
73 |
90 | {pokemons && cardIndex + 1 < pokemons.length ? (
91 |
92 | ) : (
93 |
94 | )}
95 |
96 | )}
97 | {pokemons.length >= 1 && cardIndex < pokemons.length + 1 && (
98 |
126 | {pokemons && cardIndex < pokemons.length ? (
127 |
128 | ) : (
129 |
130 | )}
131 |
132 | )}
133 |
134 |
135 | )
136 | }
137 |
138 | export default Deck
139 |
--------------------------------------------------------------------------------
/components/Nav/Nav.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link"
2 | import { FaCaretDown, FaGithub } from "react-icons/fa"
3 | import { CgPlayButtonO, CgPokemon, CgProfile, CgSearch } from "react-icons/cg"
4 | import { IoPodiumOutline } from "react-icons/io5"
5 | import { VscSignIn } from "react-icons/vsc"
6 | import { signIn, signOut, useSession } from "next-auth/client"
7 | import { useEffect } from "react"
8 | import { useAppDispatch } from "@lib/reduxHooks"
9 | import { signout, userSignIn } from "@lib/userSlice"
10 | import { useRouter } from "next/router"
11 | import ActiveLink from "./ActiveLink"
12 |
13 | const Nav = () => {
14 | const [session] = useSession()
15 | const router = useRouter()
16 | const dispatch = useAppDispatch()
17 |
18 | useEffect(() => {
19 | if (session && session.user) {
20 | dispatch(userSignIn(session.user))
21 | } else dispatch(signout())
22 | }, [session, dispatch])
23 |
24 | const handleSignOut = () => {
25 | signOut()
26 | router.push("/")
27 | }
28 |
29 | return (
30 |
31 |
32 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 | {session && session.user ? (
109 |
110 |
111 |
112 |
113 |
114 | ) : (
115 |
signIn("github")}>
116 |
117 |
118 |
119 |
120 | )}
121 |
122 |
123 |
124 | )
125 | }
126 |
127 | export default Nav
128 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Pokénex
6 |
7 |
8 | Pokédex app in card style. Swipe through pokemon cards or guess who's that pokemon?.
9 |
10 | Explore the docs »
11 |
12 |
13 | View Demo
14 | ·
15 | Report Bug
16 | ·
17 | Request Feature
18 | ·
19 | How it's made?
20 |
21 |
22 |
23 |
24 |
25 | Table of Contents
26 |
27 |
28 | About The Project
29 |
32 |
33 |
34 | Getting Started
35 |
39 |
40 | Roadmap
41 | Contact
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | ## About The Project
51 |
52 | 
53 |
54 | There are many great pokédex apps online but I found most of them are similarly built, displayed in a grid, scroll and click a pokémon to view more details about it. So I build my own but in a different style.
55 |
56 | No mobile version yet.
57 |
58 | Features:
59 |
60 | - Swipe pokémon cards to explore
61 | - Add to favorites
62 | - Play "who's that pokémon?"
63 | - Climb the leaderboard
64 |
65 | Of course, I will be adding more soon or you can request a feature .
66 |
67 |
68 |
69 |
70 | ### Built With
71 |
72 | - [PokéAPI](https://pokeapi.co/)
73 | - [Nextjs](https://nextjs.org/)
74 | - [TypeScript](https://www.typescriptlang.org/)
75 | - [Sass](https://sass-lang.com/)
76 | - [redux-toolkit](https://redux-toolkit.js.org/)
77 | - [Framer](https://www.framer.com/api/motion/)
78 | - [next-auth](https://firebase.google.com/docs/auth)
79 | - [MongoDB](https://www.mongodb.com/)
80 | - [Mongoose](https://mongoosejs.com/)
81 | - [Pokémon Assets from HybridShivam](https://github.com/HybridShivam/pokémon)
82 |
83 |
84 |
85 |
86 |
87 |
88 | ## Getting Started
89 |
90 | To get a local copy up and running follow these simple example steps.
91 |
92 | ### Prerequisites
93 |
94 | This is an example of how to list things you need to use the software and how to install them.
95 |
96 | - yarn
97 | ```sh
98 | npm install --global yarn
99 | ```
100 |
101 | ### Installation
102 |
103 | 1. Clone the repo
104 | ```sh
105 | git clone https://github.com/kitharvey/pokenex.git
106 | ```
107 | 2. Go to project directory
108 | ```sh
109 | cd pokenex
110 | ```
111 | 3. Install NPM packages
112 | ```sh
113 | yarn
114 | ```
115 | 4. Create [MongoDB](https://www.mongodb.com/), set up your database and copy MONGODB_URI
116 | 5. Create [Github-OAuth](https://github.com/settings/applications/new) and copy client ID and secrets
117 | 6. Create `.env` file and add URI and keys
118 | ```js
119 | MONGODB_URI = "xxxxxxxxxxxxxxxxxxxxxxxx"
120 | GITHUB_CLIENT_ID = "xxxxxxxxxxxxxxxxxxxxxxxx"
121 | GITHUB_CLIENT_SECRET = "xxxxxxxxxxxxxxxxxxxxxxxx"
122 | AUTH_SECRET = "xxxxxxxxxxxxxxxxxxxxxxxx"
123 | JWT_SECRET = "xxxxxxxxxxxxxxxxxxxxxxxx"
124 | JWT_SIGNING_PRIVATE_KEY = "xxxxxxxxxxxxxxxxxxxxxxxx"
125 | NEXTAUTH_URL = "http://localhost:3000/"
126 | ```
127 | 7. Start the application in your localhost
128 | ```sh
129 | yarn dev
130 | ```
131 |
132 |
133 |
134 | ## Roadmap
135 |
136 | This project is still under development.
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 | ## Contact
146 |
147 | Kit Harvey - [linkedIn](https://www.linkedin.com/in/kitharvey/)
148 |
149 | Project Link: [https://github.com/kitharvey/pokenex](https://github.com/kitharvey/pokenex)
150 |
151 |
152 |
153 |
--------------------------------------------------------------------------------