├── .eslintrc.json ├── .prettierrc ├── src ├── lib │ ├── firebase │ │ ├── index.ts │ │ ├── clientApp.ts │ │ └── util.ts │ ├── getCategories.ts │ ├── getItems.ts │ └── getScores.ts ├── store │ ├── scores.ts │ ├── categories.ts │ ├── items.ts │ └── auth.ts ├── hooks │ └── useDebounce.ts ├── components │ ├── MotionFade.tsx │ ├── PeopleCard.tsx │ ├── Form │ │ ├── Select.tsx │ │ ├── Input.tsx │ │ ├── InputGroup.tsx │ │ └── FileInput.tsx │ ├── DraggableImage.tsx │ ├── ScoreCard.tsx │ ├── LoadingOverlay.tsx │ ├── Layout.tsx │ ├── WelcomeScreen.tsx │ ├── Card.tsx │ ├── RecycleBox.tsx │ ├── ItemWindow.tsx │ └── Navbar.tsx ├── styles │ └── tailwind.css ├── pages │ ├── scoreboard.tsx │ ├── [slug].tsx │ ├── _document.tsx │ ├── map.tsx │ ├── index.tsx │ ├── _app.tsx │ └── admin.tsx └── posts │ ├── how.mdx │ └── about.mdx ├── public ├── drop.mp3 ├── logo.png ├── woman.png ├── banner.png ├── marker.png ├── background.png ├── icons │ ├── favicon.png │ ├── ios │ │ ├── 100.png │ │ ├── 114.png │ │ ├── 120.png │ │ ├── 128.png │ │ ├── 144.png │ │ ├── 152.png │ │ ├── 16.png │ │ ├── 167.png │ │ ├── 180.png │ │ ├── 192.png │ │ ├── 20.png │ │ ├── 256.png │ │ ├── 29.png │ │ ├── 32.png │ │ ├── 40.png │ │ ├── 50.png │ │ ├── 512.png │ │ ├── 57.png │ │ ├── 58.png │ │ ├── 60.png │ │ ├── 64.png │ │ ├── 72.png │ │ ├── 76.png │ │ ├── 80.png │ │ ├── 87.png │ │ └── 1024.png │ ├── windows11 │ │ ├── LargeTile.scale-100.png │ │ ├── LargeTile.scale-125.png │ │ ├── LargeTile.scale-150.png │ │ ├── LargeTile.scale-200.png │ │ ├── LargeTile.scale-400.png │ │ ├── SmallTile.scale-100.png │ │ ├── SmallTile.scale-125.png │ │ ├── SmallTile.scale-150.png │ │ ├── SmallTile.scale-200.png │ │ ├── SmallTile.scale-400.png │ │ ├── StoreLogo.scale-100.png │ │ ├── StoreLogo.scale-125.png │ │ ├── StoreLogo.scale-150.png │ │ ├── StoreLogo.scale-200.png │ │ ├── StoreLogo.scale-400.png │ │ ├── SplashScreen.scale-100.png │ │ ├── SplashScreen.scale-125.png │ │ ├── SplashScreen.scale-150.png │ │ ├── SplashScreen.scale-200.png │ │ ├── SplashScreen.scale-400.png │ │ ├── Square150x150Logo.scale-100.png │ │ ├── Square150x150Logo.scale-125.png │ │ ├── Square150x150Logo.scale-150.png │ │ ├── Square150x150Logo.scale-200.png │ │ ├── Square150x150Logo.scale-400.png │ │ ├── Square44x44Logo.scale-100.png │ │ ├── Square44x44Logo.scale-125.png │ │ ├── Square44x44Logo.scale-150.png │ │ ├── Square44x44Logo.scale-200.png │ │ ├── Square44x44Logo.scale-400.png │ │ ├── Wide310x150Logo.scale-100.png │ │ ├── Wide310x150Logo.scale-125.png │ │ ├── Wide310x150Logo.scale-150.png │ │ ├── Wide310x150Logo.scale-200.png │ │ ├── Wide310x150Logo.scale-400.png │ │ ├── Square44x44Logo.targetsize-16.png │ │ ├── Square44x44Logo.targetsize-20.png │ │ ├── Square44x44Logo.targetsize-24.png │ │ ├── Square44x44Logo.targetsize-30.png │ │ ├── Square44x44Logo.targetsize-32.png │ │ ├── Square44x44Logo.targetsize-36.png │ │ ├── Square44x44Logo.targetsize-40.png │ │ ├── Square44x44Logo.targetsize-44.png │ │ ├── Square44x44Logo.targetsize-48.png │ │ ├── Square44x44Logo.targetsize-60.png │ │ ├── Square44x44Logo.targetsize-64.png │ │ ├── Square44x44Logo.targetsize-72.png │ │ ├── Square44x44Logo.targetsize-80.png │ │ ├── Square44x44Logo.targetsize-96.png │ │ ├── Square44x44Logo.targetsize-256.png │ │ ├── Square44x44Logo.altform-unplated_targetsize-16.png │ │ ├── Square44x44Logo.altform-unplated_targetsize-20.png │ │ ├── Square44x44Logo.altform-unplated_targetsize-24.png │ │ ├── Square44x44Logo.altform-unplated_targetsize-256.png │ │ ├── Square44x44Logo.altform-unplated_targetsize-30.png │ │ ├── Square44x44Logo.altform-unplated_targetsize-32.png │ │ ├── Square44x44Logo.altform-unplated_targetsize-36.png │ │ ├── Square44x44Logo.altform-unplated_targetsize-40.png │ │ ├── Square44x44Logo.altform-unplated_targetsize-44.png │ │ ├── Square44x44Logo.altform-unplated_targetsize-48.png │ │ ├── Square44x44Logo.altform-unplated_targetsize-60.png │ │ ├── Square44x44Logo.altform-unplated_targetsize-64.png │ │ ├── Square44x44Logo.altform-unplated_targetsize-72.png │ │ ├── Square44x44Logo.altform-unplated_targetsize-80.png │ │ ├── Square44x44Logo.altform-unplated_targetsize-96.png │ │ ├── Square44x44Logo.altform-lightunplated_targetsize-16.png │ │ ├── Square44x44Logo.altform-lightunplated_targetsize-20.png │ │ ├── Square44x44Logo.altform-lightunplated_targetsize-24.png │ │ ├── Square44x44Logo.altform-lightunplated_targetsize-256.png │ │ ├── Square44x44Logo.altform-lightunplated_targetsize-30.png │ │ ├── Square44x44Logo.altform-lightunplated_targetsize-32.png │ │ ├── Square44x44Logo.altform-lightunplated_targetsize-36.png │ │ ├── Square44x44Logo.altform-lightunplated_targetsize-40.png │ │ ├── Square44x44Logo.altform-lightunplated_targetsize-44.png │ │ ├── Square44x44Logo.altform-lightunplated_targetsize-48.png │ │ ├── Square44x44Logo.altform-lightunplated_targetsize-60.png │ │ ├── Square44x44Logo.altform-lightunplated_targetsize-64.png │ │ ├── Square44x44Logo.altform-lightunplated_targetsize-72.png │ │ ├── Square44x44Logo.altform-lightunplated_targetsize-80.png │ │ └── Square44x44Logo.altform-lightunplated_targetsize-96.png │ └── android │ │ ├── android-launchericon-48-48.png │ │ ├── android-launchericon-72-72.png │ │ ├── android-launchericon-96-96.png │ │ ├── android-launchericon-144-144.png │ │ ├── android-launchericon-192-192.png │ │ └── android-launchericon-512-512.png ├── images │ └── people │ │ ├── baki.jpeg │ │ ├── kerim.png │ │ └── semih.jpeg └── manifest.json ├── postcss.config.js ├── .env.example ├── tailwind.config.js ├── next.config.js ├── tsconfig.json ├── .gitignore ├── package.json └── README.md /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier-plugin-tailwindcss"] 3 | } 4 | -------------------------------------------------------------------------------- /src/lib/firebase/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./clientApp"; 2 | export * from "./util"; 3 | -------------------------------------------------------------------------------- /public/drop.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/drop.mp3 -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/logo.png -------------------------------------------------------------------------------- /public/woman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/woman.png -------------------------------------------------------------------------------- /public/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/banner.png -------------------------------------------------------------------------------- /public/marker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/marker.png -------------------------------------------------------------------------------- /public/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/background.png -------------------------------------------------------------------------------- /public/icons/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/favicon.png -------------------------------------------------------------------------------- /public/icons/ios/100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/ios/100.png -------------------------------------------------------------------------------- /public/icons/ios/114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/ios/114.png -------------------------------------------------------------------------------- /public/icons/ios/120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/ios/120.png -------------------------------------------------------------------------------- /public/icons/ios/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/ios/128.png -------------------------------------------------------------------------------- /public/icons/ios/144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/ios/144.png -------------------------------------------------------------------------------- /public/icons/ios/152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/ios/152.png -------------------------------------------------------------------------------- /public/icons/ios/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/ios/16.png -------------------------------------------------------------------------------- /public/icons/ios/167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/ios/167.png -------------------------------------------------------------------------------- /public/icons/ios/180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/ios/180.png -------------------------------------------------------------------------------- /public/icons/ios/192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/ios/192.png -------------------------------------------------------------------------------- /public/icons/ios/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/ios/20.png -------------------------------------------------------------------------------- /public/icons/ios/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/ios/256.png -------------------------------------------------------------------------------- /public/icons/ios/29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/ios/29.png -------------------------------------------------------------------------------- /public/icons/ios/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/ios/32.png -------------------------------------------------------------------------------- /public/icons/ios/40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/ios/40.png -------------------------------------------------------------------------------- /public/icons/ios/50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/ios/50.png -------------------------------------------------------------------------------- /public/icons/ios/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/ios/512.png -------------------------------------------------------------------------------- /public/icons/ios/57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/ios/57.png -------------------------------------------------------------------------------- /public/icons/ios/58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/ios/58.png -------------------------------------------------------------------------------- /public/icons/ios/60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/ios/60.png -------------------------------------------------------------------------------- /public/icons/ios/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/ios/64.png -------------------------------------------------------------------------------- /public/icons/ios/72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/ios/72.png -------------------------------------------------------------------------------- /public/icons/ios/76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/ios/76.png -------------------------------------------------------------------------------- /public/icons/ios/80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/ios/80.png -------------------------------------------------------------------------------- /public/icons/ios/87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/ios/87.png -------------------------------------------------------------------------------- /public/icons/ios/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/ios/1024.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/images/people/baki.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/images/people/baki.jpeg -------------------------------------------------------------------------------- /public/images/people/kerim.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/images/people/kerim.png -------------------------------------------------------------------------------- /public/images/people/semih.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/images/people/semih.jpeg -------------------------------------------------------------------------------- /public/icons/windows11/LargeTile.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/LargeTile.scale-100.png -------------------------------------------------------------------------------- /public/icons/windows11/LargeTile.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/LargeTile.scale-125.png -------------------------------------------------------------------------------- /public/icons/windows11/LargeTile.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/LargeTile.scale-150.png -------------------------------------------------------------------------------- /public/icons/windows11/LargeTile.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/LargeTile.scale-200.png -------------------------------------------------------------------------------- /public/icons/windows11/LargeTile.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/LargeTile.scale-400.png -------------------------------------------------------------------------------- /public/icons/windows11/SmallTile.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/SmallTile.scale-100.png -------------------------------------------------------------------------------- /public/icons/windows11/SmallTile.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/SmallTile.scale-125.png -------------------------------------------------------------------------------- /public/icons/windows11/SmallTile.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/SmallTile.scale-150.png -------------------------------------------------------------------------------- /public/icons/windows11/SmallTile.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/SmallTile.scale-200.png -------------------------------------------------------------------------------- /public/icons/windows11/SmallTile.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/SmallTile.scale-400.png -------------------------------------------------------------------------------- /public/icons/windows11/StoreLogo.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/StoreLogo.scale-100.png -------------------------------------------------------------------------------- /public/icons/windows11/StoreLogo.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/StoreLogo.scale-125.png -------------------------------------------------------------------------------- /public/icons/windows11/StoreLogo.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/StoreLogo.scale-150.png -------------------------------------------------------------------------------- /public/icons/windows11/StoreLogo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/StoreLogo.scale-200.png -------------------------------------------------------------------------------- /public/icons/windows11/StoreLogo.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/StoreLogo.scale-400.png -------------------------------------------------------------------------------- /src/store/scores.ts: -------------------------------------------------------------------------------- 1 | import { IUser } from "@/store/auth"; 2 | import { atom } from "jotai"; 3 | 4 | export const scoresAtom = atom([]); 5 | -------------------------------------------------------------------------------- /public/icons/windows11/SplashScreen.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/SplashScreen.scale-100.png -------------------------------------------------------------------------------- /public/icons/windows11/SplashScreen.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/SplashScreen.scale-125.png -------------------------------------------------------------------------------- /public/icons/windows11/SplashScreen.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/SplashScreen.scale-150.png -------------------------------------------------------------------------------- /public/icons/windows11/SplashScreen.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/SplashScreen.scale-200.png -------------------------------------------------------------------------------- /public/icons/windows11/SplashScreen.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/SplashScreen.scale-400.png -------------------------------------------------------------------------------- /public/icons/android/android-launchericon-48-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/android/android-launchericon-48-48.png -------------------------------------------------------------------------------- /public/icons/android/android-launchericon-72-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/android/android-launchericon-72-72.png -------------------------------------------------------------------------------- /public/icons/android/android-launchericon-96-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/android/android-launchericon-96-96.png -------------------------------------------------------------------------------- /public/icons/android/android-launchericon-144-144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/android/android-launchericon-144-144.png -------------------------------------------------------------------------------- /public/icons/android/android-launchericon-192-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/android/android-launchericon-192-192.png -------------------------------------------------------------------------------- /public/icons/android/android-launchericon-512-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/android/android-launchericon-512-512.png -------------------------------------------------------------------------------- /public/icons/windows11/Square150x150Logo.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square150x150Logo.scale-100.png -------------------------------------------------------------------------------- /public/icons/windows11/Square150x150Logo.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square150x150Logo.scale-125.png -------------------------------------------------------------------------------- /public/icons/windows11/Square150x150Logo.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square150x150Logo.scale-150.png -------------------------------------------------------------------------------- /public/icons/windows11/Square150x150Logo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square150x150Logo.scale-200.png -------------------------------------------------------------------------------- /public/icons/windows11/Square150x150Logo.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square150x150Logo.scale-400.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square44x44Logo.scale-100.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square44x44Logo.scale-125.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square44x44Logo.scale-150.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square44x44Logo.scale-200.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square44x44Logo.scale-400.png -------------------------------------------------------------------------------- /public/icons/windows11/Wide310x150Logo.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Wide310x150Logo.scale-100.png -------------------------------------------------------------------------------- /public/icons/windows11/Wide310x150Logo.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Wide310x150Logo.scale-125.png -------------------------------------------------------------------------------- /public/icons/windows11/Wide310x150Logo.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Wide310x150Logo.scale-150.png -------------------------------------------------------------------------------- /public/icons/windows11/Wide310x150Logo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Wide310x150Logo.scale-200.png -------------------------------------------------------------------------------- /public/icons/windows11/Wide310x150Logo.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Wide310x150Logo.scale-400.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.targetsize-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square44x44Logo.targetsize-16.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.targetsize-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square44x44Logo.targetsize-20.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.targetsize-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square44x44Logo.targetsize-24.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.targetsize-30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square44x44Logo.targetsize-30.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.targetsize-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square44x44Logo.targetsize-32.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.targetsize-36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square44x44Logo.targetsize-36.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.targetsize-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square44x44Logo.targetsize-40.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.targetsize-44.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square44x44Logo.targetsize-44.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.targetsize-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square44x44Logo.targetsize-48.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.targetsize-60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square44x44Logo.targetsize-60.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.targetsize-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square44x44Logo.targetsize-64.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.targetsize-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square44x44Logo.targetsize-72.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.targetsize-80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square44x44Logo.targetsize-80.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.targetsize-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square44x44Logo.targetsize-96.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.targetsize-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square44x44Logo.targetsize-256.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-16.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-20.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-24.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-256.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-30.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-32.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-36.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-40.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-44.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-44.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-48.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-60.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-64.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-72.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-80.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-96.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-16.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-20.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-24.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-256.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-30.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-32.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-36.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-40.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-44.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-44.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-48.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-60.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-64.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-72.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-80.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggsy/recycling-platform/HEAD/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-96.png -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_FIREBASE_API_KEY= 2 | NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN= 3 | NEXT_PUBLIC_FIREBASE_PROJECT_ID= 4 | NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET= 5 | NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID= 6 | NEXT_PUBLIC_FIREBASE_APP_ID= 7 | NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID= 8 | NEXT_PUBLIC_GOOGLE_MAPS_KEY= -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ["./src/**/*.{js,ts,jsx,tsx}"], 4 | theme: { 5 | extend: { 6 | colors: { 7 | brand: "#BF8D2C", 8 | }, 9 | }, 10 | }, 11 | plugins: [require("@tailwindcss/typography"), require("@tailwindcss/forms")], 12 | }; 13 | -------------------------------------------------------------------------------- /src/store/categories.ts: -------------------------------------------------------------------------------- 1 | import { atom } from "jotai"; 2 | 3 | export interface ICategory { 4 | id: string; 5 | name: string; 6 | image: string; 7 | } 8 | 9 | export const categoriesAtom = atom<{ 10 | selectedCategoryId: string | null; 11 | categories: ICategory[]; 12 | }>({ 13 | selectedCategoryId: null, 14 | categories: [], 15 | }); 16 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const pwaConfig = { 2 | dest: "public", 3 | }; 4 | 5 | /** @type {import('next').NextConfig} */ 6 | const nextConfig = { 7 | reactStrictMode: true, 8 | productionBrowserSourceMaps: true, 9 | images: { 10 | domains: ["picsum.photos", "i.imgur.com", "firebasestorage.googleapis.com"], 11 | }, 12 | }; 13 | 14 | const withPWA = require("next-pwa")(pwaConfig); 15 | module.exports = withPWA(nextConfig); 16 | -------------------------------------------------------------------------------- /src/store/items.ts: -------------------------------------------------------------------------------- 1 | import { atom } from "jotai"; 2 | 3 | export interface IItem { 4 | id: string; 5 | categoryId: string; 6 | name: string; 7 | image: string; 8 | decomposeTime: string; 9 | wasteType: string; 10 | benefits?: string[]; 11 | results?: string[]; 12 | } 13 | 14 | export const itemsAtom = atom<{ 15 | selectedItemId: string | null; 16 | items: IItem[]; 17 | }>({ 18 | selectedItemId: null, 19 | items: [], 20 | }); 21 | -------------------------------------------------------------------------------- /src/store/auth.ts: -------------------------------------------------------------------------------- 1 | import { atom } from "jotai"; 2 | import type { User } from "firebase/auth"; 3 | 4 | export interface IUser { 5 | uid?: string; 6 | displayName: string; 7 | avatar: string; 8 | score: number; 9 | isAdmin?: boolean; 10 | } 11 | 12 | interface IAuth { 13 | user: User | null; 14 | userDb: IUser | null; 15 | isAdmin?: boolean; 16 | } 17 | 18 | export const authAtom = atom({ 19 | user: null, 20 | userDb: null, 21 | isAdmin: false, 22 | }); 23 | -------------------------------------------------------------------------------- /src/hooks/useDebounce.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | function useDebounce(value: T, delay?: number): T { 4 | const [debouncedValue, setDebouncedValue] = useState(value); 5 | 6 | useEffect(() => { 7 | const timer = setTimeout(() => setDebouncedValue(value), delay || 500); 8 | 9 | return () => { 10 | clearTimeout(timer); 11 | }; 12 | }, [value, delay]); 13 | 14 | return debouncedValue; 15 | } 16 | 17 | export default useDebounce; 18 | -------------------------------------------------------------------------------- /src/components/MotionFade.tsx: -------------------------------------------------------------------------------- 1 | import { motion } from "framer-motion"; 2 | import { ReactNode } from "react"; 3 | 4 | export const MotionFade = ({ 5 | children, 6 | className, 7 | }: { 8 | children: ReactNode; 9 | className?: string; 10 | }) => { 11 | return ( 12 | 24 | {children} 25 | 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /src/styles/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .no-highlight { 6 | -webkit-tap-highlight-color: transparent; 7 | } 8 | 9 | .filepond--wrapper .filepond--root { 10 | margin-bottom: 0; 11 | } 12 | 13 | .filepond--wrapper .filepond--drop-label { 14 | @apply rounded-lg border border-black/30 bg-white text-sm text-black/50 transition-colors hover:border-black/40 hover:text-black/70; 15 | } 16 | 17 | .keep-scrolling { 18 | -ms-overflow-style: none; 19 | scrollbar-width: none; 20 | } 21 | 22 | .keep-scrolling::-webkit-scrollbar { 23 | display: none; 24 | } 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "paths": { 18 | "@/*": ["./src/*"] 19 | } 20 | }, 21 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 22 | "exclude": ["node_modules"] 23 | } 24 | -------------------------------------------------------------------------------- /src/lib/getCategories.ts: -------------------------------------------------------------------------------- 1 | import { firestore } from "@/lib/firebase"; 2 | import { ICategory } from "@/store/categories"; 3 | import { collection, query, getDocs, orderBy } from "firebase/firestore"; 4 | 5 | const categoriesCollection = collection(firestore, "categories"); 6 | 7 | export const getCategories = async () => { 8 | const categoriesQuery = query(categoriesCollection, orderBy("name", "asc")); 9 | const querySnapshot = await getDocs(categoriesQuery); 10 | 11 | const results: ICategory[] = []; 12 | 13 | for (const snapshot of querySnapshot.docs) { 14 | results.push({ 15 | id: snapshot.id, 16 | ...snapshot.data(), 17 | } as ICategory); 18 | } 19 | 20 | return results; 21 | }; 22 | -------------------------------------------------------------------------------- /src/lib/getItems.ts: -------------------------------------------------------------------------------- 1 | import { firestore } from "@/lib/firebase"; 2 | import { IItem } from "@/store/items"; 3 | import { collection, query, getDocs, where, orderBy } from "firebase/firestore"; 4 | 5 | const itemsCollection = collection(firestore, "items"); 6 | 7 | export const getItems = async (id: string) => { 8 | const itemsQuery = query( 9 | itemsCollection, 10 | orderBy("name", "asc"), 11 | where("categoryId", "==", id) 12 | ); 13 | 14 | const querySnapshot = await getDocs(itemsQuery); 15 | 16 | const results: IItem[] = []; 17 | 18 | for (const snapshot of querySnapshot.docs) { 19 | results.push({ 20 | id: snapshot.id, 21 | ...snapshot.data(), 22 | } as IItem); 23 | } 24 | 25 | return results; 26 | }; 27 | -------------------------------------------------------------------------------- /src/pages/scoreboard.tsx: -------------------------------------------------------------------------------- 1 | import { useAtom } from "jotai"; 2 | 3 | // Store 4 | import { scoresAtom } from "@/store/scores"; 5 | 6 | // Components 7 | import { Layout } from "@/components/Layout"; 8 | import { ScoreCard } from "@/components/ScoreCard"; 9 | 10 | export default function Scoreboard() { 11 | const [scores] = useAtom(scoresAtom); 12 | 13 | return ( 14 | 15 | {scores.length === 0 && ( 16 | 17 | No one has found out about the game yet! 18 | 19 | )} 20 | 21 | {scores.map((score) => ( 22 | 23 | ))} 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/lib/getScores.ts: -------------------------------------------------------------------------------- 1 | import { firestore } from "@/lib/firebase"; 2 | import { IUser } from "@/store/auth"; 3 | import { 4 | collection, 5 | query, 6 | getDocs, 7 | orderBy, 8 | limit, 9 | where, 10 | } from "firebase/firestore"; 11 | 12 | const usersCollection = collection(firestore, "users"); 13 | 14 | export const getScores = async () => { 15 | const scoresQuery = query( 16 | usersCollection, 17 | where("score", ">", 0), 18 | orderBy("score", "desc"), 19 | limit(10) 20 | ); 21 | 22 | const querySnapshot = await getDocs(scoresQuery); 23 | const results: IUser[] = []; 24 | 25 | for (const snapshot of querySnapshot.docs) { 26 | results.push({ 27 | uid: snapshot.id, 28 | ...snapshot.data(), 29 | } as IUser); 30 | } 31 | 32 | return results; 33 | }; 34 | -------------------------------------------------------------------------------- /src/components/PeopleCard.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | 3 | export const PeopleCard = ({ 4 | image, 5 | name, 6 | linkedIn, 7 | }: { 8 | image: string; 9 | name: string; 10 | linkedIn: string; 11 | }) => { 12 | return ( 13 | 20 | Person image 27 | {name} 28 | 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /.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 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | # next-pwa 39 | /public/precache.*.*.js 40 | /public/sw.js 41 | /public/workbox-*.js 42 | /public/worker-*.js 43 | /public/fallback-*.js 44 | /public/precache.*.*.js.map 45 | /public/sw.js.map 46 | /public/workbox-*.js.map 47 | /public/worker-*.js.map 48 | /public/fallback-*.js 49 | -------------------------------------------------------------------------------- /src/lib/firebase/clientApp.ts: -------------------------------------------------------------------------------- 1 | import { initializeApp } from "firebase/app"; 2 | import { getAuth } from "firebase/auth"; 3 | import { getFirestore } from "firebase/firestore"; 4 | import { getStorage } from "firebase/storage"; 5 | import { getAnalytics, isSupported } from "firebase/analytics"; 6 | 7 | initializeApp({ 8 | apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY, 9 | authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN, 10 | projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, 11 | storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET, 12 | messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID, 13 | appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID, 14 | measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID, 15 | }); 16 | 17 | const firestore = getFirestore(); 18 | const auth = getAuth(); 19 | const storage = getStorage(); 20 | 21 | isSupported().then((supported) => { 22 | if (supported) getAnalytics(); 23 | }); 24 | 25 | export { firestore, auth, storage }; 26 | -------------------------------------------------------------------------------- /src/components/Form/Select.tsx: -------------------------------------------------------------------------------- 1 | export const Select = ({ 2 | setValue, label, placeholder, options, 3 | }: { 4 | value: string; 5 | setValue: any; 6 | label: string; 7 | placeholder: string; 8 | options?: { label: string; value: string; }[]; 9 | }) => { 10 | return ( 11 |
12 | 15 | 16 | 31 |
32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /src/components/DraggableImage.tsx: -------------------------------------------------------------------------------- 1 | import { useDraggable } from "@dnd-kit/core"; 2 | import Image from "next/image"; 3 | 4 | export const DraggableImage = ({ 5 | title, 6 | image, 7 | }: { 8 | title: string; 9 | image: string; 10 | }) => { 11 | const { attributes, listeners, setNodeRef, transform } = useDraggable({ 12 | id: "draggable", 13 | data: { 14 | item: title, 15 | }, 16 | }); 17 | 18 | const style = transform 19 | ? { 20 | transform: `translate3d(${transform.x}px, ${transform.y}px, 0) scaleX(${transform.scaleX}) scaleY(${transform.scaleY})`, 21 | } 22 | : undefined; 23 | 24 | return ( 25 | Image 41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /src/components/Form/Input.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | 3 | export const Input = ({ 4 | label, 5 | placeholder, 6 | value, 7 | setValue, 8 | onKeyDown, 9 | disabled, 10 | grow, 11 | }: { 12 | label?: string; 13 | placeholder: string; 14 | value: string; 15 | setValue?: any; 16 | disabled?: boolean; 17 | grow?: boolean; 18 | onKeyDown?: (e: React.KeyboardEvent) => void; 19 | }) => { 20 | return ( 21 |
22 | {label && ( 23 | 26 | )} 27 | 28 | setValue?.(e.target.value)} 32 | placeholder={placeholder} 33 | onKeyDown={onKeyDown} 34 | disabled={disabled} 35 | value={value} 36 | required 37 | /> 38 |
39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /src/components/ScoreCard.tsx: -------------------------------------------------------------------------------- 1 | import { FaCrown } from "react-icons/fa"; 2 | import { IUser } from "@/store/auth"; 3 | 4 | export const ScoreCard = ({ displayName, avatar, score, isAdmin }: IUser) => { 5 | return ( 6 |
7 | {/* eslint-disable-next-line @next/next/no-img-element */} 8 | {`Avatar 16 | 17 |
18 | {displayName} 19 | 20 |
21 | 22 | {score} recycles 23 | 24 | 25 | {isAdmin && ( 26 |
30 | 31 |
32 | )} 33 |
34 |
35 |
36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /src/components/LoadingOverlay.tsx: -------------------------------------------------------------------------------- 1 | import { MotionFade } from "@/components/MotionFade"; 2 | import { AnimatePresence } from "framer-motion"; 3 | 4 | export const LoadingOverlay = ({ active }: { active: boolean }) => { 5 | return ( 6 | 7 | {active && ( 8 | 9 | 10 | 11 | )} 12 | 13 | ); 14 | }; 15 | 16 | const Spinner = () => ( 17 | 22 | 27 | 31 | 38 | 39 | 40 | ); 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "recycling-platform", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@dnd-kit/core": "^6.0.8", 13 | "@formkit/auto-animate": "1.0.0-pre-alpha.3", 14 | "@react-google-maps/api": "^2.18.1", 15 | "eslint": "8.37.0", 16 | "eslint-config-next": "13.2.4", 17 | "filepond": "^4.30.4", 18 | "firebase": "^9.19.1", 19 | "framer-motion": "^10.10.0", 20 | "jotai": "^2.0.3", 21 | "next": "13.2.4", 22 | "next-mdx-remote": "^4.4.1", 23 | "next-pwa": "^5.6.0", 24 | "react": "18.2.0", 25 | "react-dom": "18.2.0", 26 | "react-filepond": "^7.1.2", 27 | "react-icons": "^4.8.0", 28 | "sonner": "^0.3.0", 29 | "typescript": "5.0.3", 30 | "use-sound": "^4.0.1" 31 | }, 32 | "devDependencies": { 33 | "@tailwindcss/forms": "^0.5.3", 34 | "@tailwindcss/typography": "^0.5.9", 35 | "@types/node": "18.15.11", 36 | "@types/react": "18.0.32", 37 | "@types/react-dom": "18.0.11", 38 | "autoprefixer": "^10.4.14", 39 | "clsx": "^1.2.1", 40 | "postcss": "^8.4.21", 41 | "prettier": "^2.8.7", 42 | "prettier-plugin-tailwindcss": "^0.2.6", 43 | "tailwindcss": "^3.3.1" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/posts/how.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: How to Recycle 3 | --- 4 | 5 | Recycling helps to reduce the amount of waste that goes into landfills, conserves natural resources, and saves energy. It also reduces greenhouse gas emissions and helps to mitigate climate change. Here are some simple answers to important questions. 6 | 7 | 1. What can be recycled? 8 | 9 | There are many materials that can be recycled, including paper, cardboard, plastic, glass, metal, and electronics. However, it's important to check with your local recycling program to find out what materials they accept. 10 | 11 | 2. How to recycle properly? 12 | 13 | - Separate ♻️ recyclable materials from non-recyclable materials. 14 | - Make sure that recyclable materials are clean and free of food residue. 15 | - Place recyclable materials in the appropriate recycling bin or container. 16 | - Do not put plastic bags or other non-recyclable materials in the recycling bin. 17 | 18 | 3. What can you do to prevent waste? 19 | 20 | - Reduce your overall waste by using reusable products. 21 | - Buy products made from recycled materials to support the recycling industry. 22 | - Check the recycling symbol on products to identify which materials can be recycled. 23 | 24 | 4. What if you don't recycle? 25 | 26 | - Your waste will end up in a landfill which are a major source of greenhouse gas emissions. 27 | - Landfills also take up a lot of space, which is a problem for cities. 28 | - You won't be helping sustaining better life conditions for your future, children and grandchildren. 29 | 30 | Taking these simple actions will be a big step towards a more sustainable future. 31 | -------------------------------------------------------------------------------- /src/components/Layout.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import Head from "next/head"; 3 | import { useRouter } from "next/router"; 4 | import { ReactNode } from "react"; 5 | import { TbChevronLeft } from "react-icons/tb"; 6 | 7 | export const Layout = ({ 8 | children, 9 | title, 10 | mainClass, 11 | rightSide, 12 | padding = true, 13 | grow, 14 | }: { 15 | children: ReactNode; 16 | title: string; 17 | mainClass?: string; 18 | rightSide?: ReactNode; 19 | padding?: boolean; 20 | grow?: boolean; 21 | }) => { 22 | const router = useRouter(); 23 | 24 | return ( 25 | <> 26 | 27 | {title} 28 | 29 | 30 |
31 |
32 |
33 | router.back()} 37 | /> 38 | 39 |

{title}

40 |
41 | 42 | {rightSide} 43 |
44 | 45 |
53 | {children} 54 |
55 |
56 | 57 | ); 58 | }; 59 | -------------------------------------------------------------------------------- /src/posts/about.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: About the Project 3 | --- 4 | 5 | This platform was created for the GDSC Solution Challenge 2023 to raise awareness of the importance of the recycling through gamification, visuals and information available on the website. We want to show people that their everyday items might be a lot dangerous to the environment then they think. We wanted to keep things as simple as possible so everybody can understand it. The information is gathered from various sources and the platform is designed to be easily accessible and usable on mobile devices. We are trying to help people realize the problem and solve the following [UN Sustainable Development Goals](https://sdgs.un.org/goals): 6 | 7 | 8 | 9 | While creating the platform, we used the following technologies: 10 | 11 | - React.js & Next.js 12 | - PWA (native apps, offline access) 13 | - Firebase (authentication, firestore, file hosting) 14 | - Google Analytics 15 | - Netlify (hosting) 16 | - Tailwind CSS (styling) 17 | - Framer Motion (animations) 18 | - [Filepond](https://pqina.nl) 19 | - Google Domains 20 | 21 | Our team: 22 | 23 | Our website is open source and you can find the code on [GitHub](https://github.com/eggsy/recycling-platform). 24 | 25 | --- 26 | 27 | **Notice: Data on this website might not be 100% accurate. We try to collect as accurate data as possible, but we cannot guarantee our success. We are not associated with any organization. All copyrights belong to their respective owners. Our main source for information is [Zero Waste](http://zerowaste.gov.tr/) website of the Turkish Government.** 28 | -------------------------------------------------------------------------------- /src/components/Form/InputGroup.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { TbX } from "react-icons/tb"; 3 | import { Input } from "./Input"; 4 | 5 | export const InputGroup = ({ 6 | label, 7 | placeholder, 8 | value, 9 | setValue, 10 | }: { 11 | label: string; 12 | placeholder: string; 13 | value: string[]; 14 | setValue: any; 15 | }) => { 16 | const [activeValue, setActive] = useState(""); 17 | 18 | const handleEnter = (e: React.KeyboardEvent) => { 19 | if (e.key === "Enter") { 20 | setValue([...value, activeValue]); 21 | setActive(""); 22 | } 23 | }; 24 | 25 | return ( 26 |
27 | 30 | 31 |
32 | {value.map((val, index) => ( 33 |
34 | 41 | 42 | 53 |
54 | ))} 55 | 56 | 62 |
63 | 64 | 65 | press enter to add more 66 | 67 |
68 | ); 69 | }; 70 | -------------------------------------------------------------------------------- /src/pages/[slug].tsx: -------------------------------------------------------------------------------- 1 | import { readdirSync, readFileSync } from "fs"; 2 | import { join } from "path"; 3 | import { serialize } from "next-mdx-remote/serialize"; 4 | import { MDXRemote } from "next-mdx-remote"; 5 | 6 | // Components 7 | import { PeopleCard } from "@/components/PeopleCard"; 8 | import { Layout } from "@/components/Layout"; 9 | import Image from "next/image"; 10 | 11 | const postsDirectory = join(process.cwd(), "src/posts"); 12 | 13 | export async function getStaticPaths() { 14 | const files = readdirSync(postsDirectory); 15 | 16 | return { 17 | paths: files.map((file) => `/${file.replace(/\.mdx?$/, "")}`), 18 | fallback: false, 19 | }; 20 | } 21 | 22 | export async function getStaticProps({ params }: { params: { slug: string } }) { 23 | const fileContents = readFileSync( 24 | join(postsDirectory, `${params.slug}.mdx`), 25 | "utf8" 26 | ); 27 | 28 | const mdxSource = await serialize(fileContents, { 29 | parseFrontmatter: true, 30 | }); 31 | 32 | return { 33 | props: { 34 | source: mdxSource, 35 | }, 36 | }; 37 | } 38 | 39 | export default function Post(props: any) { 40 | return ( 41 | 45 | 46 | 47 | ); 48 | } 49 | 50 | const TargetedGoals = () => ( 51 |
52 | {[ 53 | "https://i.imgur.com/tq5Kl40.jpg", 54 | "https://i.imgur.com/z297DNB.jpg", 55 | "https://i.imgur.com/jZlV2SG.jpg", 56 | "https://i.imgur.com/0MknMaW.jpg", 57 | "https://i.imgur.com/zIlnkK5.jpg", 58 | "https://i.imgur.com/OAIrcoR.jpg", 59 | ].map((image) => ( 60 | Targeted Goal 73 | ))} 74 |
75 | ); 76 | -------------------------------------------------------------------------------- /src/components/Form/FileInput.tsx: -------------------------------------------------------------------------------- 1 | import { storage } from "@/lib/firebase"; 2 | import { FilePond } from "react-filepond"; 3 | import { ProcessServerConfigFunction } from "filepond"; 4 | import { 5 | getDownloadURL, 6 | ref, 7 | uploadBytesResumable, 8 | deleteObject, 9 | } from "firebase/storage"; 10 | import { toast } from "sonner"; 11 | 12 | export const FileInput = ({ 13 | label, 14 | value, 15 | setValue, 16 | }: { 17 | label: string; 18 | value: string; 19 | setValue: any; 20 | }) => { 21 | const uploadImage: ProcessServerConfigFunction = async ( 22 | fieldName, 23 | file, 24 | metadata, 25 | load, 26 | error, 27 | progress 28 | ) => { 29 | const fileExtension = file.type.split("/")[1]; 30 | const storageRef = ref(storage, `image-${Date.now()}.${fileExtension}`); 31 | 32 | const uploadTask = uploadBytesResumable(storageRef, file); 33 | 34 | const unsubscribe = uploadTask.on( 35 | "state_changed", 36 | (snapshot) => { 37 | progress(true, snapshot.bytesTransferred, snapshot.totalBytes); 38 | }, 39 | (err) => { 40 | error(err.message); 41 | toast.error(err.message); 42 | unsubscribe(); 43 | }, 44 | async () => { 45 | const downloadLink = await getDownloadURL(uploadTask.snapshot.ref); 46 | 47 | setValue(downloadLink); 48 | load(downloadLink); 49 | unsubscribe(); 50 | } 51 | ); 52 | }; 53 | 54 | const removeImage = async () => { 55 | if (!value) return; 56 | const storageRef = ref(storage, value); 57 | 58 | await deleteObject(storageRef) 59 | .then(() => setValue("")) 60 | .catch((err) => { 61 | toast.error(err.message); 62 | }); 63 | 64 | setValue(""); 65 | }; 66 | 67 | return ( 68 |
69 | 72 | 73 | removeImage()} 75 | server={{ 76 | process: uploadImage.bind(this), 77 | remove: () => removeImage(), 78 | }} 79 | acceptedFileTypes={["image/png"]} 80 | credits={false} 81 | /> 82 |
83 | ); 84 | }; 85 | -------------------------------------------------------------------------------- /src/lib/firebase/util.ts: -------------------------------------------------------------------------------- 1 | import { toast } from "sonner"; 2 | import { IUser } from "@/store/auth"; 3 | import { auth, firestore } from "@/lib/firebase/clientApp"; 4 | import { doc, getDoc, setDoc, updateDoc } from "firebase/firestore"; 5 | import { GoogleAuthProvider, signInWithPopup } from "firebase/auth"; 6 | 7 | const googleProvider = new GoogleAuthProvider(); 8 | 9 | export const getCurrentUser = async () => { 10 | const user = auth.currentUser; 11 | 12 | if (!user?.uid) return null; 13 | 14 | return await getDoc(doc(firestore, `users/${user.uid}`)) 15 | .then(async (userDoc) => { 16 | if (!userDoc.exists()) { 17 | const newUser = await createUser({ 18 | displayName: user.displayName, 19 | avatar: user.photoURL, 20 | lastUpdatedAt: new Date(), 21 | uid: user.uid, 22 | }); 23 | 24 | return newUser as IUser; 25 | } 26 | 27 | return userDoc.data() as IUser; 28 | }) 29 | .catch((err) => { 30 | toast.error(err.message); 31 | return null; 32 | }); 33 | }; 34 | 35 | const createUser = async ({ 36 | displayName, 37 | avatar, 38 | lastUpdatedAt, 39 | uid, 40 | }: { 41 | displayName: string | null; 42 | avatar: string | null; 43 | lastUpdatedAt: Date; 44 | uid: string; 45 | }) => { 46 | const dataObject = { 47 | displayName, 48 | avatar, 49 | lastUpdatedAt, 50 | score: 0, 51 | }; 52 | 53 | await setDoc(doc(firestore, `users/${uid}`), dataObject); 54 | return dataObject; 55 | }; 56 | 57 | export const updateScore = async (score: number) => { 58 | const user = auth.currentUser; 59 | 60 | if (user) { 61 | await updateDoc(doc(firestore, `users/${user.uid}`), { 62 | lastUpdatedAt: new Date(), 63 | score, 64 | }); 65 | } 66 | }; 67 | 68 | export const signInPopup = () => { 69 | const sure = confirm( 70 | "By logging in you agree to share your display name and avatar with us. Do you wish you continue?" 71 | ); 72 | 73 | if (!sure) { 74 | toast.error("Okay, no it is."); 75 | return; 76 | } 77 | 78 | signInWithPopup(auth, googleProvider) 79 | .then((user) => { 80 | toast.success(`Welcome ${user.user.displayName}!`); 81 | }) 82 | .catch((error) => { 83 | const errorCode = error.code; 84 | const errorMessage = error.message; 85 | const email = error.customData.email; 86 | const credential = GoogleAuthProvider.credentialFromError(error); 87 | 88 | console.error(errorCode, errorMessage, email, credential); 89 | }); 90 | }; 91 | -------------------------------------------------------------------------------- /src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from "next/document"; 2 | import Image from "next/image"; 3 | 4 | export default function Document() { 5 | const title = "Recycling is Important"; 6 | const description = 7 | "Uncover the unknown environmental damage of everyday items with our recycling awareness platform. Discover the power of recycling and help protect the planet."; 8 | const themeColor = "#BF8D2C"; 9 | const url = "https://recycling.is-important.net"; 10 | 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 31 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 |
57 | Background image 65 |
66 | 67 |
68 | 69 | 70 | 71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | ♻️ Recycling Platform 3 |

4 | 5 | We are trying to raise awareness to the importance of recycling. This is a [Google Solution Challenge 2023](https://developers.google.com/community/gdsc-solution-challenge) project that uses Firebase (authentication, analytics, firestore and storage) as well as latest technologies such as React.js, Next.js, Tailwind CSS and Framer Motion to ensure best user experience. [Click here for the real-life app](https://recycling.is-important.net). 6 | 7 | ## Watch the Trailer 8 | 9 | [![Watch the Trailer](/public/banner.png)](https://youtu.be/tBjP00O3QrU) 10 | 11 | ## Features 12 | 13 | - Easy to use and simple UI. 14 | - Efficient data with Firestore. 15 | - User authentication with Firebase. 16 | - Admin dashboard. 17 | - Nearby recycling centers map with Google Maps API. 18 | - File upload with Filepond to Firebase Storage. 19 | - Smooth animations with Framer Motion and FormKit AutoAnimate. 20 | 21 | ## Techstack 22 | 23 | - React.js & Next.js 24 | - Tailwind CSS 25 | - Google Analytics 26 | - Firebase (firestore, authentication, storage, analytics) 27 | - PWA (native apps, offline access) 28 | 29 | ## Quick start 30 | 31 | Make sure you have [Node.js](https://nodejs.org) and [Git](https://git-scm.com) installed on your system. Node.js comes with a package manager called `npm` but I suggest you use [`pnpm`](https://pnpm.io/) instead since it's faster and more efficient. 32 | 33 | 1. Clone the repository: 34 | - `git clone https://github.com/eggsy/recycling-platform` 35 | 2. Install dependencies: 36 | - `pnpm install` or `npm install` 37 | 3. Create a Firestore account and get required fileds in `.env.example` file. 38 | 4. Fill in the required fields and rename it to `.env.local`. 39 | 5. Run the app: 40 | - `pnpm run dev` or `npm run dev` for development mode. 41 | - ` run build` and ` start` for production mode. 42 | 43 | ## Team 44 | 45 | 46 | 47 | 52 | 57 | 60 | 61 | 62 | 65 | 68 | 71 | 72 | 73 | 77 | 81 | 85 | 86 |
48 | 49 | image 50 | 51 | 53 | 54 | image 55 | 56 | 58 | image 59 |
63 | eggsy 64 | 66 | merloss 67 | 69 | Semih Özdaş 70 |
74 | 🔗 - 75 | 💼 76 | 78 | 🔗 - 79 | 💼 80 | 82 | 🔗 - 83 | 💼 84 |
87 | -------------------------------------------------------------------------------- /src/components/WelcomeScreen.tsx: -------------------------------------------------------------------------------- 1 | import { AnimatePresence } from "framer-motion"; 2 | import Image from "next/image"; 3 | import { useEffect, useState } from "react"; 4 | 5 | // Components 6 | import { MotionFade } from "@/components/MotionFade"; 7 | 8 | export const WelcomeScreen = ({ 9 | isFetchingData, 10 | }: { 11 | isFetchingData: boolean; 12 | }) => { 13 | const [isVisible, setVisible] = useState(false); 14 | 15 | useEffect(() => { 16 | const storage = localStorage.getItem("welcome-screen-dismised"); 17 | if (!storage) setVisible(true); 18 | }, []); 19 | 20 | const handleClose = () => { 21 | localStorage.setItem("welcome-screen-dismised", "true"); 22 | setVisible(false); 23 | }; 24 | 25 | return ( 26 | 27 | {!isFetchingData && isVisible && ( 28 | 29 |
30 |
31 |

Welcome 👋

32 |
33 | 34 |
35 |

36 | Hey there! Thank you for checking our app out, we are currently 37 | in beta and we continue to develop our application, please 38 | report any bugs you find. 39 |

40 | 41 |

42 | We want to build a platform to show people what their everyday 43 | items do to our environment if not recycled properly. Simply, 44 | elegantly and accesible. You can watch our trailer here: 45 |

46 | 47 | 53 | Trailer thumbnail 65 | 66 | 67 |

68 | Check out the about page for more information and look for a 69 | hidden game 👀 on the website! Check out the{" "} 70 | 71 | GitHub repository 72 | {" "} 73 | if you have any issues. 74 |

75 |
76 | 77 |
78 | 87 |
88 |
89 |
90 | )} 91 |
92 | ); 93 | }; 94 | -------------------------------------------------------------------------------- /src/components/Card.tsx: -------------------------------------------------------------------------------- 1 | import { TbRecycle, TbClock, TbX } from "react-icons/tb"; 2 | import Image from "next/image"; 3 | import { deleteDoc, doc } from "firebase/firestore"; 4 | import { firestore, storage } from "@/lib/firebase"; 5 | import { deleteObject, ref } from "firebase/storage"; 6 | import { toast } from "sonner"; 7 | import { ReactNode } from "react"; 8 | import clsx from "clsx"; 9 | 10 | interface ICardProps { 11 | id: string; 12 | image: string; 13 | name: string; 14 | decomposeTime?: string; 15 | type?: "category" | "item"; 16 | benefits?: string[]; 17 | isAdmin?: boolean; 18 | setItems: (p: any) => void; 19 | setCategories: any; 20 | } 21 | 22 | export const Card = ({ 23 | id, 24 | type = "item", 25 | image, 26 | name, 27 | decomposeTime, 28 | benefits, 29 | isAdmin, 30 | setCategories, 31 | setItems, 32 | }: ICardProps) => { 33 | const handleClick = () => { 34 | if (type === "category") { 35 | setCategories((p: any) => ({ ...p, selectedCategoryId: id })); 36 | } else { 37 | setItems((p: any) => ({ ...p, selectedItemId: id })); 38 | } 39 | }; 40 | 41 | const handleDelete = async () => { 42 | if (!isAdmin) return; 43 | const sure = confirm("Are you sure you want to delete this category?"); 44 | 45 | if (!sure) { 46 | toast.error("Category deletion cancelled."); 47 | return; 48 | } 49 | 50 | try { 51 | await deleteDoc(doc(firestore, `categories/${id}`)); 52 | await deleteObject(ref(storage, image)); 53 | 54 | setCategories((p: any) => ({ 55 | ...p, 56 | categories: p.categories.filter((i: any) => i.id !== id), 57 | })); 58 | 59 | toast.success("Category deleted successfully."); 60 | } catch (err: any) { 61 | toast.error(err.message); 62 | } 63 | }; 64 | 65 | return ( 66 |
70 |
71 | Item image 82 | 83 | {isAdmin && ( 84 | 98 | )} 99 |
100 | 101 |
102 |

{name}

103 | 104 | {type === "item" && (decomposeTime || benefits) && ( 105 |
106 | {decomposeTime && ( 107 | } danger> 108 | {decomposeTime} 109 | 110 | )} 111 | 112 | {Boolean(benefits?.length) && ( 113 | }>{benefits?.length} benefit(s) 114 | )} 115 |
116 | )} 117 |
118 |
119 | ); 120 | }; 121 | 122 | const Pill = ({ 123 | children, 124 | icon, 125 | danger, 126 | }: { 127 | children: ReactNode; 128 | icon: ReactNode; 129 | danger?: boolean; 130 | }) => { 131 | return ( 132 | 138 | {icon} 139 | {children} 140 | 141 | ); 142 | }; 143 | -------------------------------------------------------------------------------- /src/components/RecycleBox.tsx: -------------------------------------------------------------------------------- 1 | import { useDroppable } from "@dnd-kit/core"; 2 | import clsx from "clsx"; 3 | import { AnimatePresence, motion } from "framer-motion"; 4 | 5 | export const RecycleBox = ({ active }: { active: boolean }) => { 6 | const { setNodeRef, isOver } = useDroppable({ 7 | id: "recycle-box", 8 | }); 9 | 10 | return ( 11 | 12 | {active && ( 13 | 28 |
36 | 42 |
43 |
44 | )} 45 |
46 | ); 47 | }; 48 | 49 | const Svg = ({ className }: { className?: string }) => { 50 | return ( 51 | 59 | 60 | 64 | 68 | 72 | 76 | 77 | 81 | 85 | 89 | 93 | 97 | 101 | 105 | 109 | 113 | 114 | 115 | 116 | ); 117 | }; 118 | -------------------------------------------------------------------------------- /src/components/ItemWindow.tsx: -------------------------------------------------------------------------------- 1 | import { TbCheck, TbClock, TbRecycle, TbX } from "react-icons/tb"; 2 | import clsx from "clsx"; 3 | import { ReactNode } from "react"; 4 | import { toast } from "sonner"; 5 | import { deleteDoc, doc } from "firebase/firestore"; 6 | import { deleteObject, ref } from "firebase/storage"; 7 | import { firestore, storage } from "@/lib/firebase"; 8 | 9 | // Store 10 | import { IItem } from "@/store/items"; 11 | 12 | // Components 13 | import { DraggableImage } from "@/components/DraggableImage"; 14 | 15 | interface IItemWindowProps { 16 | item: IItem; 17 | setItems: any; 18 | category?: string; 19 | isAdmin?: boolean; 20 | } 21 | 22 | export const ItemWindow = ({ 23 | item, 24 | setItems, 25 | isAdmin, 26 | category, 27 | }: IItemWindowProps) => { 28 | const handleItemDelete = async (itemId: string) => { 29 | const sure = confirm("Are you sure you want to delete this item?"); 30 | 31 | if (!sure) { 32 | toast.error("Item deletion cancelled."); 33 | return; 34 | } 35 | 36 | try { 37 | await deleteDoc(doc(firestore, `items/${itemId}`)); 38 | await deleteObject(ref(storage, item.image)); 39 | 40 | setItems((p: any) => ({ 41 | ...p, 42 | items: p.items.filter((i: any) => i.id !== itemId), 43 | })); 44 | 45 | toast.success("Item deleted successfully."); 46 | } catch (err: any) { 47 | toast.error(err.message); 48 | } 49 | }; 50 | 51 | return ( 52 | <> 53 |
54 |
55 | 59 | setItems((p: any) => ({ ...p, selectedItemId: null })) 60 | } 61 | /> 62 | 63 |

