├── app ├── layouts │ ├── NavBar │ │ ├── index.ts │ │ └── NavBar.tsx │ └── DefaultLayout │ │ ├── index.ts │ │ └── DefaultLayout.tsx ├── components │ ├── SearchForm │ │ ├── index.tsx │ │ └── SearchForm.tsx │ ├── ThemeToggle │ │ ├── index.tsx │ │ └── ThemeToggle.tsx │ ├── ResourceCardGroup │ │ ├── index.ts │ │ └── ResourceCardGroup.tsx │ ├── ViewSourceGithub │ │ ├── index.ts │ │ └── ViewSourceGithub.tsx │ ├── SearchFilterSideBar │ │ ├── index.tsx │ │ └── SearchFilterSidebar.tsx │ └── Icons │ │ ├── index.ts │ │ ├── MoonIcon.tsx │ │ └── SunIcon.tsx ├── entry.client.tsx ├── types │ └── dbTypes.d.ts ├── utils │ ├── debug.ts │ ├── database │ │ ├── sqliteDB.ts │ │ ├── database.ts │ │ ├── index.ts │ │ └── supabaseDB.ts │ ├── gtag.client.ts │ ├── useMediaQuery.ts │ └── index.ts ├── entry.server.tsx ├── routes │ ├── about.tsx │ └── index.tsx ├── styles │ └── index.css └── root.tsx ├── .husky └── pre-commit ├── public ├── favicon.ico ├── icons │ ├── google.png │ ├── hp.com.png │ ├── nc.me.png │ ├── 21st.com.png │ ├── dell.com.png │ ├── levi.com.png │ ├── loom.com.png │ ├── miro.com.png │ ├── nike.com.png │ ├── proto.io.png │ ├── software.png │ ├── toms.com.png │ ├── wideo.co.png │ ├── wsj.com.png │ ├── 23andme.com.png │ ├── adobe.com.png │ ├── amazon.com.png │ ├── amtrak.com.png │ ├── bestbuy.com.png │ ├── canva-logo.png │ ├── chase.com.png │ ├── coder-logo.png │ ├── farmers.com.png │ ├── figma.com.png │ ├── framer.com.png │ ├── geico.com.png │ ├── gitpod.io.webp │ ├── joann.com.png │ ├── landbot.io.png │ ├── lenovo.com.png │ ├── mailgun.com.png │ ├── notion.com.png │ ├── nytimes.com.png │ ├── popsql.com.png │ ├── reebok.com.png │ ├── samsung.com.png │ ├── sentry.io.png │ ├── sketch.com.png │ ├── slack.com.png │ ├── sonos.com.png │ ├── spotify.com.png │ ├── staples.com.png │ ├── usbank.com.png │ ├── verizon.com.png │ ├── www.moo.com.png │ ├── xfinity.com.png │ ├── youtube.com.png │ ├── allstate.com.png │ ├── cinemark.com.png │ ├── coursera.org.png │ ├── datadoghq.com.png │ ├── economist.com.png │ ├── evernote.com.png │ ├── gettech-logo.jpeg │ ├── greyhound.com.png │ ├── iconscout.com.png │ ├── logitech.com.png │ ├── michaels.com.png │ ├── pdfexpert.com.png │ ├── regmovies.com.png │ ├── slice.design.png │ ├── spiritapp.io.png │ ├── surfshark.com.png │ ├── t-mobile.com.png │ ├── testmail.app.png │ ├── ticktick.com.png │ ├── whimsical.com.png │ ├── www.axure.com.png │ ├── www.drama.app.png │ ├── www.maxon.net.png │ ├── www.uxpin.com.png │ ├── amctheatres.com.png │ ├── digitalocean.com.png │ ├── footlocker.com.png │ ├── getcleanshot.com.png │ ├── getpixelsnap.com.png │ ├── help.trello.com.png │ ├── invisionapp.com.jpeg │ ├── officedepot.com.png │ ├── squarespace.com.png │ ├── thenorthface.com.png │ ├── wellsfargo.com.png │ ├── www.powtoon.com.png │ ├── alphauniverse.com.png │ ├── bankofamerica.com.png │ ├── barnesandnoble.com.png │ ├── bootstrapstudio.io.png │ ├── cyberghostvpn.com.png │ ├── help.marvelapp.com.png │ ├── teamtreehouse.com.png │ ├── www.termsfeed.com.png │ ├── github-README-banner.jpg │ ├── office.microsoft.com.png │ ├── www.amazingmarvin.com.png │ ├── www.surveymonkey.co.uk.png │ ├── privateinternetaccess.com.png │ ├── autodesk-logo.svg │ ├── at-and-t-logo.svg │ ├── google-logo.svg │ ├── apple-logo.svg │ ├── hulu-logo.svg │ ├── github-logo.svg │ ├── logo-buninux.svg │ ├── microsoft-office-logo.svg │ ├── adidas-logo.svg │ └── coder-logo.svg └── fonts │ └── Inter │ ├── Inter-Bold.ttf │ ├── Inter-Thin.ttf │ ├── Inter-Black.ttf │ ├── Inter-Light.ttf │ ├── Inter-Medium.ttf │ ├── Inter-ExtraBold.ttf │ ├── Inter-Regular.ttf │ ├── Inter-SemiBold.ttf │ └── Inter-ExtraLight.ttf ├── .prettierignore ├── remix.env.d.ts ├── vercel.json ├── .prettierrc ├── .env.example ├── scripts ├── lint.sh ├── pretty.sh └── check-volta-cli.sh ├── server.js ├── .github ├── config.yml ├── feature-request.yml └── bug_report.yml ├── .gitignore ├── .eslintrc.js ├── .vscode ├── settings.json └── launch.json ├── .editorconfig ├── tsconfig.json ├── remix.config.js ├── README.md ├── package.json └── docs ├── getting-started.md └── code-architecture.md /app/layouts/NavBar/index.ts: -------------------------------------------------------------------------------- 1 | export { default as NavBar } from "./NavBar"; 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /app/components/SearchForm/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as SearchForm } from "./SearchForm"; 2 | -------------------------------------------------------------------------------- /app/components/ThemeToggle/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as ThemeToggle } from "./ThemeToggle"; 2 | -------------------------------------------------------------------------------- /app/layouts/DefaultLayout/index.ts: -------------------------------------------------------------------------------- 1 | export { default as DefaultLayout } from "./DefaultLayout"; 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | .cache 3 | dist 4 | public 5 | pnpm-lock.yaml 6 | node_modules 7 | package.json 8 | -------------------------------------------------------------------------------- /app/components/ResourceCardGroup/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ResourceCardGroup } from "./ResourceCardGroup"; 2 | -------------------------------------------------------------------------------- /app/components/ViewSourceGithub/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ViewSourceGithub } from "./ViewSourceGithub"; 2 | -------------------------------------------------------------------------------- /public/icons/google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/google.png -------------------------------------------------------------------------------- /public/icons/hp.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/hp.com.png -------------------------------------------------------------------------------- /public/icons/nc.me.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/nc.me.png -------------------------------------------------------------------------------- /remix.env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /public/icons/21st.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/21st.com.png -------------------------------------------------------------------------------- /public/icons/dell.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/dell.com.png -------------------------------------------------------------------------------- /public/icons/levi.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/levi.com.png -------------------------------------------------------------------------------- /public/icons/loom.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/loom.com.png -------------------------------------------------------------------------------- /public/icons/miro.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/miro.com.png -------------------------------------------------------------------------------- /public/icons/nike.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/nike.com.png -------------------------------------------------------------------------------- /public/icons/proto.io.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/proto.io.png -------------------------------------------------------------------------------- /public/icons/software.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/software.png -------------------------------------------------------------------------------- /public/icons/toms.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/toms.com.png -------------------------------------------------------------------------------- /public/icons/wideo.co.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/wideo.co.png -------------------------------------------------------------------------------- /public/icons/wsj.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/wsj.com.png -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | "env": { 4 | "ENABLE_FILE_SYSTEM_API": "1" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /app/components/SearchFilterSideBar/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as SearchFilterSideBar } from "./SearchFilterSidebar"; 2 | -------------------------------------------------------------------------------- /public/icons/23andme.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/23andme.com.png -------------------------------------------------------------------------------- /public/icons/adobe.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/adobe.com.png -------------------------------------------------------------------------------- /public/icons/amazon.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/amazon.com.png -------------------------------------------------------------------------------- /public/icons/amtrak.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/amtrak.com.png -------------------------------------------------------------------------------- /public/icons/bestbuy.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/bestbuy.com.png -------------------------------------------------------------------------------- /public/icons/canva-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/canva-logo.png -------------------------------------------------------------------------------- /public/icons/chase.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/chase.com.png -------------------------------------------------------------------------------- /public/icons/coder-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/coder-logo.png -------------------------------------------------------------------------------- /public/icons/farmers.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/farmers.com.png -------------------------------------------------------------------------------- /public/icons/figma.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/figma.com.png -------------------------------------------------------------------------------- /public/icons/framer.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/framer.com.png -------------------------------------------------------------------------------- /public/icons/geico.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/geico.com.png -------------------------------------------------------------------------------- /public/icons/gitpod.io.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/gitpod.io.webp -------------------------------------------------------------------------------- /public/icons/joann.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/joann.com.png -------------------------------------------------------------------------------- /public/icons/landbot.io.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/landbot.io.png -------------------------------------------------------------------------------- /public/icons/lenovo.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/lenovo.com.png -------------------------------------------------------------------------------- /public/icons/mailgun.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/mailgun.com.png -------------------------------------------------------------------------------- /public/icons/notion.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/notion.com.png -------------------------------------------------------------------------------- /public/icons/nytimes.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/nytimes.com.png -------------------------------------------------------------------------------- /public/icons/popsql.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/popsql.com.png -------------------------------------------------------------------------------- /public/icons/reebok.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/reebok.com.png -------------------------------------------------------------------------------- /public/icons/samsung.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/samsung.com.png -------------------------------------------------------------------------------- /public/icons/sentry.io.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/sentry.io.png -------------------------------------------------------------------------------- /public/icons/sketch.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/sketch.com.png -------------------------------------------------------------------------------- /public/icons/slack.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/slack.com.png -------------------------------------------------------------------------------- /public/icons/sonos.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/sonos.com.png -------------------------------------------------------------------------------- /public/icons/spotify.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/spotify.com.png -------------------------------------------------------------------------------- /public/icons/staples.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/staples.com.png -------------------------------------------------------------------------------- /public/icons/usbank.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/usbank.com.png -------------------------------------------------------------------------------- /public/icons/verizon.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/verizon.com.png -------------------------------------------------------------------------------- /public/icons/www.moo.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/www.moo.com.png -------------------------------------------------------------------------------- /public/icons/xfinity.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/xfinity.com.png -------------------------------------------------------------------------------- /public/icons/youtube.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/youtube.com.png -------------------------------------------------------------------------------- /public/icons/allstate.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/allstate.com.png -------------------------------------------------------------------------------- /public/icons/cinemark.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/cinemark.com.png -------------------------------------------------------------------------------- /public/icons/coursera.org.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/coursera.org.png -------------------------------------------------------------------------------- /public/icons/datadoghq.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/datadoghq.com.png -------------------------------------------------------------------------------- /public/icons/economist.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/economist.com.png -------------------------------------------------------------------------------- /public/icons/evernote.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/evernote.com.png -------------------------------------------------------------------------------- /public/icons/gettech-logo.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/gettech-logo.jpeg -------------------------------------------------------------------------------- /public/icons/greyhound.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/greyhound.com.png -------------------------------------------------------------------------------- /public/icons/iconscout.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/iconscout.com.png -------------------------------------------------------------------------------- /public/icons/logitech.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/logitech.com.png -------------------------------------------------------------------------------- /public/icons/michaels.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/michaels.com.png -------------------------------------------------------------------------------- /public/icons/pdfexpert.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/pdfexpert.com.png -------------------------------------------------------------------------------- /public/icons/regmovies.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/regmovies.com.png -------------------------------------------------------------------------------- /public/icons/slice.design.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/slice.design.png -------------------------------------------------------------------------------- /public/icons/spiritapp.io.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/spiritapp.io.png -------------------------------------------------------------------------------- /public/icons/surfshark.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/surfshark.com.png -------------------------------------------------------------------------------- /public/icons/t-mobile.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/t-mobile.com.png -------------------------------------------------------------------------------- /public/icons/testmail.app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/testmail.app.png -------------------------------------------------------------------------------- /public/icons/ticktick.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/ticktick.com.png -------------------------------------------------------------------------------- /public/icons/whimsical.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/whimsical.com.png -------------------------------------------------------------------------------- /public/icons/www.axure.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/www.axure.com.png -------------------------------------------------------------------------------- /public/icons/www.drama.app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/www.drama.app.png -------------------------------------------------------------------------------- /public/icons/www.maxon.net.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/www.maxon.net.png -------------------------------------------------------------------------------- /public/icons/www.uxpin.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/www.uxpin.com.png -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "doubleQuote": true, 4 | "trailingComma": "all", 5 | "endOfLine": "auto" 6 | } 7 | -------------------------------------------------------------------------------- /public/fonts/Inter/Inter-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/fonts/Inter/Inter-Bold.ttf -------------------------------------------------------------------------------- /public/fonts/Inter/Inter-Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/fonts/Inter/Inter-Thin.ttf -------------------------------------------------------------------------------- /public/icons/amctheatres.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/amctheatres.com.png -------------------------------------------------------------------------------- /public/icons/digitalocean.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/digitalocean.com.png -------------------------------------------------------------------------------- /public/icons/footlocker.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/footlocker.com.png -------------------------------------------------------------------------------- /public/icons/getcleanshot.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/getcleanshot.com.png -------------------------------------------------------------------------------- /public/icons/getpixelsnap.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/getpixelsnap.com.png -------------------------------------------------------------------------------- /public/icons/help.trello.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/help.trello.com.png -------------------------------------------------------------------------------- /public/icons/invisionapp.com.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/invisionapp.com.jpeg -------------------------------------------------------------------------------- /public/icons/officedepot.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/officedepot.com.png -------------------------------------------------------------------------------- /public/icons/squarespace.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/squarespace.com.png -------------------------------------------------------------------------------- /public/icons/thenorthface.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/thenorthface.com.png -------------------------------------------------------------------------------- /public/icons/wellsfargo.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/wellsfargo.com.png -------------------------------------------------------------------------------- /public/icons/www.powtoon.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/www.powtoon.com.png -------------------------------------------------------------------------------- /public/fonts/Inter/Inter-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/fonts/Inter/Inter-Black.ttf -------------------------------------------------------------------------------- /public/fonts/Inter/Inter-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/fonts/Inter/Inter-Light.ttf -------------------------------------------------------------------------------- /public/fonts/Inter/Inter-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/fonts/Inter/Inter-Medium.ttf -------------------------------------------------------------------------------- /public/icons/alphauniverse.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/alphauniverse.com.png -------------------------------------------------------------------------------- /public/icons/bankofamerica.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/bankofamerica.com.png -------------------------------------------------------------------------------- /public/icons/barnesandnoble.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/barnesandnoble.com.png -------------------------------------------------------------------------------- /public/icons/bootstrapstudio.io.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/bootstrapstudio.io.png -------------------------------------------------------------------------------- /public/icons/cyberghostvpn.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/cyberghostvpn.com.png -------------------------------------------------------------------------------- /public/icons/help.marvelapp.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/help.marvelapp.com.png -------------------------------------------------------------------------------- /public/icons/teamtreehouse.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/teamtreehouse.com.png -------------------------------------------------------------------------------- /public/icons/www.termsfeed.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/www.termsfeed.com.png -------------------------------------------------------------------------------- /public/fonts/Inter/Inter-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/fonts/Inter/Inter-ExtraBold.ttf -------------------------------------------------------------------------------- /public/fonts/Inter/Inter-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/fonts/Inter/Inter-Regular.ttf -------------------------------------------------------------------------------- /public/fonts/Inter/Inter-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/fonts/Inter/Inter-SemiBold.ttf -------------------------------------------------------------------------------- /public/icons/github-README-banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/github-README-banner.jpg -------------------------------------------------------------------------------- /public/icons/office.microsoft.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/office.microsoft.com.png -------------------------------------------------------------------------------- /public/icons/www.amazingmarvin.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/www.amazingmarvin.com.png -------------------------------------------------------------------------------- /public/fonts/Inter/Inter-ExtraLight.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/fonts/Inter/Inter-ExtraLight.ttf -------------------------------------------------------------------------------- /public/icons/www.surveymonkey.co.uk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/www.surveymonkey.co.uk.png -------------------------------------------------------------------------------- /app/entry.client.tsx: -------------------------------------------------------------------------------- 1 | import { hydrate } from "react-dom"; 2 | import { RemixBrowser } from "remix"; 3 | 4 | hydrate(, document); 5 | -------------------------------------------------------------------------------- /public/icons/privateinternetaccess.com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cliffordfajardo/micro-discounts/HEAD/public/icons/privateinternetaccess.com.png -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | SUPABASE_SERVICE_KEY="" 2 | SUPABASE_URL="" 3 | DB_REFRESH_INTERVAL_MS= -------------------------------------------------------------------------------- /scripts/lint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "\n-----------------Running: lint.sh---------------------" 3 | npm run lint; 4 | echo "-----------------Finished: lint.sh---------------------\n" 5 | -------------------------------------------------------------------------------- /scripts/pretty.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "\n-----------------Running: pretty.sh---------------------" 3 | npm run pretty; 4 | echo "-----------------Finished: pretty.sh---------------------\n" 5 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | import { createRequestHandler } from "@remix-run/vercel"; 2 | import * as build from "@remix-run/dev/server-build"; 3 | 4 | export default createRequestHandler({ build, mode: process.env.NODE_ENV }); 5 | -------------------------------------------------------------------------------- /.github/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Feature Requests & Questions 4 | url: https://github.com/TODO/TODO/discussions 5 | about: Please ask and answer questions here. 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | .cache 4 | .env 5 | .vercel 6 | .output 7 | 8 | /build/ 9 | /public/build 10 | /api/index.js 11 | 12 | # Local dev for Dom 13 | yarn.lock 14 | 15 | NOTES.md 16 | SCRATCHPAD* 17 | 18 | seed-db 19 | temp 20 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | "@remix-run/eslint-config", 4 | // "@remix-run/eslint-config/jest-testing-library", 5 | ], 6 | ignorePatterns: ["./node_modules", "./**/node_modules/**", "./build", "./cache", "./public/build", "./public"], 7 | }; 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.detectIndentation": false, 3 | "editor.tabSize": 2, 4 | "files.eol": "\n", 5 | "editor.codeActionsOnSave": { 6 | "source.fixAll.eslint": true 7 | }, 8 | "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"] 9 | } 10 | -------------------------------------------------------------------------------- /app/types/dbTypes.d.ts: -------------------------------------------------------------------------------- 1 | export interface ResourceTable { 2 | id: number; 3 | title?: string; 4 | description?: string; 5 | category?: string[]; 6 | tags?: string[]; 7 | domain?: string; 8 | url?: string; 9 | tfa?: string[]; 10 | } 11 | 12 | export type DiscountTagTable = { 13 | id: number; 14 | inserted_at: string; 15 | tag: string; 16 | }; 17 | -------------------------------------------------------------------------------- /app/utils/debug.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 3 | * Simple function that's only purpose it to help with debugging in VSCode. 4 | * 5 | * @example 6 | * Typical use case: 7 | * 1. put a break point inside the body of this function 8 | * 2. call `debug()` where you want to catch the debug in your code 9 | */ 10 | export function debug() { 11 | return; 12 | } 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # Go to https://editorconfig.org/ to download the plugin for your editor. 4 | 5 | root = true 6 | 7 | [*] 8 | charset = utf-8 9 | indent_style = space 10 | indent_size = 2 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | 14 | # Markdown files 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /app/components/Icons/index.ts: -------------------------------------------------------------------------------- 1 | import { styled, CSS } from "@nextui-org/react"; 2 | 3 | export interface IconProps { 4 | fill?: string; 5 | filled?: boolean; 6 | size?: string | number; 7 | height?: string | number; 8 | width?: string | number; 9 | label?: string; 10 | onClick?: () => void; 11 | className?: string; 12 | css?: CSS; 13 | } 14 | 15 | export { default as MoonIcon } from "./MoonIcon"; 16 | export { default as SunIcon } from "./SunIcon"; 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"], 3 | "compilerOptions": { 4 | "lib": ["DOM", "DOM.Iterable", "ES2019"], 5 | "isolatedModules": true, 6 | "esModuleInterop": true, 7 | "jsx": "react-jsx", 8 | "moduleResolution": "node", 9 | "resolveJsonModule": true, 10 | "target": "ES2019", 11 | "strict": true, 12 | "baseUrl": ".", 13 | "paths": { 14 | "~/*": ["./app/*"] 15 | }, 16 | 17 | // Remix takes care of building everything in `remix build`. 18 | "noEmit": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/entry.server.tsx: -------------------------------------------------------------------------------- 1 | import { renderToString } from "react-dom/server"; 2 | import { RemixServer } from "remix"; 3 | import type { EntryContext } from "remix"; 4 | 5 | export default function handleRequest( 6 | request: Request, 7 | responseStatusCode: number, 8 | responseHeaders: Headers, 9 | remixContext: EntryContext, 10 | ) { 11 | const markup = renderToString(); 12 | 13 | responseHeaders.set("Content-Type", "text/html"); 14 | 15 | return new Response("" + markup, { 16 | status: responseStatusCode, 17 | headers: responseHeaders, 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /app/utils/database/sqliteDB.ts: -------------------------------------------------------------------------------- 1 | // TODO install sqlite 2 | import { ResourceTable } from "~/types/dbTypes"; 3 | 4 | if (!process.env.IS_SQLITE_ENABLED) { 5 | console.error( 6 | `Please set SUPABASE_URL and SUPABASE_SERVICE_KEY environment variables.\n` + 7 | `Please check that you have .env file created with the aforementioned supbase credentials.`, 8 | ); 9 | process.exit(1); 10 | } 11 | 12 | export class SQLiteDB { 13 | private db: any; 14 | constructor() {} 15 | 16 | public async setup(): Promise { 17 | // TODO 18 | } 19 | 20 | public async fetchAllResources(): Promise { 21 | // TODO 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/layouts/DefaultLayout/DefaultLayout.tsx: -------------------------------------------------------------------------------- 1 | import { Container } from "@nextui-org/react"; 2 | import React from "react"; 3 | import { ViewSourceGithub } from "~/components/ViewSourceGithub"; 4 | import { NavBar } from "../NavBar"; 5 | 6 | type Props = { 7 | title?: string; 8 | submitForm?: () => void; 9 | }; 10 | 11 | const DefaultLayout: React.FC = ({ title = "About | Microdiscounts.website", children, submitForm }) => { 12 | return ( 13 | <> 14 | 15 | void} /> 16 | {children} 17 | 18 | ); 19 | }; 20 | export default DefaultLayout; 21 | -------------------------------------------------------------------------------- /app/utils/database/database.ts: -------------------------------------------------------------------------------- 1 | import { PostgrestResponse } from "@supabase/supabase-js"; 2 | import { ResourceTable } from "~/types/dbTypes"; 3 | 4 | export interface SQLPreparedStatement { 5 | get(...params: any[]): Promise; 6 | all(...params: any[]): Promise; 7 | } 8 | 9 | export abstract class SQLDatabase { 10 | /** 11 | * Asynchronously create a new database connection 12 | */ 13 | public async setup(): Promise { 14 | return Promise.reject("Not yet implemented"); 15 | } 16 | 17 | public async fetchAllResourcesCached(): Promise { 18 | return Promise.reject("Not yet implemented"); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /remix.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('@remix-run/dev').AppConfig} 3 | */ 4 | module.exports = { 5 | serverBuildTarget: "vercel", 6 | // When running locally in development mode, we use the built in remix 7 | // server. This does not understand the vercel lambda module format, 8 | // so we default back to the standard build output. 9 | server: process.env.NODE_ENV === "development" ? undefined : "./server.js", 10 | ignoredRouteFiles: [".*"], 11 | // Why is the `appDirectory` 12 | 13 | // appDirectory: "app", 14 | // assetsBuildDirectory: "public/build", 15 | // serverBuildPath: "api/index.js", 16 | // publicPath: "/build/", 17 | // devServerPort: 8002 18 | }; 19 | -------------------------------------------------------------------------------- /app/utils/gtag.client.ts: -------------------------------------------------------------------------------- 1 | export const GA_TRACKING_ID = "G-27NWG851Z9"; 2 | 3 | declare global { 4 | interface Window { 5 | gtag: (option: string, gaTrackingId: string, options: Record) => void; 6 | } 7 | } 8 | 9 | // https://developers.google.com/analytics/devguides/collection/gtagjs/pages 10 | export const pageview = (url: string) => { 11 | window.gtag("config", GA_TRACKING_ID, { 12 | page_path: url, 13 | // debug_mode: true, 14 | }); 15 | }; 16 | 17 | // https://developers.google.com/analytics/devguides/collection/gtagjs/events 18 | export const event = ({ action, category, label, value }: Record) => { 19 | console.log("logging event event", { action, category, label, value }); 20 | window.gtag("event", action, { 21 | event_category: category, 22 | event_label: label, 23 | value: value, 24 | }); 25 | }; 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MicroDiscounts 2 | 3 | 4 | 5 | [![PRs Welcome][prs-badge]][prs] 6 | 7 | MicroDiscount - The world's largest directory of tech & software discounts for students, teachers & hobbyists. 8 | 9 | 10 | 11 | ## Technology Stack 12 | 13 | - [Remix Run](https://remix.run/) 14 | - [Vercel](https://vercel.com) 15 | - [NextUI](https://nextui.org/) 16 | 17 | ## Contributing and Development 18 | 19 | Instructions coming soon 👀 20 | 21 | ## Credits 22 | 23 | - [Clifford](https://twitter.com/cliffordfajard0) 24 | - [Dom](https://twitter.com/domnguyen5653) 25 | - [Rob](https://twitter.com/_robcerda) 26 | 27 | [license]: https://github.com/cliffordfajardo/micro-discount/cross-env/blob/master/other/LICENSE 28 | [prs]: http://makeapullrequest.com 29 | [prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square 30 | -------------------------------------------------------------------------------- /public/icons/autodesk-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /scripts/check-volta-cli.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "\n-----------------Running: check-volta-cli.sh---------------------" 3 | echo "[DEV_SETUP]: 🔎 checking if volta CLI binary is installed on your computer.... (More info: https://volta.sh/)" 4 | 5 | 6 | if volta -v > /dev/null 2>&1; then 7 | # Check for the existence of the binary. redirect output to stderror. Without this, this `2>&1` the command may fail on linux distros in CI environments (vercel etc) 8 | # https://stackoverflow.com/a/818265/2971795 9 | echo "[DEV_SETUP]: ✅ 'volta' binary exists on your computer"; 10 | else 11 | echo "[DEV_SETUP]: 🌕 Could not find "volta" binary installed on your computer. Volta is used to automagically install or update node versions for you."; 12 | echo "[DEV_SETUP]: ⬇️ Downloading volta ...."; 13 | curl https://get.volta.sh | bash; 14 | echo "[DEV_SETUP]: Finished downloading volta CLI"; 15 | fi 16 | 17 | echo "[DEV_SETUP]: See https://volta.sh/ for more details on volta"; 18 | echo "-----------------Finished: check-volta-cli.sh---------------------\n" 19 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | 7 | "configurations": [ 8 | { 9 | "command": "npm run dev", 10 | "name": "Run npm run dev", 11 | "request": "launch", 12 | "type": "node-terminal", 13 | "cwd": "${workspaceFolder}" 14 | }, 15 | // TODO: remix CLI option to install VS Code template. Maybe link to Kiliman 16 | // If you are using any of the other providers (Vercel, Express etc), you will us this config below. 17 | // The way it works: 18 | // - run the dev server 19 | // - run the vscode debug command and attach the port that the app is running on. 20 | { 21 | "name": "Attach by Process ID", 22 | "processId": "${command:PickProcess}", 23 | "request": "attach", 24 | "skipFiles": ["/**"], 25 | "type": "pwa-node" 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /app/utils/useMediaQuery.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | //https://usehooks-ts.com/react-hook/use-media-query 3 | function useMediaQuery(query: string): boolean { 4 | const getMatches = (query: string): boolean => { 5 | // Prevents SSR issues 6 | if (typeof window !== "undefined") { 7 | return window.matchMedia(query).matches; 8 | } 9 | return false; 10 | }; 11 | 12 | const [matches, setMatches] = useState(getMatches(query)); 13 | 14 | function handleChange() { 15 | setMatches(getMatches(query)); 16 | } 17 | 18 | useEffect(() => { 19 | const matchMedia = window.matchMedia(query); 20 | 21 | // Triggered at the first client-side load and if query changes 22 | handleChange(); 23 | 24 | // Listen matchMedia 25 | matchMedia.addEventListener("change", handleChange); 26 | 27 | return () => { 28 | matchMedia.removeEventListener("change", handleChange); 29 | }; 30 | // eslint-disable-next-line react-hooks/exhaustive-deps 31 | }, [query]); 32 | 33 | return matches; 34 | } 35 | 36 | export default useMediaQuery; 37 | -------------------------------------------------------------------------------- /public/icons/at-and-t-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/icons/google-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/components/ThemeToggle/ThemeToggle.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { CSS, styled, useTheme } from "@nextui-org/react"; 3 | import { MoonIcon, SunIcon } from "~/components/Icons"; 4 | import useDarkMode from "use-dark-mode"; 5 | 6 | interface Props { 7 | className?: string; 8 | css?: CSS; 9 | } 10 | 11 | // @ts-ignore 12 | const StyledButton = styled("button", { 13 | dflex: "center", 14 | size: "auto", 15 | cursor: "pointer", 16 | background: "transparent", 17 | border: "none", 18 | padding: 0, 19 | "& .theme-selector-icon": { 20 | color: "$colors$headerIconColor", 21 | }, 22 | "@xsMax": { 23 | px: "$2", 24 | }, 25 | }); 26 | 27 | export const ThemeToggle: React.FC = ({ className = "", css }) => { 28 | const darkMode = useDarkMode(); 29 | const { isDark } = useTheme(); 30 | 31 | const handleToggleTheme = React.useCallback(() => { 32 | darkMode.toggle(); 33 | }, [darkMode]); 34 | 35 | return ( 36 | 42 | {isDark ? ( 43 | 44 | ) : ( 45 | 46 | )} 47 | 48 | ); 49 | }; 50 | 51 | export default ThemeToggle; 52 | -------------------------------------------------------------------------------- /public/icons/apple-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /public/icons/hulu-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/components/Icons/MoonIcon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { IconProps } from "./index"; 3 | 4 | const Moon: React.FC = ({ fill = "currentColor", filled, size, height, width, label, ...props }) => { 5 | if (filled) { 6 | return ( 7 | 8 | 12 | 13 | ); 14 | } 15 | return ( 16 | 23 | 27 | 28 | ); 29 | }; 30 | 31 | export default Moon; 32 | -------------------------------------------------------------------------------- /app/components/Icons/SunIcon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { IconProps } from "./index"; 3 | 4 | const Moon: React.FC = ({ fill = "currentColor", filled, size, height, width, label, ...props }) => { 5 | if (filled) { 6 | return ( 7 | 8 | 12 | 13 | ); 14 | } 15 | return ( 16 | 23 | 27 | 28 | ); 29 | }; 30 | 31 | export default Moon; 32 | -------------------------------------------------------------------------------- /app/utils/database/index.ts: -------------------------------------------------------------------------------- 1 | import { SupabaseDB } from "./supabaseDB"; 2 | // import { SQLiteDB } from "./sqliteDB"; 3 | import { type SQLDatabase } from "./database"; 4 | 5 | type DB_Type = "supabase" | "sqlite"; 6 | 7 | /** 8 | * @description 9 | * Determine which database to run given the environment variable. Ex: Supbabase or sqlite 10 | */ 11 | export function determineDbType(): DB_Type { 12 | const databaseType = (process.env.DB_TYPE || "").trim().toLowerCase(); 13 | switch (databaseType) { 14 | case "supabase": { 15 | console.log("Database Type: supabase"); 16 | return "supabase"; 17 | } 18 | case "sqlite": { 19 | console.log("Database Type: sqlite"); 20 | return "sqlite"; 21 | } 22 | default: 23 | console.log("Database Type: supabase"); 24 | return "supabase"; 25 | } 26 | } 27 | export const DB_TYPE = determineDbType(); 28 | 29 | /** 30 | * @description 31 | * Get a database connection 32 | * 33 | * Use the DB_TYPE environment variable to switch between 34 | * SQLite or supabase database connections. Define 35 | * connection details in the ./database.json 36 | */ 37 | export async function getDb(): Promise { 38 | const supabaseDB = new SupabaseDB(); 39 | await supabaseDB.setup(); 40 | return supabaseDB; 41 | // if (DB_TYPE === "supabase") { 42 | // console.log("USING THE FOLLOWING DB: supabase"); 43 | // const supabaseDB = new SupabaseDB(); 44 | // const DB = await supabaseDB.setup() 45 | // return DB; 46 | // } else { 47 | // console.log("[utils.js] USING THE FOLLOWING DB: SQLITE"); 48 | // const sqliteDB = new SQLiteDB(); 49 | // // return sqliteDB; 50 | // } 51 | } 52 | 53 | let db: SQLDatabase | null = null; 54 | export async function getDbInstance(): Promise { 55 | if (!db) { 56 | db = await getDb(); 57 | } 58 | return db; 59 | } 60 | -------------------------------------------------------------------------------- /public/icons/github-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /app/components/ViewSourceGithub/ViewSourceGithub.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-no-target-blank */ 2 | import useMediaQuery from "~/utils/useMediaQuery"; 3 | 4 | /** 5 | * @description 6 | * Small Icon on top left showing github icon. 7 | * When clicked it will direct user to repo. 8 | */ 9 | const ViewSourceGithub = () => { 10 | const isMobile = useMediaQuery("(max-width: 1200px)"); 11 | 12 | return ( 13 | <> 14 | 20 | 27 | 28 | 32 | 36 | 37 | 38 | 39 | ); 40 | }; 41 | export default ViewSourceGithub; 42 | -------------------------------------------------------------------------------- /app/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { type ResourceTable } from "~/types/dbTypes"; 2 | 3 | export * from "./debug"; 4 | export * from "./database"; 5 | // TODO export supabase database class 6 | 7 | // ************* CONSTANTS************************ 8 | /** 9 | * @description 10 | * This type keeps track of all the names of the forms in the app. 11 | * This is useful for scenarios like: 12 | * - associating a form field with a specific form in the app. 13 | * 14 | * @example 15 | * See `SearchForm.tsx` 16 | * type SUPPORTED_FORM_IDS = 'search-form' | 'new-form-name-here' ... 17 | */ 18 | export type SUPPORTED_FORM_IDS = 19 | // this form is used on the homepage for displaying the list of educational discount items 20 | "search-form"; 21 | 22 | type filterDBItemsFunction = ( 23 | data: ResourceTable[], 24 | queryParams: { category: string[]; searchTerm: string; tags: string[] }, 25 | ) => ResourceTable[]; 26 | 27 | export const filterDBItems: filterDBItemsFunction = (data = [], { category = [], searchTerm = "", tags = [] }) => { 28 | if (data.length <= 1) return data; 29 | 30 | let result: ResourceTable[] = data; 31 | const hasSearchTerm = !!searchTerm; 32 | const hasCategories = category.length > 0; 33 | const hasTags = tags.length > 0; 34 | const isDefaultSearch = !hasSearchTerm && !hasCategories && !hasTags; 35 | 36 | if (isDefaultSearch) { 37 | return result; 38 | } 39 | if (hasSearchTerm) { 40 | result = data.filter((item) => { 41 | return item.title?.toLocaleLowerCase().includes(searchTerm) || item.description?.includes(searchTerm); 42 | }); 43 | } 44 | if (hasCategories) { 45 | result = result.filter((item) => { 46 | return (item.category || []).some((cat) => category.includes(cat)); 47 | }); 48 | } 49 | 50 | if (hasTags) { 51 | result = result.filter((item) => { 52 | return (item.tags || []).some((tag) => tags.includes(tag)); 53 | }); 54 | } 55 | return result; 56 | }; 57 | -------------------------------------------------------------------------------- /app/utils/database/supabaseDB.ts: -------------------------------------------------------------------------------- 1 | import { createClient, PostgrestResponse } from "@supabase/supabase-js"; 2 | import { type SupabaseClient } from "@supabase/supabase-js"; 3 | import { ResourceTable } from "~/types/dbTypes"; 4 | import { SQLDatabase } from "./database"; 5 | 6 | if (!process.env.SUPABASE_URL || !process.env.SUPABASE_SERVICE_KEY) { 7 | console.error( 8 | `Please set SUPABASE_URL and SUPABASE_SERVICE_KEY environment variables.\n` + 9 | `Please check that you have .env file created with the aforementioned supbase credentials.`, 10 | ); 11 | process.exit(1); 12 | } 13 | 14 | const DB_REFRESH_INTERVAL_MS = process.env.DB_REFRESH_INTERVAL_MS 15 | ? parseInt(process.env.DB_REFRESH_INTERVAL_MS) 16 | : 1000 * 10; 17 | 18 | export class SupabaseDB extends SQLDatabase { 19 | // @ts-ignore 20 | public db: SupabaseClient; 21 | 22 | private resourceTableCache: ResourceTable[] | null = null; 23 | 24 | private lastFetch: number = Date.now(); 25 | 26 | public async setup(): Promise { 27 | const supabase = createClient(process.env.SUPABASE_URL as string, process.env.SUPABASE_SERVICE_KEY as string, { 28 | schema: "public", 29 | autoRefreshToken: true, 30 | persistSession: true, 31 | detectSessionInUrl: true, 32 | }); 33 | this.db = supabase; 34 | this.lastFetch = Date.now(); 35 | console.log("NEW DB IS CREATED"); 36 | return this; 37 | } 38 | 39 | public async fetchAllResourcesCached(): Promise { 40 | if (!this.resourceTableCache || Date.now() - this.lastFetch > DB_REFRESH_INTERVAL_MS) { 41 | console.warn("Refetching resource table cache", { 42 | lastFetch: this.lastFetch, 43 | now: Date.now(), 44 | diff: Date.now() - this.lastFetch, 45 | DB_REFRESH_INTERVAL_MS, 46 | }); 47 | this.resourceTableCache = (await this.db.from("resources").select("*")).data; 48 | this.lastFetch = Date.now(); 49 | } 50 | return this.resourceTableCache as any; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/routes/about.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-no-target-blank */ 2 | import { DefaultLayout } from "~/layouts/DefaultLayout"; 3 | 4 | export default function AboutPage() { 5 | // 6 | return ( 7 | 8 |

