├── .eslintrc.json
├── public
├── og.jpg
└── favicon.ico
├── postcss.config.js
├── next.config.js
├── src
├── utils
│ ├── constants.ts
│ ├── api.ts
│ └── lookup.ts
├── components
│ ├── icon.tsx
│ ├── loader.tsx
│ ├── footer.tsx
│ ├── animated-section.tsx
│ ├── meta-tags.tsx
│ ├── header.tsx
│ ├── toast.tsx
│ ├── banner.tsx
│ └── select.tsx
├── pages
│ ├── _app.tsx
│ ├── api
│ │ └── generate.ts
│ └── index.tsx
├── styles
│ └── globals.css
└── json
│ └── flags.json
├── .gitignore
├── tailwind.config.js
├── tsconfig.json
└── package.json
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/public/og.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mustafa-turk/where-next/HEAD/public/og.jpg
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mustafa-turk/where-next/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | reactStrictMode: true,
4 | }
5 |
6 | module.exports = nextConfig
7 |
--------------------------------------------------------------------------------
/src/utils/constants.ts:
--------------------------------------------------------------------------------
1 | export const ERR_MESSAGE_NOT_FOUND =
2 | "We could not find any suggestions, please try again";
3 | export const ERR_MESSAGE_SELECT = "Please select at least one country";
4 |
--------------------------------------------------------------------------------
/src/components/icon.tsx:
--------------------------------------------------------------------------------
1 | import { FaGithub } from "react-icons/fa";
2 | import { RiErrorWarningFill } from "react-icons/ri";
3 |
4 | export const GithubIcon = FaGithub;
5 | export const ErrorIcon = RiErrorWarningFill;
6 |
--------------------------------------------------------------------------------
/src/components/loader.tsx:
--------------------------------------------------------------------------------
1 | import { Oval } from "react-loader-spinner";
2 |
3 | function Loader() {
4 | return (
5 |
13 | );
14 | }
15 |
16 | export default Loader;
17 |
--------------------------------------------------------------------------------
/src/utils/api.ts:
--------------------------------------------------------------------------------
1 | export function fetchSuggestions(selected: string[]) {
2 | return fetch("api/generate", {
3 | method: "POST",
4 | headers: {
5 | Accept: "application/json",
6 | "Content-Type": "application/json",
7 | },
8 | body: JSON.stringify({ selected }),
9 | })
10 | .then((res) => res.json())
11 | .then((data) => JSON.parse(data.suggestions))
12 | .catch(() => {
13 | throw new Error();
14 | });
15 | }
16 |
--------------------------------------------------------------------------------
/src/components/footer.tsx:
--------------------------------------------------------------------------------
1 | function Footer() {
2 | return (
3 |
16 | );
17 | }
18 |
19 | export default Footer;
20 |
--------------------------------------------------------------------------------
/src/components/animated-section.tsx:
--------------------------------------------------------------------------------
1 | import { motion } from "framer-motion";
2 |
3 | type Props = {
4 | children: JSX.Element | JSX.Element[];
5 | name: string;
6 | };
7 |
8 | function AnimatedSection({ children, name }: Props) {
9 | return (
10 |
17 | {children}
18 |
19 | );
20 | }
21 |
22 | export default AnimatedSection;
23 |
--------------------------------------------------------------------------------
/src/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import type { AppProps } from "next/app";
2 | import { QueryClient, QueryClientProvider } from "react-query";
3 | import { Analytics } from "@vercel/analytics/react";
4 |
5 | const queryClient = new QueryClient();
6 |
7 | import "@/styles/globals.css";
8 |
9 | export default function App({ Component, pageProps }: AppProps) {
10 | return (
11 | <>
12 |
13 |
14 |
15 |
16 | >
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | .pnpm-debug.log*
27 |
28 | # local env files
29 | .env
30 | .env*.local
31 |
32 | # vercel
33 | .vercel
34 |
35 | # typescript
36 | *.tsbuildinfo
37 | next-env.d.ts
38 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: [
4 | "./src/pages/**/*.{js,ts,jsx,tsx}",
5 | "./src/components/**/*.{js,ts,jsx,tsx}",
6 | "./src/**/*.{js,ts,jsx,tsx}",
7 | ],
8 | theme: {
9 | extend: {
10 | animation: {
11 | move: "move 160s ease infinite",
12 | },
13 | keyframes: {
14 | move: {
15 | "0%": { "margin-left": "0" },
16 | "100%": { "margin-left": "-5746px" },
17 | },
18 | },
19 | height: {
20 | box: "452px",
21 | },
22 | },
23 | },
24 | plugins: [],
25 | };
26 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true,
17 | "baseUrl": ".",
18 | "paths": {
19 | "@/*": ["./src/*"]
20 | }
21 | },
22 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
23 | "exclude": ["node_modules"]
24 | }
25 |
--------------------------------------------------------------------------------
/src/components/meta-tags.tsx:
--------------------------------------------------------------------------------
1 | const title = "Where next?";
2 | const description = "AI will pick the top destinations for you!";
3 | const imgUrl = "https://www.wherenext.app/og.jpg";
4 |
5 | function MetaTags() {
6 | return (
7 | <>
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | >
17 | );
18 | }
19 |
20 | export default MetaTags;
21 |
--------------------------------------------------------------------------------
/src/utils/lookup.ts:
--------------------------------------------------------------------------------
1 | import { shuffle } from "lodash";
2 | import countries from "@/json/flags.json";
3 |
4 | const { lookup } = require("country-data-list");
5 |
6 | export function getCountriesByCode(list: String[]) {
7 | return list.map((name) => {
8 | const result = lookup.countries({ name });
9 |
10 | if (!result) {
11 | return null;
12 | }
13 |
14 | const [details] = result;
15 | if (details && details.emoji) {
16 | const url = `https://en.wikipedia.org/wiki/${encodeURIComponent(
17 | name.trim()
18 | )}`;
19 | return { emoji: details.emoji, name, url };
20 | }
21 | return null;
22 | });
23 | }
24 |
25 | export function getAllCountries() {
26 | return countries;
27 | }
28 |
29 | export function getRandomCountries() {
30 | return shuffle(countries);
31 | }
32 |
--------------------------------------------------------------------------------
/src/components/header.tsx:
--------------------------------------------------------------------------------
1 | import { GithubIcon } from "./icon";
2 |
3 | function Header() {
4 | return (
5 |
25 | );
26 | }
27 |
28 | export default Header;
29 |
--------------------------------------------------------------------------------
/src/components/toast.tsx:
--------------------------------------------------------------------------------
1 | import dynamic from "next/dynamic";
2 | import { motion, AnimatePresence } from "framer-motion";
3 | import { resolveValue, toast, Toaster } from "react-hot-toast";
4 | import { ErrorIcon } from "@/components/icon";
5 |
6 | function Toast() {
7 | return (
8 |
9 | {(t) => (
10 |
11 | {t.visible && (
12 |
18 |
19 | {resolveValue(t.message, t)}
20 |
21 | )}
22 |
23 | )}
24 |
25 | );
26 | }
27 |
28 | export default dynamic(() => Promise.resolve(Toast), {
29 | ssr: false,
30 | });
31 |
32 | export { toast };
33 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "where-next",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@next/font": "13.1.4",
13 | "@types/node": "18.11.18",
14 | "@types/react": "18.0.27",
15 | "@types/react-dom": "18.0.10",
16 | "@vercel/analytics": "^0.1.8",
17 | "country-data-list": "^1.2.3",
18 | "eslint": "8.32.0",
19 | "eslint-config-next": "13.1.4",
20 | "framer-motion": "^8.5.2",
21 | "lodash": "^4.17.21",
22 | "lru-cache": "^7.14.1",
23 | "next": "13.1.4",
24 | "react": "18.2.0",
25 | "react-dom": "18.2.0",
26 | "react-hot-toast": "^2.4.0",
27 | "react-icons": "^4.7.1",
28 | "react-loader-spinner": "^5.3.4",
29 | "react-query": "^3.39.2",
30 | "react-select": "^5.7.0",
31 | "typescript": "4.9.4",
32 | "uuid": "^9.0.0"
33 | },
34 | "devDependencies": {
35 | "@types/lodash": "^4.14.191",
36 | "@types/uuid": "^9.0.0",
37 | "autoprefixer": "^10.4.13",
38 | "postcss": "^8.4.21",
39 | "tailwindcss": "^3.2.4"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/components/banner.tsx:
--------------------------------------------------------------------------------
1 | import { times } from "lodash";
2 | import { getRandomCountries } from "@/utils/lookup";
3 | import { memo } from "react";
4 | import dynamic from "next/dynamic";
5 |
6 | function Banner() {
7 | const isWindows = navigator.platform.indexOf("Win") === 0;
8 |
9 | return (
10 |
11 |
12 |
13 | {times(3, (index) => (
14 |
15 | {getRandomCountries().map((c) => {
16 | const label = isWindows ? c.name : `${c.emoji} ${c.name}`;
17 | return (
18 |
22 | {label}
23 |
24 | );
25 | })}
26 |
27 | ))}
28 |
29 |
30 | );
31 | }
32 |
33 | export default dynamic(() => Promise.resolve(memo(Banner)), {
34 | ssr: false,
35 | });
36 |
--------------------------------------------------------------------------------
/src/components/select.tsx:
--------------------------------------------------------------------------------
1 | import dynamic from "next/dynamic";
2 | import Select from "react-select";
3 |
4 | import { getAllCountries } from "@/utils/lookup";
5 |
6 | type Props = {
7 | onChange: Function;
8 | isDisabled: boolean;
9 | };
10 |
11 | function CustomSelect({ onChange, isDisabled }: Props) {
12 | const isWindows = navigator.platform.indexOf("Win") === 0;
13 |
14 | const options = getAllCountries().map((c) => ({
15 | value: c.name,
16 | label: isWindows ? c.name : `${c.emoji} ${c.name}`,
17 | }));
18 |
19 | return (
20 |