{item.name}

64 |
65 | 66 | {isAdmin && ( 67 |
68 | 78 |
79 | )} 80 |
81 | 82 |
83 | {item.image && } 84 | 85 |

86 | {item.name}take(s){" "} 87 | 88 | 89 | {item.decomposeTime} 90 | 91 | to decompose. We can protect our environment by throwing them into{" "} 92 | 93 | 94 | {category} 95 | 96 | bin(s). 97 |

98 | 99 | {Boolean(item.results?.length) && ( 100 | <> 101 |

Environmental damage

102 | 103 |
    104 | {item.results?.map((result) => ( 105 |
  • 106 | 110 | {result} 111 |
  • 112 | ))} 113 |
114 | 115 | )} 116 | 117 | {Boolean(item.benefits?.length) && ( 118 | <> 119 |

When recycled properly

120 | 121 |
    122 | {item.benefits?.map((benefit) => ( 123 |
  • 124 | 128 | {benefit} 129 |
  • 130 | ))} 131 |
132 | 133 | )} 134 |
135 | 136 | ); 137 | }; 138 | 139 | const Pill = ({ 140 | variant = "black", 141 | children, 142 | }: { 143 | variant?: "black" | "red" | "green"; 144 | children: ReactNode; 145 | }) => { 146 | return ( 147 | 157 | {children} 158 | 159 | ); 160 | }; 161 | -------------------------------------------------------------------------------- /src/pages/map.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { 3 | GoogleMap, 4 | useLoadScript, 5 | MarkerF, 6 | InfoWindowF, 7 | } from "@react-google-maps/api"; 8 | import { toast } from "sonner"; 9 | import type { Libraries } from "@react-google-maps/api/dist/utils/make-load-script-url"; 10 | 11 | // Components 12 | import { Layout } from "@/components/Layout"; 13 | 14 | const mapLibraries = ["places"] as Libraries; 15 | 16 | interface IMarker { 17 | lat: number; 18 | lng: number; 19 | isOpen: boolean; 20 | title: string; 21 | address: string; 22 | } 23 | 24 | export default function MapPage() { 25 | const [loading, setLoading] = useState(true); 26 | const [markers, setMarkers] = useState([]); 27 | const [coords, setCoords] = useState({ 28 | latitude: 0, 29 | longtitude: 0, 30 | }); 31 | 32 | const { isLoaded } = useLoadScript({ 33 | googleMapsApiKey: process.env.NEXT_PUBLIC_GOOGLE_MAPS_KEY as string, 34 | libraries: mapLibraries, 35 | }); 36 | 37 | useEffect(() => { 38 | if (!navigator.geolocation) { 39 | toast.error("Geolocation is not supported by your browser."); 40 | setLoading(false); 41 | return; 42 | } 43 | 44 | navigator.geolocation.getCurrentPosition( 45 | (position) => { 46 | setCoords({ 47 | latitude: position.coords.latitude, 48 | longtitude: position.coords.longitude, 49 | }); 50 | 51 | setLoading(false); 52 | }, 53 | (err) => { 54 | toast.error( 55 | "Couldn't retreive your location. Make sure you have given permission to access your location." 56 | ); 57 | console.error(err); 58 | setLoading(false); 59 | } 60 | ); 61 | }, []); 62 | 63 | const fetchNearbyCenters = async (map: google.maps.Map) => { 64 | let service = new google.maps.places.PlacesService(map); 65 | 66 | service.nearbySearch( 67 | { 68 | location: { 69 | lat: coords.latitude, 70 | lng: coords.longtitude, 71 | }, 72 | keyword: "Recycling center", 73 | radius: 10000, 74 | }, 75 | (results, status) => { 76 | if (results?.length === 0) { 77 | toast.error( 78 | "Couldn't find any recycling centres in 10 kilometers radius." 79 | ); 80 | return; 81 | } 82 | 83 | if (status === google.maps.places.PlacesServiceStatus.OK && results) { 84 | setMarkers( 85 | results.map((result) => ({ 86 | lat: result.geometry?.location?.lat() as number, 87 | lng: result.geometry?.location?.lng() as number, 88 | title: result.name as string, 89 | address: result.vicinity as string, 90 | isOpen: false, 91 | })) 92 | ); 93 | } 94 | } 95 | ); 96 | }; 97 | 98 | const toggleWindow = (marker: IMarker) => { 99 | setMarkers( 100 | markers.map((m) => { 101 | if (m.lat === marker.lat) { 102 | return { 103 | ...m, 104 | isOpen: !m.isOpen, 105 | }; 106 | } 107 | 108 | return m; 109 | }) 110 | ); 111 | }; 112 | 113 | return ( 114 | 115 | {!loading && !Boolean(coords.latitude) && ( 116 |
117 | ⛔ Make sure you have given us permission to access your location (we 118 | are not storing your location). 119 |
120 | )} 121 | 122 | {isLoaded && !loading && coords.latitude !== 0 && ( 123 | 142 | 148 | 149 | {markers.map((marker) => ( 150 | toggleWindow(marker)} 158 | > 159 | {marker.isOpen && ( 160 | toggleWindow(marker)} 163 | > 164 |
165 |

{marker.title}

166 | 167 | 168 | {marker.address} 169 | 170 | 171 | 177 | Get Directions 178 | 179 |
180 |
181 | )} 182 |
183 | ))} 184 |
185 | )} 186 |
187 | ); 188 | } 189 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { TbSearch, TbChevronLeft } from "react-icons/tb"; 2 | import { useEffect, useMemo, useRef, useState } from "react"; 3 | import autoAnimate from "@formkit/auto-animate"; 4 | import { useAtom } from "jotai"; 5 | import { AnimatePresence, motion } from "framer-motion"; 6 | import { useRouter } from "next/router"; 7 | 8 | // Store 9 | import { categoriesAtom } from "@/store/categories"; 10 | import { itemsAtom } from "@/store/items"; 11 | import { authAtom } from "@/store/auth"; 12 | 13 | // Components 14 | import { Card } from "@/components/Card"; 15 | import { ItemWindow } from "@/components/ItemWindow"; 16 | 17 | export default function Home() { 18 | const [search, setSearch] = useState(""); 19 | const parent = useRef(null); 20 | const router = useRouter(); 21 | 22 | const [categories, setCategories] = useAtom(categoriesAtom); 23 | const [items, setItems] = useAtom(itemsAtom); 24 | const [authCache] = useAtom(authAtom); 25 | 26 | useEffect(() => { 27 | if (parent.current) autoAnimate(parent.current); 28 | }, [parent]); 29 | 30 | useEffect(() => { 31 | const search = (router.query.q || 32 | router.query.s || 33 | router.query.search || 34 | router.query.query) as string; 35 | 36 | if (search) setSearch(search); 37 | }, [router.query]); 38 | 39 | useEffect(() => { 40 | if (categories.selectedCategoryId && search) setSearch(""); 41 | }, [categories, search, categories.selectedCategoryId]); 42 | 43 | const getFilteredItems = useMemo(() => { 44 | return items.items.filter((i) => 45 | i.name.toLowerCase().includes(search.toLowerCase()) 46 | ); 47 | }, [search, items.items]); 48 | 49 | const getFilteredCategories = useMemo(() => { 50 | return categories.categories.filter((c) => 51 | c.name.toLowerCase().includes(search.toLowerCase()) 52 | ); 53 | }, [search, categories.categories]); 54 | 55 | const getSelectedItem = useMemo(() => { 56 | if (!items.selectedItemId) return null; 57 | return items.items.find((c) => c.id === items.selectedItemId); 58 | }, [items.items, items.selectedItemId]); 59 | 60 | return ( 61 |
62 | 133 | 134 | 135 | {getSelectedItem?.id && ( 136 | 160 | i.id === getSelectedItem.categoryId) 166 | ?.name.toLowerCase()} 167 | /> 168 | 169 | )} 170 | 171 |
172 | ); 173 | } 174 | -------------------------------------------------------------------------------- /src/components/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import { TbMenu2, TbX, TbLogout, TbLogin } from "react-icons/tb"; 2 | import Image from "next/image"; 3 | import clsx from "clsx"; 4 | import Link from "next/link"; 5 | import { useEffect, useMemo, useRef, useState } from "react"; 6 | import autoAnimate from "@formkit/auto-animate"; 7 | import { auth, signInPopup } from "@/lib/firebase"; 8 | import { useAtom } from "jotai"; 9 | import { authAtom } from "@/store/auth"; 10 | import { toast } from "sonner"; 11 | 12 | const links = [ 13 | { 14 | label: "How to Recycle", 15 | href: "/how", 16 | }, 17 | { 18 | label: "Nearby Recycling Centers", 19 | href: "/map", 20 | }, 21 | { 22 | label: "Scoreboard", 23 | href: "/scoreboard", 24 | }, 25 | { 26 | label: "About", 27 | href: "/about", 28 | }, 29 | ]; 30 | 31 | export const Navbar = ({ fontFamily }: { fontFamily: string }) => { 32 | const [isOpen, setOpen] = useState(false); 33 | const [authCache] = useAtom(authAtom); 34 | const parent = useRef(null); 35 | 36 | const handleLogout = async () => { 37 | await auth.signOut(); 38 | toast.error("You have been logged out."); 39 | }; 40 | 41 | useEffect(() => { 42 | if (parent.current) autoAnimate(parent.current); 43 | }, [parent]); 44 | 45 | return ( 46 | 131 | ); 132 | }; 133 | 134 | const Links = ({ 135 | className, 136 | isAdmin, 137 | }: { 138 | className?: string; 139 | isAdmin?: boolean; 140 | }) => { 141 | const getLinks = useMemo(() => { 142 | if (isAdmin) return [...links, { label: "Admin", href: "/admin" }]; 143 | return links; 144 | }, [isAdmin]); 145 | 146 | return ( 147 |
153 | {getLinks.map((link) => ( 154 | 159 | {link.label} 160 | 161 | ))} 162 |
163 | ); 164 | }; 165 | 166 | const LoginButton = ({ 167 | signInPopup, 168 | className, 169 | }: { 170 | signInPopup: any; 171 | className?: string; 172 | }) => ( 173 | 186 | ); 187 | 188 | const NavbarUser = ({ 189 | type = "desktop", 190 | score, 191 | displayName, 192 | photoUrl, 193 | handleLogout, 194 | }: { 195 | type?: "desktop" | "mobile"; 196 | displayName: string | null; 197 | score?: number; 198 | photoUrl: string | null; 199 | handleLogout: any; 200 | }) => ( 201 |
209 |
210 | {Boolean(score) && ( 211 | 215 | {score} 216 | 217 | )} 218 | 219 | {displayName} 220 | 221 | {photoUrl && ( 222 | // eslint-disable-next-line @next/next/no-img-element 223 | User profile picture 232 | )} 233 |
234 | 235 | 246 |
247 | ); 248 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import type { AppProps } from "next/app"; 2 | import { Montserrat } from "next/font/google"; 3 | import Head from "next/head"; 4 | import { toast, Toaster } from "sonner"; 5 | import clsx from "clsx"; 6 | import { AnimatePresence, motion } from "framer-motion"; 7 | import { useAtom } from "jotai"; 8 | import { getCategories } from "@/lib/getCategories"; 9 | import { getItems } from "@/lib/getItems"; 10 | import { useCallback, useEffect, useState } from "react"; 11 | import { auth, updateScore, getCurrentUser } from "@/lib/firebase"; 12 | import { useRouter } from "next/router"; 13 | import { 14 | type DragEndEvent, 15 | DndContext, 16 | MouseSensor, 17 | TouchSensor, 18 | useSensor, 19 | useSensors, 20 | } from "@dnd-kit/core"; 21 | import { useSound } from "use-sound"; 22 | import { TbCheck } from "react-icons/tb"; 23 | import { User } from "firebase/auth"; 24 | import { getScores } from "@/lib/getScores"; 25 | 26 | // Store 27 | import { categoriesAtom } from "@/store/categories"; 28 | import { itemsAtom } from "@/store/items"; 29 | import { authAtom, IUser } from "@/store/auth"; 30 | import { scoresAtom } from "@/store/scores"; 31 | 32 | // Hooks 33 | import useDebounce from "@/hooks/useDebounce"; 34 | 35 | // Styles 36 | import "@/styles/tailwind.css"; 37 | 38 | // Components 39 | import { Navbar } from "@/components/Navbar"; 40 | import { RecycleBox } from "@/components/RecycleBox"; 41 | import { LoadingOverlay } from "@/components/LoadingOverlay"; 42 | import { WelcomeScreen } from "@/components/WelcomeScreen"; 43 | 44 | const montserrat = Montserrat({ 45 | subsets: ["latin"], 46 | }); 47 | 48 | export default function App({ Component, pageProps }: AppProps) { 49 | const [scoreCache, setScores] = useAtom(scoresAtom); 50 | const [category, setCategories] = useAtom(categoriesAtom); 51 | const [authCache, setAuth] = useAtom(authAtom); 52 | const [, setItems] = useAtom(itemsAtom); 53 | 54 | const [loading, setLoading] = useState(true); 55 | const [score, setScore] = useState(authCache.userDb?.score || 0); 56 | const [isActive, setActive] = useState(false); 57 | 58 | const debouncedScore = useDebounce(score, 1500); 59 | const { asPath, pathname } = useRouter(); 60 | 61 | const mouseSensor = useSensor(MouseSensor); 62 | const touchSensor = useSensor(TouchSensor); 63 | 64 | const sensors = useSensors(mouseSensor, touchSensor); 65 | 66 | const [play] = useSound("/drop.mp3", { 67 | volume: 0.5, 68 | }); 69 | 70 | /* Auth state change handlers */ 71 | const onAuthStateChanged = useCallback( 72 | async (user: User | null) => { 73 | if (user && authCache.user?.uid !== user.uid) { 74 | const userDetails = await getCurrentUser(); 75 | 76 | setScore(userDetails?.score || 0); 77 | 78 | setAuth({ 79 | isAdmin: userDetails?.isAdmin || false, 80 | userDb: userDetails, 81 | user, 82 | }); 83 | } else if (!user && authCache.user) { 84 | setScore(0); 85 | 86 | setAuth({ 87 | isAdmin: false, 88 | user: null, 89 | userDb: null, 90 | }); 91 | } 92 | }, 93 | [authCache.user, setAuth] 94 | ); 95 | 96 | const fetchInitialData = useCallback(async () => { 97 | try { 98 | const scores = await getScores(); 99 | const categories = await getCategories(); 100 | 101 | setScores(scores); 102 | setCategories((p) => ({ 103 | ...p, 104 | categories, 105 | })); 106 | } catch (err: any) { 107 | toast.error(err.message); 108 | setLoading(false); 109 | } finally { 110 | setLoading(false); 111 | } 112 | }, [setCategories, setScores]); 113 | 114 | useEffect(() => { 115 | const unsubscribe = auth.onAuthStateChanged(onAuthStateChanged); 116 | return () => unsubscribe(); 117 | }, [onAuthStateChanged]); 118 | 119 | /* Fetch categories & scores */ 120 | useEffect(() => { 121 | fetchInitialData(); 122 | }, [fetchInitialData]); 123 | 124 | /* Fetch categoryitems */ 125 | useEffect(() => { 126 | if (!category.selectedCategoryId) return; 127 | 128 | getItems(category.selectedCategoryId).then((items) => 129 | setItems((p) => ({ 130 | ...p, 131 | items, 132 | })) 133 | ); 134 | }, [category.selectedCategoryId, setItems]); 135 | 136 | /* Update score */ 137 | useEffect(() => { 138 | if (debouncedScore === 0 || authCache.userDb?.score === debouncedScore) 139 | return; 140 | 141 | updateScore(debouncedScore) 142 | .then(() => { 143 | setAuth((p) => ({ 144 | ...p, 145 | userDb: { 146 | ...p.userDb, 147 | score: debouncedScore, 148 | } as IUser, 149 | })); 150 | 151 | const userInScoreboard = scoreCache.find( 152 | (s) => s.uid === authCache.user?.uid 153 | ); 154 | 155 | if (userInScoreboard) { 156 | setScores((p) => 157 | p.map((s) => 158 | s.uid === authCache.user?.uid 159 | ? { ...s, score: debouncedScore } 160 | : s 161 | ) 162 | ); 163 | } else if (authCache.user) { 164 | setScores((p) => [ 165 | ...p, 166 | { 167 | uid: authCache.user?.uid, 168 | displayName: authCache.user?.displayName, 169 | avatar: authCache.user?.photoURL, 170 | score: debouncedScore, 171 | isAdmin: authCache.isAdmin, 172 | } as IUser, 173 | ]); 174 | } 175 | }) 176 | .catch(() => 177 | toast.error("Something went wrong while saving your score!") 178 | ); 179 | }, [ 180 | authCache.userDb?.score, 181 | authCache.user, 182 | authCache.isAdmin, 183 | scoreCache, 184 | setScores, 185 | debouncedScore, 186 | setAuth, 187 | ]); 188 | 189 | const handleDragEnd = (event: DragEndEvent) => { 190 | const { over, active } = event; 191 | setActive(false); 192 | 193 | if (over?.id === "recycle-box") { 194 | setScore((p) => p + 1); 195 | 196 | play(); 197 | 198 | toast.success( 199 | `You just recycled ${active.data.current?.item}! 🎉 ${ 200 | !authCache.user ? "Log in to save your progress." : "" 201 | }`, 202 | { 203 | icon: , 204 | } 205 | ); 206 | } 207 | }; 208 | 209 | return ( 210 | <> 211 | 212 | Importance of Recycling 213 | 217 | 218 | 219 | 220 | 221 | { 225 | setActive(true); 226 | }} 227 | > 228 | 229 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 259 | 260 | 261 | ); 262 | } 263 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Recycling Platform", 3 | "short_name": "Recycle", 4 | "description": "Uncover the unknown environmental damage of everyday items with our recycling awareness platform. Discover the power of recycling and help protect the planet.", 5 | "display": "fullscreen", 6 | "orientation": "portrait", 7 | "scope": "/", 8 | "start_url": "/", 9 | "theme_color": "#BF8D2C", 10 | "background_color": "#BF8D2C", 11 | "icons": [ 12 | { 13 | "src": "/icons/windows11/SmallTile.scale-100.png", 14 | "sizes": "71x71" 15 | }, 16 | { 17 | "src": "/icons/windows11/SmallTile.scale-125.png", 18 | "sizes": "89x89" 19 | }, 20 | { 21 | "src": "/icons/windows11/SmallTile.scale-150.png", 22 | "sizes": "107x107" 23 | }, 24 | { 25 | "src": "/icons/windows11/SmallTile.scale-200.png", 26 | "sizes": "142x142" 27 | }, 28 | { 29 | "src": "/icons/windows11/SmallTile.scale-400.png", 30 | "sizes": "284x284" 31 | }, 32 | { 33 | "src": "/icons/windows11/Square150x150Logo.scale-100.png", 34 | "sizes": "150x150" 35 | }, 36 | { 37 | "src": "/icons/windows11/Square150x150Logo.scale-125.png", 38 | "sizes": "188x188" 39 | }, 40 | { 41 | "src": "/icons/windows11/Square150x150Logo.scale-150.png", 42 | "sizes": "225x225" 43 | }, 44 | { 45 | "src": "/icons/windows11/Square150x150Logo.scale-200.png", 46 | "sizes": "300x300" 47 | }, 48 | { 49 | "src": "/icons/windows11/Square150x150Logo.scale-400.png", 50 | "sizes": "600x600" 51 | }, 52 | { 53 | "src": "/icons/windows11/Wide310x150Logo.scale-100.png", 54 | "sizes": "310x150" 55 | }, 56 | { 57 | "src": "/icons/windows11/Wide310x150Logo.scale-125.png", 58 | "sizes": "388x188" 59 | }, 60 | { 61 | "src": "/icons/windows11/Wide310x150Logo.scale-150.png", 62 | "sizes": "465x225" 63 | }, 64 | { 65 | "src": "/icons/windows11/Wide310x150Logo.scale-200.png", 66 | "sizes": "620x300" 67 | }, 68 | { 69 | "src": "/icons/windows11/Wide310x150Logo.scale-400.png", 70 | "sizes": "1240x600" 71 | }, 72 | { 73 | "src": "/icons/windows11/LargeTile.scale-100.png", 74 | "sizes": "310x310" 75 | }, 76 | { 77 | "src": "/icons/windows11/LargeTile.scale-125.png", 78 | "sizes": "388x388" 79 | }, 80 | { 81 | "src": "/icons/windows11/LargeTile.scale-150.png", 82 | "sizes": "465x465" 83 | }, 84 | { 85 | "src": "/icons/windows11/LargeTile.scale-200.png", 86 | "sizes": "620x620" 87 | }, 88 | { 89 | "src": "/icons/windows11/LargeTile.scale-400.png", 90 | "sizes": "1240x1240" 91 | }, 92 | { 93 | "src": "/icons/windows11/Square44x44Logo.scale-100.png", 94 | "sizes": "44x44" 95 | }, 96 | { 97 | "src": "/icons/windows11/Square44x44Logo.scale-125.png", 98 | "sizes": "55x55" 99 | }, 100 | { 101 | "src": "/icons/windows11/Square44x44Logo.scale-150.png", 102 | "sizes": "66x66" 103 | }, 104 | { 105 | "src": "/icons/windows11/Square44x44Logo.scale-200.png", 106 | "sizes": "88x88" 107 | }, 108 | { 109 | "src": "/icons/windows11/Square44x44Logo.scale-400.png", 110 | "sizes": "176x176" 111 | }, 112 | { 113 | "src": "/icons/windows11/StoreLogo.scale-100.png", 114 | "sizes": "50x50" 115 | }, 116 | { 117 | "src": "/icons/windows11/StoreLogo.scale-125.png", 118 | "sizes": "63x63" 119 | }, 120 | { 121 | "src": "/icons/windows11/StoreLogo.scale-150.png", 122 | "sizes": "75x75" 123 | }, 124 | { 125 | "src": "/icons/windows11/StoreLogo.scale-200.png", 126 | "sizes": "100x100" 127 | }, 128 | { 129 | "src": "/icons/windows11/StoreLogo.scale-400.png", 130 | "sizes": "200x200" 131 | }, 132 | { 133 | "src": "/icons/windows11/SplashScreen.scale-100.png", 134 | "sizes": "620x300" 135 | }, 136 | { 137 | "src": "/icons/windows11/SplashScreen.scale-125.png", 138 | "sizes": "775x375" 139 | }, 140 | { 141 | "src": "/icons/windows11/SplashScreen.scale-150.png", 142 | "sizes": "930x450" 143 | }, 144 | { 145 | "src": "/icons/windows11/SplashScreen.scale-200.png", 146 | "sizes": "1240x600" 147 | }, 148 | { 149 | "src": "/icons/windows11/SplashScreen.scale-400.png", 150 | "sizes": "2480x1200" 151 | }, 152 | { 153 | "src": "/icons/windows11/Square44x44Logo.targetsize-16.png", 154 | "sizes": "16x16" 155 | }, 156 | { 157 | "src": "/icons/windows11/Square44x44Logo.targetsize-20.png", 158 | "sizes": "20x20" 159 | }, 160 | { 161 | "src": "/icons/windows11/Square44x44Logo.targetsize-24.png", 162 | "sizes": "24x24" 163 | }, 164 | { 165 | "src": "/icons/windows11/Square44x44Logo.targetsize-30.png", 166 | "sizes": "30x30" 167 | }, 168 | { 169 | "src": "/icons/windows11/Square44x44Logo.targetsize-32.png", 170 | "sizes": "32x32" 171 | }, 172 | { 173 | "src": "/icons/windows11/Square44x44Logo.targetsize-36.png", 174 | "sizes": "36x36" 175 | }, 176 | { 177 | "src": "/icons/windows11/Square44x44Logo.targetsize-40.png", 178 | "sizes": "40x40" 179 | }, 180 | { 181 | "src": "/icons/windows11/Square44x44Logo.targetsize-44.png", 182 | "sizes": "44x44" 183 | }, 184 | { 185 | "src": "/icons/windows11/Square44x44Logo.targetsize-48.png", 186 | "sizes": "48x48" 187 | }, 188 | { 189 | "src": "/icons/windows11/Square44x44Logo.targetsize-60.png", 190 | "sizes": "60x60" 191 | }, 192 | { 193 | "src": "/icons/windows11/Square44x44Logo.targetsize-64.png", 194 | "sizes": "64x64" 195 | }, 196 | { 197 | "src": "/icons/windows11/Square44x44Logo.targetsize-72.png", 198 | "sizes": "72x72" 199 | }, 200 | { 201 | "src": "/icons/windows11/Square44x44Logo.targetsize-80.png", 202 | "sizes": "80x80" 203 | }, 204 | { 205 | "src": "/icons/windows11/Square44x44Logo.targetsize-96.png", 206 | "sizes": "96x96" 207 | }, 208 | { 209 | "src": "/icons/windows11/Square44x44Logo.targetsize-256.png", 210 | "sizes": "256x256" 211 | }, 212 | { 213 | "src": "/icons/windows11/Square44x44Logo.altform-unplated_targetsize-16.png", 214 | "sizes": "16x16" 215 | }, 216 | { 217 | "src": "/icons/windows11/Square44x44Logo.altform-unplated_targetsize-20.png", 218 | "sizes": "20x20" 219 | }, 220 | { 221 | "src": "/icons/windows11/Square44x44Logo.altform-unplated_targetsize-24.png", 222 | "sizes": "24x24" 223 | }, 224 | { 225 | "src": "/icons/windows11/Square44x44Logo.altform-unplated_targetsize-30.png", 226 | "sizes": "30x30" 227 | }, 228 | { 229 | "src": "/icons/windows11/Square44x44Logo.altform-unplated_targetsize-32.png", 230 | "sizes": "32x32" 231 | }, 232 | { 233 | "src": "/icons/windows11/Square44x44Logo.altform-unplated_targetsize-36.png", 234 | "sizes": "36x36" 235 | }, 236 | { 237 | "src": "/icons/windows11/Square44x44Logo.altform-unplated_targetsize-40.png", 238 | "sizes": "40x40" 239 | }, 240 | { 241 | "src": "/icons/windows11/Square44x44Logo.altform-unplated_targetsize-44.png", 242 | "sizes": "44x44" 243 | }, 244 | { 245 | "src": "/icons/windows11/Square44x44Logo.altform-unplated_targetsize-48.png", 246 | "sizes": "48x48" 247 | }, 248 | { 249 | "src": "/icons/windows11/Square44x44Logo.altform-unplated_targetsize-60.png", 250 | "sizes": "60x60" 251 | }, 252 | { 253 | "src": "/icons/windows11/Square44x44Logo.altform-unplated_targetsize-64.png", 254 | "sizes": "64x64" 255 | }, 256 | { 257 | "src": "/icons/windows11/Square44x44Logo.altform-unplated_targetsize-72.png", 258 | "sizes": "72x72" 259 | }, 260 | { 261 | "src": "/icons/windows11/Square44x44Logo.altform-unplated_targetsize-80.png", 262 | "sizes": "80x80" 263 | }, 264 | { 265 | "src": "/icons/windows11/Square44x44Logo.altform-unplated_targetsize-96.png", 266 | "sizes": "96x96" 267 | }, 268 | { 269 | "src": "/icons/windows11/Square44x44Logo.altform-unplated_targetsize-256.png", 270 | "sizes": "256x256" 271 | }, 272 | { 273 | "src": "/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-16.png", 274 | "sizes": "16x16" 275 | }, 276 | { 277 | "src": "/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-20.png", 278 | "sizes": "20x20" 279 | }, 280 | { 281 | "src": "/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-24.png", 282 | "sizes": "24x24" 283 | }, 284 | { 285 | "src": "/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-30.png", 286 | "sizes": "30x30" 287 | }, 288 | { 289 | "src": "/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-32.png", 290 | "sizes": "32x32" 291 | }, 292 | { 293 | "src": "/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-36.png", 294 | "sizes": "36x36" 295 | }, 296 | { 297 | "src": "/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-40.png", 298 | "sizes": "40x40" 299 | }, 300 | { 301 | "src": "/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-44.png", 302 | "sizes": "44x44" 303 | }, 304 | { 305 | "src": "/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-48.png", 306 | "sizes": "48x48" 307 | }, 308 | { 309 | "src": "/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-60.png", 310 | "sizes": "60x60" 311 | }, 312 | { 313 | "src": "/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-64.png", 314 | "sizes": "64x64" 315 | }, 316 | { 317 | "src": "/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-72.png", 318 | "sizes": "72x72" 319 | }, 320 | { 321 | "src": "/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-80.png", 322 | "sizes": "80x80" 323 | }, 324 | { 325 | "src": "/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-96.png", 326 | "sizes": "96x96" 327 | }, 328 | { 329 | "src": "/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-256.png", 330 | "sizes": "256x256" 331 | }, 332 | { 333 | "src": "/icons/android/android-launchericon-512-512.png", 334 | "sizes": "512x512" 335 | }, 336 | { 337 | "src": "/icons/android/android-launchericon-192-192.png", 338 | "sizes": "192x192" 339 | }, 340 | { 341 | "src": "/icons/android/android-launchericon-144-144.png", 342 | "sizes": "144x144" 343 | }, 344 | { 345 | "src": "/icons/android/android-launchericon-96-96.png", 346 | "sizes": "96x96" 347 | }, 348 | { 349 | "src": "/icons/android/android-launchericon-72-72.png", 350 | "sizes": "72x72" 351 | }, 352 | { 353 | "src": "/icons/android/android-launchericon-48-48.png", 354 | "sizes": "48x48" 355 | }, 356 | { 357 | "src": "/icons/ios/16.png", 358 | "sizes": "16x16" 359 | }, 360 | { 361 | "src": "/icons/ios/20.png", 362 | "sizes": "20x20" 363 | }, 364 | { 365 | "src": "/icons/ios/29.png", 366 | "sizes": "29x29" 367 | }, 368 | { 369 | "src": "/icons/ios/32.png", 370 | "sizes": "32x32" 371 | }, 372 | { 373 | "src": "/icons/ios/40.png", 374 | "sizes": "40x40" 375 | }, 376 | { 377 | "src": "/icons/ios/50.png", 378 | "sizes": "50x50" 379 | }, 380 | { 381 | "src": "/icons/ios/57.png", 382 | "sizes": "57x57" 383 | }, 384 | { 385 | "src": "/icons/ios/58.png", 386 | "sizes": "58x58" 387 | }, 388 | { 389 | "src": "/icons/ios/60.png", 390 | "sizes": "60x60" 391 | }, 392 | { 393 | "src": "/icons/ios/64.png", 394 | "sizes": "64x64" 395 | }, 396 | { 397 | "src": "/icons/ios/72.png", 398 | "sizes": "72x72" 399 | }, 400 | { 401 | "src": "/icons/ios/76.png", 402 | "sizes": "76x76" 403 | }, 404 | { 405 | "src": "/icons/ios/80.png", 406 | "sizes": "80x80" 407 | }, 408 | { 409 | "src": "/icons/ios/87.png", 410 | "sizes": "87x87" 411 | }, 412 | { 413 | "src": "/icons/ios/100.png", 414 | "sizes": "100x100" 415 | }, 416 | { 417 | "src": "/icons/ios/114.png", 418 | "sizes": "114x114" 419 | }, 420 | { 421 | "src": "/icons/ios/120.png", 422 | "sizes": "120x120" 423 | }, 424 | { 425 | "src": "/icons/ios/128.png", 426 | "sizes": "128x128" 427 | }, 428 | { 429 | "src": "/icons/ios/144.png", 430 | "sizes": "144x144" 431 | }, 432 | { 433 | "src": "/icons/ios/152.png", 434 | "sizes": "152x152" 435 | }, 436 | { 437 | "src": "/icons/ios/167.png", 438 | "sizes": "167x167" 439 | }, 440 | { 441 | "src": "/icons/ios/180.png", 442 | "sizes": "180x180" 443 | }, 444 | { 445 | "src": "/icons/ios/192.png", 446 | "sizes": "192x192" 447 | }, 448 | { 449 | "src": "/icons/ios/256.png", 450 | "sizes": "256x256" 451 | }, 452 | { 453 | "src": "/icons/ios/512.png", 454 | "sizes": "512x512" 455 | }, 456 | { 457 | "src": "/icons/ios/1024.png", 458 | "sizes": "1024x1024" 459 | } 460 | ] 461 | } 462 | -------------------------------------------------------------------------------- /src/pages/admin.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, useEffect, useMemo, useState } from "react"; 2 | import { 3 | TbCategory, 4 | TbSitemap, 5 | TbLogin, 6 | TbHome, 7 | TbDeviceFloppy, 8 | } from "react-icons/tb"; 9 | import { AnimatePresence } from "framer-motion"; 10 | import { useAtom } from "jotai"; 11 | import clsx from "clsx"; 12 | import { authAtom } from "@/store/auth"; 13 | import { NextRouter, useRouter } from "next/router"; 14 | import { firestore, signInPopup } from "@/lib/firebase"; 15 | import { addDoc, collection, doc, setDoc } from "firebase/firestore"; 16 | import { toast } from "sonner"; 17 | 18 | // Styles 19 | import "filepond/dist/filepond.min.css"; 20 | 21 | // Store 22 | import { categoriesAtom, ICategory } from "@/store/categories"; 23 | import { itemsAtom, IItem } from "@/store/items"; 24 | 25 | // Components 26 | import { Layout } from "@/components/Layout"; 27 | import { Select } from "@/components/Form/Select"; 28 | import { FileInput } from "@/components/Form/FileInput"; 29 | import { InputGroup } from "@/components/Form/InputGroup"; 30 | import { Input } from "@/components/Form/Input"; 31 | import { MotionFade } from "@/components/MotionFade"; 32 | 33 | // Types 34 | type Category = { 35 | image: string; 36 | name: string; 37 | }; 38 | 39 | type Item = { 40 | name: string; 41 | decomposeTime: string; 42 | image: string; 43 | categoryId: string; 44 | results?: string[]; 45 | benefits?: string[]; 46 | }; 47 | 48 | type ExtendExisting = Omit & U; 49 | 50 | interface IData { 51 | category: Category; 52 | item: Item; 53 | editCategory: ExtendExisting; 54 | editItem: ExtendExisting; 55 | } 56 | 57 | export default function Admin() { 58 | const [categories, setCategories] = useAtom(categoriesAtom); 59 | const [which, setWhich] = useState< 60 | "category" | "item" | "editCategory" | "editItem" 61 | >("category"); 62 | const [authCache] = useAtom(authAtom); 63 | const [items, setItems] = useAtom(itemsAtom); 64 | const [loading, setLoading] = useState(false); 65 | const [data, setData] = useState({} as IData); 66 | const router = useRouter(); 67 | 68 | useEffect(() => { 69 | if (!router.query.which) return; 70 | setWhich(router.query.which as "category" | "item"); 71 | }, [router.query.which]); 72 | 73 | const isDisabled = useMemo(() => { 74 | if (!data[which]) return true; 75 | 76 | if (which === "item" || which === "editItem") 77 | return !( 78 | data[which].name && 79 | data[which].decomposeTime && 80 | data[which].image && 81 | data[which].categoryId && 82 | (which === "editItem" ? data[which].id : true) 83 | ); 84 | else 85 | return !( 86 | data[which].name && 87 | data[which].image && 88 | (which === "editCategory" ? data[which].id : true) 89 | ); 90 | }, [data, which]); 91 | 92 | const handleSave = async () => { 93 | setLoading(true); 94 | 95 | const path = ["category", "editCategory"].includes(which) 96 | ? "categories" 97 | : "items"; 98 | 99 | const dataObject: { 100 | name: string; 101 | image: string; 102 | decomposeTime?: string; 103 | id?: string; 104 | categoryId?: string; 105 | results?: string[]; 106 | benefits?: string[]; 107 | } = { 108 | name: data[which].name, 109 | image: data[which].image, 110 | }; 111 | 112 | if (which === "item" || which === "editItem") { 113 | dataObject["decomposeTime"] = data[which].decomposeTime; 114 | dataObject["categoryId"] = data[which].categoryId; 115 | dataObject["benefits"] = data[which].benefits; 116 | dataObject["results"] = data[which].results; 117 | } 118 | 119 | if (which === "category" || which === "item") { 120 | await addDoc(collection(firestore, path), dataObject) 121 | .then((doc) => { 122 | toast.success(`Item/category added successfully!`); 123 | 124 | if (which === "category") 125 | setCategories((p) => ({ 126 | ...p, 127 | categories: [ 128 | ...p.categories, 129 | { id: doc.id, ...dataObject } as ICategory, 130 | ], 131 | })); 132 | else 133 | setItems((p) => ({ 134 | ...p, 135 | items: [...p.items, { id: doc.id, ...dataObject } as IItem], 136 | })); 137 | }) 138 | .catch((err) => { 139 | toast.error(err.message); 140 | setLoading(false); 141 | }); 142 | } else if (which === "editCategory" || which === "editItem") { 143 | await setDoc(doc(firestore, `${path}/${data[which].id}`), dataObject) 144 | .then(() => { 145 | if (which === "editCategory") { 146 | const cat = categories.categories.map((category) => { 147 | if (category.id === data[which].id) 148 | return { id: data[which].id, ...dataObject } as ICategory; 149 | else return category; 150 | }); 151 | 152 | setCategories((p) => ({ 153 | ...p, 154 | categories: cat, 155 | })); 156 | } else { 157 | const item = items.items.map((item) => { 158 | if (item.id === data[which].id) 159 | return { id: data[which].id, ...dataObject } as IItem; 160 | else return item; 161 | }); 162 | 163 | setItems((p) => ({ 164 | ...p, 165 | items: item, 166 | })); 167 | } 168 | 169 | toast.success(`Item/category edited successfully!`); 170 | }) 171 | .catch((err) => { 172 | toast.error(err.message); 173 | setLoading(false); 174 | }); 175 | } 176 | 177 | setLoading(false); 178 | }; 179 | 180 | return ( 181 | 191 | 192 | {which.includes("edit") ? "Update" : "Save"} 193 | 194 | } 195 | > 196 | 197 | {!authCache.isAdmin && } 198 | 199 | {authCache.isAdmin && ( 200 | 201 |
202 |
203 | 204 | Add a: 205 | 206 | 207 |
208 | } 212 | setWhich={setWhich} 213 | router={router} 214 | /> 215 | 216 | } 220 | setWhich={setWhich} 221 | router={router} 222 | /> 223 |
224 |
225 | 226 |
227 | 228 | Or: 229 | 230 | 231 |
232 | } 236 | setWhich={setWhich} 237 | router={router} 238 | /> 239 | 240 | } 244 | setWhich={setWhich} 245 | router={router} 246 | /> 247 |
248 |
249 |
250 | 251 |
252 |