About MicroDiscount

9 |

Hello world 🌍,

10 |

11 | MicroDiscount is a freecentralized place to discover and share quality discounts with the community. 12 |

13 |

14 | 15 | Clifford 16 | 17 | ,{" "} 18 | 19 | Dominic 20 | {" "} 21 | and{" "} 22 | 23 | Rob{" "} 24 | 25 | created this website because they noticed{" "} 26 | there wasn't an easy, simple centralized place to discover discounts online. Whether you're a student in 27 | college seeking a laptop or backpack discount, a teacher needing a software discount to teach their class or 28 | just a hobbyist trying out a new service out, our aim is to empower you. 29 |

30 |

31 | Most other discount websites are crowded with unhelpful ads and content distracting you from easily finding that 32 | discount you need. We are different. Our aim is to be simple and easy to use. 33 |

34 |

Would you like to submit a resource?

35 |

36 | We are open to contributions . If you would like to submit a resource, consider clicking the "Add a 37 | Resource" button on this page. Alternatively you can checkout the source code of this website on our github page 38 | here ( 39 | 40 | Github Code{" "} 41 | 42 | ) 43 |

44 |
45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "remix-app-template", 3 | "private": true, 4 | "description": "", 5 | "license": "MIT", 6 | "sideEffects": false, 7 | "scripts": { 8 | "build": "cross-env NODE_ENV=production remix build", 9 | "dev": "remix dev", 10 | "postinstall": "remix setup node", 11 | "lint": "eslint --cache --ext .tsx,.ts,.js,.jsx,.md --fix", 12 | "pretty": "prettier --write . --e", 13 | "preinstall": "npm run setup-env", 14 | "prepare": "husky install", 15 | "seed-db": "node -r ts-node/register ./app/data/convertDataToSupabase.ts", 16 | "seed-db-02": "node -r ts-node/register ./app/data/upload-discounts-02.ts", 17 | "setup-env": "sh ./scripts/check-volta-cli.sh", 18 | "test": "echo RUNNING TEST...TODO need to write tests" 19 | }, 20 | "dependencies": { 21 | "@nextui-org/react": "1.0.2-beta.4", 22 | "@remix-run/react": "^1.3.2", 23 | "@remix-run/vercel": "^1.3.2", 24 | "@supabase/supabase-js": "^1.31.1", 25 | "react": "^17.0.2", 26 | "react-dom": "^17.0.2", 27 | "remix": "^1.3.2", 28 | "use-dark-mode": "^2.3.1" 29 | }, 30 | "devDependencies": { 31 | "@remix-run/dev": "^1.3.2", 32 | "@remix-run/eslint-config": "^1.3.2", 33 | "@remix-run/serve": "^1.3.2", 34 | "@testing-library/jest-dom": "^5.16.2", 35 | "@testing-library/react": "^12.1.4", 36 | "@types/react": "^17.0.41", 37 | "@types/react-dom": "^17.0.14", 38 | "concurrently": "^7.0.0", 39 | "cross-env": "^7.0.3", 40 | "dotenv": "^16.0.0", 41 | "eslint": "^8.11.0", 42 | "husky": "^7.0.4", 43 | "jest": "^27.5.1", 44 | "lint-staged": "^12.3.7", 45 | "prettier": "^2.6.0", 46 | "sass": "^1.49.9", 47 | "ts-node": "^10.7.0", 48 | "typescript": "^4.6.2" 49 | }, 50 | "lint-staged": { 51 | "**/*.{js,tsx,ts,tsx}": [ 52 | "sh ./scripts/pretty.sh", 53 | "sh ./scripts/lint.sh", 54 | "npm run test" 55 | ] 56 | }, 57 | "engines": { 58 | "node": ">=14" 59 | }, 60 | "volta": { 61 | "node": "17.4.0", 62 | "yarn": "1.22.17" 63 | } 64 | } -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | This app was bootstrapped with [`remix-run` fullstack web framework](https://remix.run/docs). If you have ever used a tool like `create-react-app` then the setup & philosophy of this codebase might be familiar to you. 4 | 5 | ## Deployment 6 | 7 | After having run the `create-remix` command and selected "Vercel" as a deployment target, you only need to [import your Git repository](https://vercel.com/new) into Vercel, and it will be deployed. 8 | 9 | If you'd like to avoid using a Git repository, you can also deploy the directory by running [Vercel CLI](https://vercel.com/cli): 10 | 11 | ```sh 12 | npm i -g vercel 13 | vercel 14 | ``` 15 | 16 | It is generally recommended to use a Git repository, because future commits will then automatically be deployed by Vercel, through its [Git Integration](https://vercel.com/docs/concepts/git). 17 | 18 | ## Development 19 | 20 | To run your Remix app locally, make sure your project's local dependencies are installed: 21 | 22 | ```sh 23 | npm install 24 | ``` 25 | 26 | Afterwards, start the Remix development server like so: 27 | 28 | ```sh 29 | npm run dev 30 | ``` 31 | 32 | Open up [http://localhost:3000](http://localhost:3000) and you should be ready to go! 33 | 34 | If you're used to using the `vercel dev` command provided by [Vercel CLI](https://vercel.com/cli) instead, you can also use that, but it's not needed. 35 | 36 | ## Debugging 37 | 38 | If you are using VS Code as your text editor, you can use the builtin break point debugger during development. 39 | To get the application running in debugger mode follow the steps below: 40 | 41 | - ensure you are not running a local server already, if so kill/quit the process 42 | - click the bug icon in your side bar 43 | - click the "Run npm run dev" option in the select dropdown list. This will start start your server in debug mode. Try reloading the homepage and putting a break point inside your `loader` function for example. 44 | 45 | If you are unable to get VS Code debugger working follow this video: https://gist.github.com/kiliman/a9d7c874af03369a1d105a92560d89e9 46 | -------------------------------------------------------------------------------- /docs/code-architecture.md: -------------------------------------------------------------------------------- 1 | # Code Architecture 2 | 3 | This project is bootstrapped using [remix run's](https://remix.run/) CLI. 4 | Remix is a fullstack reactjs based web framework. 5 | 6 | ## Technology Stack 7 | 8 | **Frontend** 9 | 10 | - [ReactJS](https://beta.reactjs.org/) for the templating/ui rendering language 11 | 12 | **Backend** 13 | 14 | - Under the hood this uses nodeJS server (abstracted by `remix-run`). 15 | 16 | **Frontend & Backend** 17 | 18 | - both the frontend and backend use `remix-run` 19 | - [TypeScript](https://www.typescriptlang.org/) is used for both the language on the frontend & backend of this app. 20 | 21 | ## Hosting 22 | 23 | - code is hosted on [vercel](https://vercel.com/) 24 | 25 | ## Folders 26 | 27 | - `/api` 28 | - generated by `remix-run` 29 | - .... 30 | - `/app` 31 | - generated by `remix-run`. 32 | - This is where the application code lives. Both the frontend and backend live in this folder. `remix-run` may be unlike any other framework you've used, where you might have a server running with some endpoints and then the frontend code living separately making API calls to the server. Remix abstracts this for you. Our server and clientside code is co-located and its really awesome. 33 | - `/docs` 34 | - place to put any general documentation or indepth explanations of things. 35 | - `/public` 36 | - generated by `remix-run`. You will typically never interact with this folder. This is where Remix outputs the production of this code. 37 | - `/scripts` 38 | - general scripts to run, typically things for starting up the project, checking user's enviornment deploying or building code. 39 | 40 | ## General Thoughts 41 | 42 | - This website is simple in nature, and is mostly a static static data site; we use cache headers to simulate a static website. 43 | - All content is server side rendered, which is nice for SEO and we get for free using Remix without thinking about it. 44 | 45 | ## Resources 46 | 47 | - [`remix-run` YouTube Channel](https://www.youtube.com/channel/UC_9cztXyAZCli9Cky6NWWwQ) 48 | - [`remix-run` documentation](https://remix.run/) 49 | - [reactjs docs](https://beta.reactjs.org/) 50 | - [vercel](https://vercel.com/) 51 | -------------------------------------------------------------------------------- /public/icons/logo-buninux.svg: -------------------------------------------------------------------------------- 1 | 2 | logo 3 | 15 | -------------------------------------------------------------------------------- /.github/feature-request.yml: -------------------------------------------------------------------------------- 1 | name: "Feature Request" 2 | description: Suggest a new feature 3 | body: 4 | - type: markdown 5 | attributes: 6 | value: | 7 | Thank you for taking the time to request a new feature :pray: 8 | 9 | This form for suggesting new feature's or enhancements for [educationaldiscounts](https://github.com/TODO/TODO) 10 | If you have a question about how to achieve something and are struggling, please post a question 11 | inside of educationaldiscounts's [Discussions tab](https://github.com/TODO/TODO/discussions) 12 | 13 | Before submitting a feature request, please check the links below to see if there is a solution, feature request or question posted there already: 14 | - educationaldiscounts's [Discussions tab](https://github.com/TODO/TODO/discussions) 15 | - educationaldiscounts's [Open Issues](https://github.com/TODO/TODO/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc) 16 | - educationaldiscounts's [Closed Issues](https://github.com/TODO/TODO/issues?q=is%3Aissue+sort%3Aupdated-desc+is%3Aclosed) 17 | 18 | The more information you fill in, the better the community can help you. 19 | - type: textarea 20 | id: steps 21 | attributes: 22 | label: Describe the feature you would like see in educationaldiscounts 23 | description: Provide a clear and concise description. 24 | placeholder: | 25 | eg. It would be great if [...] 26 | eg. I wish there was a way to do [...] 27 | validations: 28 | required: true 29 | - type: textarea 30 | id: screenshots_or_videos 31 | attributes: 32 | label: Screenshots or Videos 33 | description: | 34 | If applicable, add screenshots or a video to help explain your problem. 35 | For more information on the supported file image/file types and the file size limits, please refer 36 | to the following link: https://docs.github.com/en/github/writing-on-github/working-with-advanced-formatting/attaching-files 37 | placeholder: | 38 | You can drag your video or image files inside of this editor ↓ 39 | - type: textarea 40 | id: additional 41 | attributes: 42 | label: Additional context 43 | description: Add any other context about the problem here. 44 | -------------------------------------------------------------------------------- /app/styles/index.css: -------------------------------------------------------------------------------- 1 | /********************** Homepage Styles ***********************/ 2 | :root { 3 | --primary: hsl(180, 59%, 51%); 4 | } 5 | 6 | .page-content { 7 | width: 100%; 8 | max-width: 1100px; 9 | margin: 0 auto; 10 | } 11 | 12 | @media (prefers-color-scheme: dark) { 13 | .navbar { 14 | background: #6f6f6f96; 15 | } 16 | } 17 | 18 | main { 19 | display: flex; 20 | } 21 | 22 | .home-header { 23 | display: flex; 24 | flex-direction: column; 25 | align-items: center; 26 | } 27 | .categories-list { 28 | display: flex; 29 | } 30 | 31 | .categories-list-item { 32 | /* */ 33 | } 34 | .category-item { 35 | padding: 16px; 36 | width: 200px; 37 | } 38 | .categories-list-item-icon { 39 | background-color: #fff; 40 | font-size: 36px; 41 | margin: 15px auto; 42 | text-align: center; 43 | width: 1.7em; 44 | line-height: 1.64em; 45 | border-radius: 50%; 46 | border: solid rgba(0, 0, 0, 0.1) 3px; 47 | } 48 | @media (prefers-color-scheme: dark) { 49 | .categories-list-item-icon { 50 | background-color: #eee !important; 51 | color: #222; 52 | } 53 | } 54 | 55 | /* Github corner icon*/ 56 | .github-corner svg { 57 | border: 0; 58 | color: #fff; 59 | fill: var(--primary); 60 | height: 80px; 61 | width: 80px; 62 | position: absolute; 63 | right: 0; 64 | top: 0; 65 | pointer-events: none; 66 | z-index: 101; 67 | } 68 | .github-corner .octo-triangle { 69 | pointer-events: auto; 70 | } 71 | .github-corner .octo-arm { 72 | fill: currentColor; 73 | transform-origin: 130px 106px; 74 | } 75 | .github-corner:hover .octo-arm { 76 | animation: octocat-wave 560ms ease-in-out; 77 | } 78 | .github-corner .octo-body { 79 | fill: currentColor; 80 | } 81 | @keyframes octocat-wave { 82 | 0%, 83 | 100% { 84 | transform: rotate(0); 85 | } 86 | 20%, 87 | 60% { 88 | transform: rotate(-25deg); 89 | } 90 | 40%, 91 | 80% { 92 | transform: rotate(10deg); 93 | } 94 | } 95 | @media (max-width: 500px) { 96 | .github-corner:hover .octo-arm { 97 | animation: none; 98 | } 99 | .github-corner .octo-arm { 100 | animation: octocat-wave 560ms ease-in-out; 101 | } 102 | } 103 | 104 | /********************** About Styles ***********************/ 105 | -------------------------------------------------------------------------------- /public/icons/microsoft-office-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import { Text } from "@nextui-org/react"; 2 | import { json, useLoaderData, type LoaderFunction } from "remix"; 3 | import { SearchForm } from "~/components/SearchForm"; 4 | import { ResourceTable } from "~/types/dbTypes"; 5 | import { filterDBItems, getDbInstance } from "~/utils"; 6 | import { DefaultLayout } from "~/layouts/DefaultLayout"; 7 | import { useRef } from "react"; 8 | import homepageCSS from "~/styles/index.css"; 9 | 10 | /** 11 | * @description 12 | * tags that will be embedded in the for this page. 13 | */ 14 | export const links = () => { 15 | return [ 16 | { 17 | rel: "stylesheet", 18 | href: homepageCSS, 19 | }, 20 | ]; 21 | }; 22 | 23 | /** 24 | * @description 25 | * Fetch the discount items on the server. 26 | * 27 | * NOTES: 28 | * `loader` function runs on the server. 29 | * `loader` is a specific term in remix. In here we fetch or return the data we want the component below to use. 30 | * `loader` can only be used in `routes` folder files 31 | */ 32 | export const loader: LoaderFunction = async ({ request, params }) => { 33 | const url = new URL(request.url); 34 | const searchTermParam = url.searchParams.get("search")?.trim().toLocaleLowerCase() || ""; 35 | const categoryParam = url.searchParams 36 | .getAll("category") 37 | .map((cat) => cat.toLowerCase()) 38 | .filter((cat) => !["all", "on"].includes(cat)); 39 | 40 | const tagsParam = url.searchParams.getAll("tags").map((t) => t.toLowerCase()); 41 | 42 | const db = await getDbInstance(); 43 | const discountItems = await db.fetchAllResourcesCached(); 44 | 45 | const items = filterDBItems(discountItems, { searchTerm: searchTermParam, category: categoryParam, tags: tagsParam }); 46 | 47 | return json(items, { 48 | // status: 301, 49 | headers: { 50 | "Cache-Control": "max-age=60, stale-while-revalidate=60", 51 | }, 52 | }); 53 | }; 54 | 55 | /** 56 | * @description 57 | * This component renders the homepage. 58 | */ 59 | export default function HomePage() { 60 | //useParams() https://remix.run/docs/en/v1/api/conventions#dynamic-route-parameters 61 | const data = useLoaderData(); 62 | const formRef = useRef(null); 63 | const submitForm = () => { 64 | if (formRef.current) { 65 | formRef.current.dispatchEvent(new Event("submit", { cancelable: true, bubbles: true })); 66 | } 67 | }; 68 | return ( 69 | 70 |
71 | 72 | The world's largest directory of software discounts and resources for students, teachers and hobbyists. 73 | 74 |
75 | 76 |
77 | 78 |
79 | 80 | {/*
87 | test 88 |
*/} 89 |
90 | ); 91 | } 92 | -------------------------------------------------------------------------------- /app/components/SearchFilterSideBar/SearchFilterSidebar.tsx: -------------------------------------------------------------------------------- 1 | import { Radio, Text, Grid } from "@nextui-org/react"; 2 | import React, { useEffect, useState } from "react"; 3 | import { useLocation } from "react-router"; 4 | import { type SUPPORTED_FORM_IDS } from "~/utils"; 5 | type SearchFilterSideBarProps = { 6 | /** 7 | * @description 8 | * The `id` value for the form element that you would like to connect 9 | * these fields to. 10 | */ 11 | formName: SUPPORTED_FORM_IDS; 12 | 13 | /** 14 | * @description 15 | * Allow programtically submit the form 16 | */ 17 | submitForm: () => void; 18 | }; 19 | 20 | const Categories = [ 21 | "Productivity", 22 | "Design", 23 | "Developer tools", 24 | "Learn", 25 | "Communication", 26 | "hardwares", 27 | "mobile contract", 28 | "shopping", 29 | "web hosting", 30 | "vpn", 31 | "Fashion", 32 | ]; 33 | const Tags = ["Students", "Teachers", "Free premium plan", "Free"]; 34 | 35 | /** 36 | * @description 37 | * .... 38 | * Associate this with the form 39 | */ 40 | const SearchFilterSideBar = ({ formName, submitForm }: SearchFilterSideBarProps) => { 41 | const [catSelected, setCatSelected] = useState(""); 42 | const [tagSelected, setTagSelected] = useState(""); 43 | const location = useLocation(); 44 | useEffect(() => { 45 | const allCat = new URLSearchParams(location.search).getAll("category"); 46 | const category = allCat.find((cat) => cat && cat.toLowerCase() !== "on"); 47 | if (category) { 48 | setCatSelected(category.toLowerCase()); 49 | } else { 50 | setCatSelected(""); 51 | } 52 | 53 | const allTag = new URLSearchParams(location.search).getAll("tags"); 54 | const tag = allTag.find((cat) => cat && cat.toLowerCase() !== "on"); 55 | if (tag) { 56 | setTagSelected(tag.toLowerCase()); 57 | } else { 58 | setTagSelected(""); 59 | } 60 | }, [location.search]); 61 | return ( 62 | <> 63 | Category 64 | 65 | 66 | { 69 | submitForm(); 70 | }} 71 | onChange={(e) => { 72 | setCatSelected(e as string); 73 | }} 74 | > 75 | 76 | All 77 | 78 | {Categories.map((category) => { 79 | return ( 80 | 88 | {category} 89 | 90 | ); 91 | })} 92 | 93 | 94 | 95 | 96 | Tags 97 | 98 | 99 | { 104 | submitForm(); 105 | }} 106 | onChange={(e) => { 107 | setTagSelected(e as string); 108 | }} 109 | > 110 | {Tags.map((tag) => ( 111 | 112 | {tag} 113 | 114 | ))} 115 | 116 | 117 | ); 118 | }; 119 | export default SearchFilterSideBar; 120 | -------------------------------------------------------------------------------- /public/icons/adidas-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: "🐛 Bug report" 2 | description: Report a reproducible bug or regression 3 | body: 4 | - type: markdown 5 | attributes: 6 | value: | 7 | Thank you for reporting an issue :pray:. 8 | 9 | This issue tracker is for reporting reproducible bugs or regression's found in the [educationaldiscounts github repository](https://github.com/robcerda/educationaldiscounts) 10 | If you have a question about how to achieve something and are struggling, please post a question 11 | inside of educationaldiscounts's [Discussions tab](https://github.com/robcerda/educationaldiscounts/discussions) 12 | 13 | Before submitting a new bug/issue, please check the links below to see if there is a solution or question posted there already: 14 | - educationaldiscounts's [Discussions tab](https://github.com/robcerda/educationaldiscounts/discussions) 15 | - educationaldiscounts's [Open Issues](https://github.com/robcerda/educationaldiscounts/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc) 16 | - educationaldiscounts's [Closed Issues](https://github.com/robcerda/educationaldiscounts/issues?q=is%3Aissue+sort%3Aupdated-desc+is%3Aclosed) 17 | 18 | The more information you fill in, the better the community can help you. 19 | - type: textarea 20 | id: description 21 | attributes: 22 | label: Describe the bug 23 | description: Provide a clear and concise description of the challenge you are running into. 24 | validations: 25 | required: true 26 | - type: textarea 27 | id: steps 28 | attributes: 29 | label: Steps to reproduce 30 | description: Describe the steps we have to take to reproduce the behavior. 31 | placeholder: | 32 | 1. Go to '...' 33 | 2. Click on '....' 34 | 3. Scroll down to '....' 35 | 4. See error 36 | validations: 37 | required: true 38 | - type: textarea 39 | id: expected 40 | attributes: 41 | label: Expected behavior 42 | description: Provide a clear and concise description of what you expected to happen. 43 | placeholder: | 44 | As a user, I expected ___ behavior but i am seeing ___ 45 | validations: 46 | required: true 47 | - type: dropdown 48 | attributes: 49 | label: How often does this bug happen? 50 | description: | 51 | Following the repro steps above, how easily are you able to reproduce this bug? 52 | options: 53 | - Every time 54 | - Often 55 | - Sometimes 56 | - Only once 57 | - type: textarea 58 | id: screenshots_or_videos 59 | attributes: 60 | label: Screenshots or Videos 61 | description: | 62 | If applicable, add screenshots or a video to help explain your problem. 63 | For more information on the supported file image/file types and the file size limits, please refer 64 | to the following link: https://docs.github.com/en/github/writing-on-github/working-with-advanced-formatting/attaching-files 65 | placeholder: | 66 | You can drag your video or image files inside of this editor ↓ 67 | - type: textarea 68 | id: platform 69 | attributes: 70 | label: Platform 71 | description: | 72 | Please let us know which Operting System, Browser and Browser version you were using when the issue occurred. 73 | placeholder: | 74 | - OS: [e.g. macOS, Windows, Linux, iOS, Android] 75 | - Browser: [e.g. Chrome, Safari, Firefox, React Native] 76 | - Version: [e.g. 91.1] 77 | validations: 78 | required: true 79 | - type: input 80 | id: educational-discounts-version 81 | attributes: 82 | label: educationaldiscounts version 83 | description: | 84 | Please let us know the exact version of educationaldiscounts you were using when the issue occurred. Please don't just put in "latest", as this is subject to change. 85 | placeholder: | 86 | e.g. v3.30.1 87 | validations: 88 | required: true 89 | - type: textarea 90 | id: additional 91 | attributes: 92 | label: Additional context 93 | description: Add any other context about the problem here. 94 | -------------------------------------------------------------------------------- /app/components/SearchForm/SearchForm.tsx: -------------------------------------------------------------------------------- 1 | import { Form, useNavigate, useTransition } from "remix"; 2 | import { SearchFilterSideBar } from "../SearchFilterSideBar"; 3 | import { type SUPPORTED_FORM_IDS } from "~/utils"; 4 | import { Button, Grid, Input, Loading, Text } from "@nextui-org/react"; 5 | import { ResourceTable } from "~/types/dbTypes"; 6 | import { ResourceCardGroup } from "../ResourceCardGroup"; 7 | import { useState } from "react"; 8 | import useMediaQuery from "~/utils/useMediaQuery"; 9 | 10 | // TODO add loading indicator 11 | 12 | type SearchFormProps = { 13 | /** 14 | * @decription 15 | * Search results items to display below the search input. 16 | */ 17 | searchResults: ResourceTable[]; 18 | /** 19 | * @description 20 | * The `id` value for the form element that you would like to connect 21 | * these fields to. 22 | */ 23 | formName: SUPPORTED_FORM_IDS; 24 | 25 | formRef: React.RefObject | null; 26 | 27 | submitForm: () => void; 28 | }; 29 | 30 | /** 31 | * @description 32 | * Renders the search input along with the search results. 33 | */ 34 | const SearchForm = ({ searchResults = [], formRef, submitForm, formName }: SearchFormProps) => { 35 | const transition = useTransition(); 36 | const navigate = useNavigate(); 37 | const isMobile = useMediaQuery("(max-width: 960px)"); 38 | const [searchValue, setSearchValue] = useState(""); 39 | const isSearchRequestInProgress = transition.state === "loading" || transition.state === "submitting"; 40 | 41 | return ( 42 | <> 43 | {!isMobile && ( 44 |
52 | 53 |
54 | )} 55 | 56 |
57 |
58 |
59 |
66 | setSearchValue(e.target.value)} 80 | onClearClick={() => { 81 | navigate("/", { replace: true }); 82 | formRef?.current?.reset(); 83 | }} 84 | contentRight={isSearchRequestInProgress ? : null} 85 | /> 86 | 93 | 94 |
95 | 96 | 97 | {searchResults.length} resources 98 | 99 | 100 | 111 | 112 | 113 | {searchResults.length > 0 ? ( 114 | 115 | 116 | 117 | ) : null} 118 |
119 |
120 | 121 | ); 122 | }; 123 | 124 | export default SearchForm; 125 | -------------------------------------------------------------------------------- /app/components/ResourceCardGroup/ResourceCardGroup.tsx: -------------------------------------------------------------------------------- 1 | import { Card, Grid, Text, styled } from "@nextui-org/react"; 2 | import type { ResourceTable } from "~/types/dbTypes"; 3 | import { event } from "~/utils/gtag.client"; 4 | type Props = { 5 | resources: ResourceTable[]; 6 | }; 7 | 8 | export default function ResourceCardGroup({ resources }: Props) { 9 | return ( 10 | 11 | {resources.map((resource) => { 12 | return ( 13 | 14 | { 22 | event({ 23 | action: "click card", 24 | label: "title", 25 | value: resource.domain || resource.title || "", 26 | }); 27 | window.open(resource.url, "_blank"); 28 | }} 29 | // clickable 30 | > 31 | 40 |
41 | logo 48 |
49 |
50 | {resource.title} 51 |
52 |
53 | 54 | 63 |
64 | {resource.description} 65 |
66 | 67 | {resource?.tags?.map((kw) => ( 68 | 69 | 70 | 71 | ))} 72 | 73 |
74 |
75 |
76 | ); 77 | })} 78 |
79 | ); 80 | } 81 | 82 | const getImageFromMultiplePlace = (domain: string | undefined, url: string | undefined) => { 83 | domain ??= ""; 84 | url ??= ""; 85 | return IconSvgPathMap[domain] || `/icons/${domain}.png` || faviconUrl(50, url); 86 | }; 87 | 88 | const IconSvgPathMap: { [key: string]: string } = { 89 | "canva.com": "/icons/canva-logo.svg", 90 | "github.com": "/icons/github-logo.svg", 91 | "edu.google.com": "/icons/google-logo.svg", 92 | "apple.com": "/icons/apple-logo.svg", 93 | "hulu.com": "/icons/hulu-logo.svg", 94 | "att.com": "/icons/at-and-t-logo.svg", 95 | "adidas.com": "/icons/adidas-logo.svg", 96 | "microsoft.com": "/icons/microsoft-office-logo.svg", 97 | "autodesk.com": "/icons/autodesk-logo.svg", 98 | "get.tech": "/icons/gettech-logo.jpeg", 99 | "buninux.com": "/icons/logo-buninux.svg", 100 | }; 101 | 102 | export function faviconUrl(size: number, url: string): string { 103 | try { 104 | const domain = new URL(url).hostname; 105 | return `https://www.google.com/s2/favicons?sz=${size}&domain=${domain}`; 106 | } catch (err) { 107 | return `./images/globe.svg`; 108 | } 109 | } 110 | 111 | // @ts-ignore : TODO: open PR/ISSUE at NextUI repo 112 | const StyledBadge = styled("span", { 113 | borderRadius: "$sm", 114 | border: "2px solid $gray200", 115 | padding: "$1", 116 | }); 117 | 118 | const Badge = ({ title }: { title: string }) => { 119 | return ( 120 | 121 | 122 | #{title} 123 | 124 | 125 | ); 126 | }; 127 | -------------------------------------------------------------------------------- /app/root.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | json, 3 | Links, 4 | LiveReload, 5 | Meta, 6 | Outlet, 7 | Scripts, 8 | ScrollRestoration, 9 | useCatch, 10 | useLoaderData, 11 | useLocation, 12 | type MetaFunction, 13 | } from "remix"; 14 | import { createTheme, NextUIProvider, CssBaseline } from "@nextui-org/react"; 15 | import IndexCss from "~/styles/index.css"; 16 | import { GA_TRACKING_ID, pageview } from "~/utils/gtag.client"; 17 | import { useEffect } from "react"; 18 | import useDarkMode from "use-dark-mode"; 19 | 20 | /** 21 | * @description 22 | * tags that will be embedded in the for this page. 23 | */ 24 | export const links = () => { 25 | return [ 26 | { 27 | rel: "stylesheet", 28 | href: IndexCss, 29 | }, 30 | ]; 31 | }; 32 | 33 | /** 34 | * @description 35 | * This loader is used for determining what enviornment we are in "development" or "production" 36 | * The environment variable is passed down to the browser 37 | * See: https://remix.run/docs/en/v1/guides/envvars#server-environment-variables 38 | */ 39 | export async function loader() { 40 | return json({ 41 | ENV: { 42 | APP_ENV: process.env.NODE_ENV, 43 | }, 44 | }); 45 | } 46 | 47 | export const meta: MetaFunction = () => { 48 | const description = `The world's largest directory of tech discounts for students, teachers & hobbyists.`; 49 | return { 50 | charset: "utf-8", 51 | description, 52 | keywords: "Educational Discounts,Tech Discounts, Coding Discounts, Programming Discounts", 53 | // "twitter:image": "https://example.com/social.png", 54 | // "twitter:card": "summary_large_image", 55 | // TODO NOTE for DOM: currently you can only list 1 username here 😞 I will leave mine here temporaily for launch for testing 56 | "twitter:creator": "@cliffordfajardo", 57 | "twitter:site": "@cliffordfajardo", 58 | "twitter:title": "MicroDiscount - quality tech discounts for students, teachers & hobbyists", 59 | "twitter:description": description, 60 | }; 61 | }; 62 | 63 | /** 64 | * @description 65 | * This is also known as `Root` in the remix docs; we renamed it, 66 | */ 67 | function Document({ children, title = "App title" }: { children: React.ReactNode; title?: string }) { 68 | const rootPageData = useLoaderData(); 69 | const isDevelopmentMode = rootPageData.ENV.APP_ENV !== "development"; 70 | const location = useLocation(); 71 | const darkMode = useDarkMode(false); 72 | 73 | useEffect(() => { 74 | if (isDevelopmentMode) { 75 | pageview(location.pathname); 76 | } 77 | }, [isDevelopmentMode, location]); 78 | 79 | return ( 80 | 81 | 82 | 83 | 84 | 85 | {title} 86 | {CssBaseline.flush()} 87 | 88 | 89 | 90 | 91 |
{children}
92 |
93 | 94 | 95 | 96 | 97 | 98 |