Details

253 | 254 | 255 | {which === "category" && } 256 | {which === "item" && ( 257 | 261 | )} 262 | 263 | {which === "editCategory" && ( 264 | 269 | )} 270 | 271 | {which === "editItem" && ( 272 | 279 | )} 280 | 281 |
282 |
283 | )} 284 |
285 |
286 | ); 287 | } 288 | 289 | /* Overlay */ 290 | const NotAuthorized = () => { 291 | const router = useRouter(); 292 | 293 | return ( 294 | 295 | You don{"'"}t have access to this page 296 | 297 |
298 | 308 | 309 | 319 |
320 |
321 | ); 322 | }; 323 | 324 | const ChooseButton = ({ 325 | which, 326 | title, 327 | router, 328 | setWhich, 329 | icon, 330 | }: { 331 | which: string; 332 | title: string; 333 | router: NextRouter; 334 | setWhich: any; 335 | icon: ReactNode; 336 | }) => { 337 | const whichToSet = useMemo( 338 | () => 339 | title 340 | .toLowerCase() 341 | .split(" ") 342 | .map((i, index) => (index !== 0 ? i[0].toUpperCase() + i.slice(1) : i)) 343 | .join(""), 344 | [title] 345 | ); 346 | 347 | const handleClick = () => { 348 | router.push("/admin", { 349 | query: { 350 | which: whichToSet, 351 | }, 352 | }); 353 | 354 | setWhich(whichToSet); 355 | }; 356 | 357 | return ( 358 | 373 | ); 374 | }; 375 | 376 | /* Forms */ 377 | const CategoryForm = ({ 378 | setData, 379 | categories, 380 | edit = false, 381 | }: { 382 | setData: any; 383 | categories?: ICategory[]; 384 | edit?: boolean; 385 | }) => { 386 | const [name, setName] = useState(""); 387 | const [image, setImage] = useState(""); 388 | const [categoryId, setCategory] = useState(""); 389 | 390 | useEffect(() => { 391 | if (!edit) return; 392 | 393 | const category = categories?.find((i) => i.id === categoryId); 394 | if (!category) return; 395 | 396 | setName(category.name); 397 | setImage(category.image); 398 | }, [categoryId, edit, categories]); 399 | 400 | useEffect(() => { 401 | const keyToSet = edit ? "editCategory" : "category"; 402 | 403 | const object: ExtendExisting = { 404 | name, 405 | image, 406 | }; 407 | 408 | if (edit) object.id = categoryId; 409 | 410 | setData((p: any) => ({ 411 | ...p, 412 | [keyToSet]: object, 413 | })); 414 | }, [name, image, setData, edit, categoryId]); 415 | 416 | return ( 417 | 418 | {edit && ( 419 | 437 | 438 | 443 | 444 | ); 445 | }; 446 | 447 | const ItemForm = ({ 448 | setData, 449 | setCategories, 450 | categories, 451 | items, 452 | edit = false, 453 | }: { 454 | edit?: boolean; 455 | setCategories?: any; 456 | categories: ICategory[]; 457 | items?: IItem[]; 458 | setData: any; 459 | }) => { 460 | const [name, setName] = useState(""); 461 | const [categoryId, setCategory] = useState(""); 462 | const [itemId, setItem] = useState(""); 463 | const [decomposeTime, setDecompose] = useState(""); 464 | const [image, setImage] = useState(""); 465 | const [results, setResults] = useState([]); 466 | const [benefits, setBenefits] = useState([]); 467 | 468 | useEffect(() => { 469 | if (!edit) return; 470 | 471 | setCategories?.((p: any) => ({ 472 | ...p, 473 | selectedCategoryId: categoryId, 474 | })); 475 | }, [categoryId, edit, setCategories]); 476 | 477 | useEffect(() => { 478 | if (!edit) return; 479 | const item = items?.find((i) => i.id === itemId); 480 | 481 | if (!item) return; 482 | 483 | setName(item.name); 484 | setDecompose(item.decomposeTime); 485 | setImage(item.image); 486 | setResults(item.results || []); 487 | setBenefits(item.benefits || []); 488 | }, [itemId, items, edit]); 489 | 490 | useEffect(() => { 491 | const keyToSet = edit ? "editItem" : "item"; 492 | 493 | const object: ExtendExisting = { 494 | name, 495 | decomposeTime, 496 | image, 497 | categoryId, 498 | results, 499 | benefits, 500 | }; 501 | 502 | if (edit) object.id = itemId; 503 | 504 | setData((p: any) => ({ 505 | ...p, 506 | [keyToSet]: object, 507 | })); 508 | }, [ 509 | edit, 510 | itemId, 511 | name, 512 | categoryId, 513 | decomposeTime, 514 | image, 515 | results, 516 | benefits, 517 | setData, 518 | ]); 519 | 520 | return ( 521 | 522 |
523 | {edit && ( 524 | <> 525 | ({ 542 | label: c.name, 543 | value: c.id, 544 | }))} 545 | /> 546 | 547 | )} 548 | 549 | 555 | 556 | 562 | 563 | 568 | 569 | {!edit && ( 570 |