├── .env.example
├── .gitignore
├── .gitpod.yml
├── README.md
├── app
├── account
│ ├── page.js
│ ├── page.module.scss
│ └── search-history
│ │ ├── page.js
│ │ └── page.module.scss
├── globals.scss
├── layout.js
├── mixins.scss
├── not-found.js
├── not-found.module.scss
├── page.js
├── page.module.scss
├── search
│ ├── (all)
│ │ ├── page.js
│ │ └── page.module.scss
│ ├── books
│ │ └── page.js
│ ├── images
│ │ ├── page.js
│ │ └── page.module.scss
│ ├── layout.js
│ ├── layout.module.scss
│ ├── loading.js
│ ├── news
│ │ └── page.js
│ └── videos
│ │ ├── page.js
│ │ └── page.module.scss
└── variables.scss
├── components
├── pages
│ ├── all
│ │ ├── Card
│ │ │ ├── card.module.scss
│ │ │ └── index.js
│ │ ├── CardSkeleton
│ │ │ └── index.js
│ │ ├── Snippet
│ │ │ ├── index.js
│ │ │ └── style.module.scss
│ │ └── SnippetSkeleton
│ │ │ └── index.js
│ ├── images
│ │ ├── Card
│ │ │ ├── index.js
│ │ │ └── style.module.scss
│ │ └── CardSkeleton
│ │ │ └── index.js
│ ├── news
│ │ └── NewsCard
│ │ │ └── index.js
│ └── videos
│ │ ├── CardSkeleton
│ │ └── index.js
│ │ └── VideoCard
│ │ ├── index.js
│ │ └── videoCard.module.scss
└── shared
│ ├── AccountBtn
│ ├── index.js
│ └── style.module.scss
│ ├── Footer
│ ├── footer.module.scss
│ └── index.js
│ ├── GhStarBtn
│ ├── ghStarBtn.module.scss
│ └── index.js
│ ├── Loader
│ ├── index.js
│ └── loader.module.scss
│ ├── LoadmoreBtn
│ ├── index.js
│ └── loadmorebtn.module.scss
│ ├── NoResults
│ ├── index.js
│ └── noResults.module.scss
│ ├── SearchBar
│ ├── index.js
│ └── style.module.scss
│ └── ThemeBtn
│ ├── index.js
│ └── themeBtn.module.scss
├── context
├── AuthContext.js
└── ThemeContext.js
├── hooks
└── useLocalStorage.js
├── jsconfig.json
├── next.config.js
├── package.json
├── pages
└── api
│ ├── auth
│ └── [...nextauth].js
│ ├── card.js
│ ├── history.js
│ └── search
│ ├── images.js
│ ├── index.js
│ ├── news.js
│ └── videos.js
├── public
├── android-chrome-192x192.png
├── android-chrome-512x512.png
├── apple-touch-icon.png
├── favicon-16x16.png
├── favicon-32x32.png
├── favicon.ico
├── favicon.png
├── images
│ ├── home.png
│ ├── home_old.png
│ ├── logo.svg
│ ├── page_1.png
│ ├── page_2.png
│ ├── page_3.png
│ └── screenshot.png
├── logo.png
├── site.webmanifest
└── vercel.svg
└── utils
├── db.js
├── fetchImageResults.js
├── fetchNewsResults.js
├── fetchSearchResults.js
├── fetchVideosResults.js
└── getInitialColorMode.js
/.env.example:
--------------------------------------------------------------------------------
1 | GOOGLE_API_KEY=
2 | GOOGLE_API_CX=
3 |
4 | YOUTUBE_API_KEY=
5 |
6 | NEWS_API_KEY=
7 | OPENAI_API_KEY=
8 |
9 | NEXTAUTH_URL=
10 | NEXTAUTH_SECRET=
11 |
12 | GITHUB_ID=
13 | GITHUB_SECRET=
14 |
15 | AUTH0_ISSUER_BASE_URL
16 | AUTH0_CLIENT_ID
17 | AUTH0_CLIENT_SECRET=
18 |
19 | MONGODB_USERNAME=
20 | MONGODB_PASSWORD=
21 | MONGODB_HOST=
22 | MONGODB_DB=
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | .pnpm-debug.log*
27 |
28 | # local env files
29 | .env*.local
30 | .env.development
31 |
32 | # vercel
33 | .vercel
34 |
--------------------------------------------------------------------------------
/.gitpod.yml:
--------------------------------------------------------------------------------
1 | # This configuration file was automatically generated by Gitpod.
2 | # Please adjust to your needs (see https://www.gitpod.io/docs/introduction/learn-gitpod/gitpod-yaml)
3 | # and commit this file to your remote git repository to share the goodness with others.
4 |
5 | # Learn more from ready-to-use templates: https://www.gitpod.io/docs/introduction/getting-started/quickstart
6 |
7 | tasks:
8 | - init: pnpm install && pnpm run build
9 | command: pnpm run start
10 |
11 |
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | ## About The Project
7 | SearchEx is a search engine clone developed using NextJs, providing a powerful and intuitive search experience. It allows users to search for web pages, images, news, and videos.
8 |
9 | ### Features
10 | * Features
11 | * Clean and user-friendly UI
12 | * Comprehensive search capabilities
13 | * Intelligent auto-suggestions
14 | * Search history page
15 | * Profile management
16 | * User authentication with GitHub and auth0
17 | * Pagination system for search results
18 | * OpenAI integration for enhanced search intelligence
19 | * Light & dark theme options
20 | * Fully responsive design
21 |
22 | ## Getting Started
23 |
24 | To get started with this project, you can simply clone this repository and install the necessary dependencies.
25 |
26 | ```bash
27 | git clone https://github.com/devxprite/searchex.git
28 | cd searchex
29 | npm install
30 | ```
31 |
32 | ### Configuration
33 | Before running the project, make sure to set up the environment variables in a .env file located in the root directory of the project. Below is a sample .env file:
34 | ```
35 | GOOGLE_API_KEY=
36 | GOOGLE_API_CX=
37 |
38 | YOUTUBE_API_KEY=
39 |
40 | NEWS_API_KEY=
41 | OPENAI_API_KEY=
42 |
43 | NEXTAUTH_URL=
44 | NEXTAUTH_SECRET=
45 |
46 | GITHUB_ID=
47 | GITHUB_SECRET=
48 |
49 | AUTH0_ISSUER_BASE_URL
50 | AUTH0_CLIENT_ID
51 | AUTH0_CLIENT_SECRET=
52 |
53 | MONGODB_USERNAME=
54 | MONGODB_PASSWORD=
55 | MONGODB_HOST=
56 | MONGODB_DB=
57 | ```
58 |
59 | ### Running the Project
60 |
61 | Once you have set up the environment variables, you can start the development server with the following command:
62 | ```bash
63 | npm run dev
64 | ```
65 | This will start the Next.js development server at http://localhost:3000.
66 | The website auto-updates as you edit the file.
67 |
68 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
69 |
70 | ## Screenshots
71 | 
72 | 
73 | 
74 | 
75 |
76 |
77 | ## License
78 | This project is licensed under the MIT License.
--------------------------------------------------------------------------------
/app/account/page.js:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useSession, signIn, signOut } from 'next-auth/react';
4 | import styles from "./page.module.scss";
5 | import Link from "next/link";
6 | import { useRouter } from "next/navigation";
7 |
8 | import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'
9 | import 'react-loading-skeleton/dist/skeleton.css'
10 |
11 | export default function Page() {
12 | const { data: session, status } = useSession();
13 | const router = useRouter();
14 |
15 |
16 | if (status === 'loading') {
17 | return (
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | )
34 | }
35 |
36 | if (!session) {
37 | signIn()
38 | }
39 |
40 | const UserEmail = session?.user?.email;
41 | const UserName = session.user.name || session.user.email.split('@')[0];
42 | const UserImage = session.user?.image;
43 |
44 | return (
45 |
46 |
47 |
48 |
49 |
50 |
51 |
{UserName}
52 |
{UserEmail}
53 |
54 | router.push('/account/search-history')} >View Search History
55 | signOut({ callbackUrl: '/' }) } className={styles.logout}>Log Out
56 |
57 |
58 |
59 |
60 | )
61 | }
62 |
--------------------------------------------------------------------------------
/app/account/page.module.scss:
--------------------------------------------------------------------------------
1 | @import "@/app/mixins";
2 |
3 | .account_page {
4 | min-height: 100vh;
5 | display: grid;
6 | place-content: center;
7 |
8 | .card {
9 | margin: auto;
10 | // display: inline-block;
11 | padding: 2rem 1.5rem;
12 | background: var(--bg-1);
13 | border-radius: 0.5rem;
14 | font-weight: normal;
15 | transition: all 0.3s;
16 | @include gradient-border(var(--gradient-2), 0.5rem, 2px);
17 |
18 | @include mobile {
19 | max-width: 94%;
20 | }
21 |
22 |
23 | .userImg {
24 | max-width: 14rem;
25 | border-radius: 50%;
26 | margin-bottom: 2.5rem;
27 | box-shadow: var(--box-shadow);
28 | border: 2px solid var(--border-color);
29 | transition: all 0.3s;
30 |
31 | @include mobile {
32 | margin-bottom: 1rem;
33 | max-width: 70%;
34 | }
35 |
36 | &:hover {
37 | box-shadow: var(--box-shadow-hover);
38 | transform: scale(1.05);
39 | }
40 | }
41 |
42 | .name {
43 | @include gradient-text(var(--gradient-2));
44 | font-weight: normal;
45 | font-size: 2.2rem;
46 | }
47 |
48 | .email {
49 | font-size: 1.5rem;
50 | color: var(--color-2);
51 | }
52 |
53 | .buttons {
54 | display: flex;
55 | justify-content: space-around;
56 | gap: 2.5rem;
57 | margin-top: 3rem;
58 |
59 | @include mobile {
60 | flex-direction: column;
61 | gap: 1rem;
62 | }
63 |
64 | button,
65 | .btn {
66 | // color: var(--color);
67 | color: black;
68 | font-size: 1.1rem;
69 | padding: 0.5rem 1rem;
70 | border-radius: 0.3rem;
71 | border: 1px solid var(--border-color);
72 | // background: var(--bg);
73 | background: var(--gradient-2);
74 | box-shadow: var(--box-shadow);
75 | transition: all 0.3s;
76 | font-family: 'Ubuntu', sans-serif;
77 | cursor: pointer;
78 |
79 | &:hover {
80 | box-shadow: var(--box-shadow-hover);
81 | // background: var(--bg-1);
82 | transform: scale(1.05);
83 | }
84 |
85 | @include mobile {
86 | width: 100%;
87 | }
88 |
89 | &.logout {
90 | background: orangered;
91 | }
92 | }
93 | }
94 | }
95 | }
--------------------------------------------------------------------------------
/app/account/search-history/page.js:
--------------------------------------------------------------------------------
1 | import { getServerSession } from "next-auth"
2 | import mongoClient from "@/utils/db"
3 | import styles from "./page.module.scss";
4 | import { authOptions } from '@/pages/api/auth/[...nextauth]'
5 |
6 | export default async function Page() {
7 |
8 | const session = await getServerSession(authOptions);
9 | const email = session?.user?.email;
10 | const name = session?.user?.name || session?.user?.email.split('@')[0];
11 |
12 | const client = await mongoClient;
13 | const db = client.db();
14 |
15 | console.log(email);
16 |
17 | const searchHistory = await db.collection("history").find({
18 | email: email
19 | }).limit(200).toArray();
20 |
21 |
22 |
23 | console.log(searchHistory);
24 |
25 | return (
26 |
27 |
Search History of {name}
28 |
29 | {(searchHistory.length > 0)
30 | ? (
31 |
32 | {searchHistory.map((item, index) => (
33 |
34 |
35 | {(item.localTimestamp)}
36 |
37 |
38 | {item.query}
39 |
40 |
41 | {item.path}
42 |
43 |
44 | ))}
45 |
46 | )
47 | : (
No Search History found!
)
48 | }
49 |
50 |
51 | )
52 | }
--------------------------------------------------------------------------------
/app/account/search-history/page.module.scss:
--------------------------------------------------------------------------------
1 | @import '@/app/mixins';
2 |
3 | .page > h2{
4 | margin: 10vh 0 5vh 0;
5 | font-size: 2.5rem;
6 | }
7 |
8 | .history{
9 |
10 | max-width: 40rem;
11 | background-color: var(--bg-2);
12 | margin: auto;
13 | padding: 0.5rem;
14 | border-radius: 0.5rem;
15 |
16 |
17 | .item{
18 |
19 | display: grid;
20 | grid-template-columns: 1fr 2fr 1fr;
21 |
22 | &__query{
23 | font-size: 1.2rem;
24 | color: var(--color);
25 | }
26 |
27 | &__path{
28 | padding-left: 0.5rem;
29 | font-size: 1rem;
30 | color: var(--color-2);
31 | }
32 | }
33 | }
--------------------------------------------------------------------------------
/app/globals.scss:
--------------------------------------------------------------------------------
1 | @import '@/app/mixins';
2 | @import '@/app/variables';
3 |
4 | *,
5 | *::after,
6 | *::before {
7 | box-sizing: border-box;
8 | margin: 0;
9 | padding: 0;
10 | }
11 |
12 | html{
13 | ::-webkit-scrollbar {
14 | width: 0.4rem;
15 |
16 | @include mobile {
17 | width: 0.2rem;
18 | }
19 | }
20 |
21 | ::-webkit-scrollbar-track {
22 | background: var(--bg)
23 | }
24 |
25 | ::-webkit-scrollbar-thumb {
26 | background: var(--color-3);
27 | border-radius: 0.5rem;
28 | }
29 |
30 | ::-webkit-scrollbar-thumb:hover {
31 | background: var(--color-2);
32 | }
33 | }
34 |
35 | body {
36 | background-color: var(--bg);
37 | color: var(--color);
38 | text-align: center;
39 | font-family: 'Ubuntu', sans-serif;
40 | min-height: 100vh;
41 | -webkit-tap-highlight-color: transparent;
42 |
43 | main{
44 | // flex: 1;
45 | min-height: 100vh;
46 | }
47 | }
48 |
49 | a{
50 | color: var(--link-color);
51 | text-decoration: none;
52 | }
--------------------------------------------------------------------------------
/app/layout.js:
--------------------------------------------------------------------------------
1 | import { headers } from 'next/headers'
2 | import AuthContext from '@/context/AuthContext';
3 | import Footer from '@/components/shared/Footer'
4 | import './globals.scss'
5 | import { ThemeProvider } from '@/context/ThemeContext';
6 |
7 | export const metadata = {
8 | title: 'SearchEx',
9 | description: 'Effortlessly explore the web',
10 | keywords: ['Next.js', 'React', 'JavaScript'],
11 | authors: [{ name: 'DevXprite', url: 'https://github.com/devxprite' }],
12 | colorScheme: 'dark',
13 | favicon: '/favicon.png',
14 | alternates: {
15 | canonical: '/'
16 | },
17 | openGraph: {
18 | images: '/images/screenshot.png',
19 | type: 'website',
20 | },
21 | };
22 |
23 | async function getSession(cookies) {
24 | const response = await fetch(`${process.env.NEXTAUTH_URL}/api/auth/session`, {
25 | headers: {
26 | cookies,
27 | },
28 | });
29 |
30 | const session = await response.json();
31 |
32 | return Object.keys(session).length > 0 ? session : null;
33 | }
34 |
35 | export default async function RootLayout({ children }) {
36 |
37 | const session = await getSession(headers().get('cookie') ?? '');
38 |
39 | return (
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | {children}
52 |
53 |
54 |
55 |
56 |
57 |
58 | )
59 | }
60 |
--------------------------------------------------------------------------------
/app/mixins.scss:
--------------------------------------------------------------------------------
1 | @mixin mobile {
2 | @media screen and (max-width: 800px) {
3 | @content;
4 | }
5 | }
6 |
7 | @mixin gradient-text($gradient) {
8 | color: transparent;
9 | -webkit-text-fill-color: transparent;
10 | background: $gradient;
11 | background-clip: text;
12 | -webkit-background-clip: text;
13 | display: inline-block;
14 | }
15 |
16 | @mixin gradient-border($gradient, $radius: 0.5rem, $width: 2px) {
17 | position: relative;
18 | border-radius: $radius;
19 | box-shadow: var(--box-shadow);
20 |
21 |
22 | &::before {
23 | content: "";
24 | opacity: 1;
25 | position: absolute;
26 | inset: $width * -1;
27 | pointer-events: none;
28 | border-radius: $radius;
29 | z-index: -20;
30 | background: $gradient;
31 | }
32 | }
--------------------------------------------------------------------------------
/app/not-found.js:
--------------------------------------------------------------------------------
1 | import styles from './not-found.module.scss'
2 | import Link from 'next/link'
3 |
4 | export default function NotFound() {
5 | return (
6 |
7 |
8 |
404
9 |
Page Not Found
10 |
The Page you are looking for doesn't exist or an other error occured. Go to Home
11 |
12 |
13 | );
14 | }
--------------------------------------------------------------------------------
/app/not-found.module.scss:
--------------------------------------------------------------------------------
1 | @import "@/app/mixins.scss";
2 |
3 | .not_found {
4 | display: flex;
5 | align-items: center;
6 | justify-content: center;
7 | min-height: 90vh;
8 | font-family: "Raleway", sans-serif;
9 | flex-direction: column;
10 | gap: 1.25rem;
11 | padding: 0 2rem;
12 |
13 | @include mobile {
14 | gap: 1rem;
15 | padding: 0 0.5rem;
16 |
17 | }
18 |
19 | h2 {
20 | @include gradient-text(var(--gradient-1));
21 | font-size: 12rem;
22 | font-weight: normal;
23 |
24 | @include mobile {
25 | font-size: 8rem;
26 | }
27 |
28 | }
29 |
30 | h3 {
31 | font-size: 3rem;
32 | font-weight: normal;
33 |
34 | @include mobile {
35 | font-size: 1.75rem;
36 | }
37 | }
38 |
39 | p {
40 | font-size: 1.5rem;
41 |
42 | @include mobile {
43 | font-size: 1rem;
44 | }
45 | }
46 | }
--------------------------------------------------------------------------------
/app/page.js:
--------------------------------------------------------------------------------
1 | import AccountBtn from '@/components/shared/AccountBtn'
2 | import SearchBar from '@/components/shared/SearchBar'
3 | import styles from './page.module.scss'
4 |
5 | export default function Home() {
6 | return (
7 | <>
8 |
11 | {/* SearchEx */}
12 |
13 | Effortlessly explore the Web
14 |
15 | >
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/app/page.module.scss:
--------------------------------------------------------------------------------
1 | @import '@/app/mixins';
2 |
3 | .accountBtn_container{
4 | position: absolute;
5 | top: 1rem;
6 | right: 1rem;
7 | }
8 |
9 | .logo {
10 | // font-family: "Raleway", sans-serif;
11 | // font-weight: lighter;
12 | // font-size: 8rem;
13 | color: #fff;
14 | margin-top: 15vh;
15 | margin-bottom: -3.25rem;
16 | max-width: 40rem;
17 |
18 | @include mobile {
19 | width: 92%;
20 | font-size: 3.5rem;
21 | margin-top: 25vh;
22 | margin-bottom: -1.5rem;
23 | }
24 | }
25 |
26 | .tagline {
27 | font-weight: lighter;
28 | font-size: 2rem;
29 | color: var(--color);
30 | text-decoration: none;
31 | margin-top: 0rem;
32 | letter-spacing: 2px;
33 | margin-bottom: 6rem;
34 | padding: 0 0.75rem;
35 | font-family: "Raleway", sans-serif;
36 |
37 | @include mobile {
38 | font-size: 1rem;
39 | margin-bottom: 6rem;
40 | letter-spacing: 1px;
41 | }
42 | }
--------------------------------------------------------------------------------
/app/search/(all)/page.js:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useSearchParams } from "next/navigation";
4 | import { useState, useEffect } from "react";
5 | import ResultsSnippet from "@/components/pages/all/Snippet";
6 | import styles from "./page.module.scss";
7 | import ResultSkeleton from "@/components/pages/all/SnippetSkeleton";
8 | import ResultCard from "@/components/pages/all/Card";
9 | import NoResults from "@/components/shared/NoResults";
10 | import LoadmoreBtn from "@/components/shared/LoadmoreBtn";
11 | import axios from "axios";
12 |
13 | const searchPage = () => {
14 | const searchParams = useSearchParams();
15 | const query = searchParams.get("q");
16 |
17 | const [results, setResults] = useState([]);
18 | const [page, setPage] = useState(1);
19 | const [loading, setLoading] = useState(true);
20 | const [error, setError] = useState(false);
21 | const [isMoreLoading, setIsMoreLoading] = useState(false);
22 |
23 | useEffect(() => {
24 | axios.get(`/api/search/?q=${query}&page=${page}`)
25 | .then(res => {
26 | setResults((results) => [...results, ...res.data || []]);
27 | })
28 | .catch(err => {
29 | console.log(err);
30 | setError(true);
31 | })
32 | .finally(() => {
33 | setLoading(false);
34 | setIsMoreLoading(false);
35 | })
36 |
37 | }, [query, page])
38 |
39 | useEffect(() => { setResults([]), setLoading(true) }, [query])
40 |
41 | if (error) {
42 | return (
43 | <>
44 |
45 |
46 |
Something went wrong
47 |
48 |
49 | >
50 | )
51 | }
52 |
53 | if (!loading && results.length === 0) {
54 | return ( )
55 | }
56 |
57 | return (
58 | <>
59 |
60 |
61 |
62 | {
63 | (loading) ? (
64 | <>{Array.from(Array(10).keys()).map((i) => )}>
65 | ) : (
66 | <>
67 | {results.map(result => (
68 |
69 | ))}
70 | {(page < 4) && { setPage((page) => page + 1); setIsMoreLoading(true) }} />}
71 | >
72 | )
73 | }
74 |
75 |
76 |
77 | >
78 | )
79 | }
80 |
81 | export default searchPage;
--------------------------------------------------------------------------------
/app/search/(all)/page.module.scss:
--------------------------------------------------------------------------------
1 | @import "@/app/mixins";
2 |
3 | .searchPage {
4 | display: grid;
5 | grid-template-columns: 2fr 1.4fr;
6 |
7 | @include mobile {
8 | grid-template-columns: 1fr;
9 |
10 | .results__container{
11 | grid-row: 2;
12 | }
13 | }
14 | }
--------------------------------------------------------------------------------
/app/search/books/page.js:
--------------------------------------------------------------------------------
1 | const Page = (props) => {
2 | return (
3 | <>
4 | This is Book Page
5 | >
6 | )
7 | }
8 |
9 | export default Page
--------------------------------------------------------------------------------
/app/search/images/page.js:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useSearchParams } from "next/navigation";
4 | import { useState, useEffect } from "react";
5 | import styles from "./page.module.scss";
6 | import Card from "@/components/pages/images/Card";
7 | import CardSkeleton from "@/components/pages/images/CardSkeleton";
8 | import LoadmoreBtn from "@/components/shared/LoadmoreBtn";
9 | import axios from "axios";
10 | import NoResults from "@/components/shared/NoResults";
11 |
12 | const Page = (props) => {
13 |
14 | const searchParams = useSearchParams();
15 | const query = searchParams.get("q");
16 |
17 | const [results, setResults] = useState([]);
18 | const [page, setPage] = useState(1);
19 | const [loading, setLoading] = useState(true);
20 | const [isMoreLoading, setIsMoreLoading] = useState(false);
21 |
22 | useEffect(() => {
23 | axios.get(`/api/search/images?q=${query}&page=${page}`)
24 | .then(res => {
25 | setResults((results) => [...results, ...res.data || []]);
26 | })
27 | .catch(err => {
28 | console.log(err);
29 | })
30 | .finally(() => {
31 | setLoading(false);
32 | setIsMoreLoading(false);
33 | })
34 | }, [query, page])
35 |
36 | useEffect(() => { setResults([]), setLoading(true) }, [query]);
37 |
38 | if (loading) {
39 | return (
40 |
41 |
42 | {Array.from(Array(15).keys()).map((i) => )}
43 |
44 |
45 | )
46 | }
47 |
48 | if (results.length === 0) {
49 | return ( )
50 | }
51 |
52 | return (
53 |
54 |
55 | {results.map(result => (
56 |
57 | ))}
58 |
59 | {
60 | (page < 5) &&
{ setPage((page) => page + 1); setIsMoreLoading(true) }} />
61 | }
62 |
63 | )
64 | }
65 |
66 | export default Page
--------------------------------------------------------------------------------
/app/search/images/page.module.scss:
--------------------------------------------------------------------------------
1 | .results__container{
2 | display: grid;
3 | grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
4 | margin-top: 2rem;
5 | grid-gap: 3rem 1.5rem;
6 | align-items: center;
7 | text-align: left;
8 |
9 | @media (max-width: 800px) {
10 | grid-template-columns: 1fr 1fr;
11 | }
12 | }
--------------------------------------------------------------------------------
/app/search/layout.js:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import styles from './layout.module.scss'
4 | import SearchBar from "@/components/shared/SearchBar";
5 | import { usePathname, useRouter, useSearchParams } from 'next/navigation';
6 | import Link from 'next/link';
7 |
8 | import { BsSearch, BsImage, BsNewspaper } from 'react-icons/bs';
9 | import { BsFillCameraVideoFill } from 'react-icons/bs';
10 | import { BsMap } from 'react-icons/bs';
11 | import { Suspense } from 'react';
12 | import AccountBtn from '@/components/shared/AccountBtn';
13 |
14 | export default function SearchLayout({ children }) {
15 | const router = useRouter();
16 | const query = useSearchParams().get('q') || '';
17 |
18 | const pages = [
19 | {
20 | title: 'All',
21 | url: '/search?q=',
22 | icon: ,
23 | pathRegex: /^\/search$/
24 | },
25 | {
26 | title: 'Videos',
27 | url: '/search/videos?q=',
28 | icon: ,
29 | pathRegex: /^\/search\/videos/
30 | },
31 | {
32 | title: 'Images',
33 | url: '/search/images?q=',
34 | icon: ,
35 | pathRegex: /^\/search\/images/
36 | },
37 | {
38 | title: 'News',
39 | url: '/search/news?q=',
40 | icon: ,
41 | pathRegex: /^\/search\/news/
42 | },
43 | {
44 | title: 'Maps',
45 | url: 'https://www.google.com/maps/search/',
46 | icon: ,
47 | pathRegex: /^\/search\/maps/
48 | },
49 | ]
50 |
51 | return (
52 |
53 |
54 |
55 |
56 |
57 |
SearchEx
58 | {/*
*/}
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | {pages.map(page => (
67 |
72 | {page.icon} {page.title}
73 |
74 | ))}
75 |
76 |
77 |
80 |
81 |
82 | {children}
83 |
84 |
85 |
86 | )
87 | }
--------------------------------------------------------------------------------
/app/search/layout.module.scss:
--------------------------------------------------------------------------------
1 | @import "@/app/mixins";
2 |
3 | .header {
4 | display: grid;
5 | grid-template-columns: 9% 40rem auto;
6 | grid-template-rows: 1fr;
7 | padding: 1rem 0.5rem 0.5rem 0.5rem;
8 | grid-gap: 1rem;
9 | background-color: var(--bg-1);
10 | box-shadow: var(--box-shadow);
11 | border-bottom: 1px solid rgba(var(--border-color), 0.5);
12 | transition: all 0.3s;
13 |
14 | @include mobile {
15 | grid-template-columns: 1fr 1fr;
16 | padding: 0.5rem;
17 | row-gap: 2rem;
18 | }
19 |
20 | .menu {
21 | display: flex;
22 | flex-direction: row;
23 | justify-content: space-around;
24 | margin-top: 1.2rem;
25 | font-size: 1rem;
26 |
27 | a {
28 | color: var(--color-1);
29 | text-decoration: none;
30 | position: relative;
31 | display: flex;
32 | flex-direction: row;
33 | gap: 0.25rem;
34 | transition: all 0.3s;
35 |
36 |
37 | @include mobile {
38 | flex-direction: column;
39 | gap: unset;
40 | }
41 |
42 | &:hover {
43 | color: var(--color);
44 | transform: scale(1.075);
45 | }
46 |
47 | &.active {
48 | color: var(--link-color);
49 | font-weight: bold;
50 | }
51 | }
52 | }
53 |
54 | .container__logo {
55 |
56 | @include mobile {
57 | text-align: left;
58 | padding-left: 0.5rem;
59 | }
60 |
61 | .logo {
62 | @include gradient-text(var(--gradient-1));
63 | font-family: "Railway", sans-serif;
64 | margin-top: 0.75rem;
65 | font-size: 1.5rem;
66 | transition: all 0.3s;
67 | // width: 90%;
68 |
69 | &:hover{
70 | transform: scale(1.1);
71 | }
72 |
73 | @include mobile {
74 | margin-top: 0.25rem;
75 | }
76 | }
77 | }
78 |
79 | .container__account {
80 | padding-right: 2rem;
81 |
82 | @include mobile {
83 | padding-right: 0.5rem;
84 | }
85 | }
86 |
87 | .container__search {
88 | @include mobile {
89 | grid-column: span 2;
90 | grid-row: 2;
91 | }
92 | }
93 | }
94 |
95 | .container__content {
96 | padding-top: 1rem;
97 | margin: 0 10%;
98 |
99 | @include mobile {
100 | margin: 0 4%;
101 | }
102 | }
--------------------------------------------------------------------------------
/app/search/loading.js:
--------------------------------------------------------------------------------
1 | import Loader from "@/components/shared/Loader";
2 |
3 | export default function Loading() {
4 | return ( )
5 | }
--------------------------------------------------------------------------------
/app/search/news/page.js:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import CardSkeleton from "@/components/pages/videos/CardSkeleton";
4 | import VideoCard from "@/components/pages/videos/VideoCard";
5 | import LoadmoreBtn from "@/components/shared/LoadmoreBtn";
6 | import NoResults from "@/components/shared/NoResults";
7 | import axios from "axios";
8 | import { useSearchParams } from "next/navigation";
9 | import { useState, useEffect } from "react";
10 |
11 | const Page = (props) => {
12 |
13 | const searchParams = useSearchParams();
14 | const query = searchParams.get("q");
15 |
16 | const [results, setResults] = useState([]);
17 | const [page, setPage] = useState(1);
18 | const [loading, setLoading] = useState(true);
19 | const [isMoreLoading, setIsMoreLoading] = useState(false);
20 |
21 | useEffect(() => {
22 | axios.get(`/api/search/news?q=${query}&page=${page}`)
23 | .then(res => {
24 | setResults((results) => [...results, ...res.data || []]);
25 | })
26 | .catch(err => {
27 | console.log(err);
28 | })
29 | .finally(() => {
30 | setLoading(false);
31 | setIsMoreLoading(false);
32 | })
33 | }, [query, page])
34 |
35 | useEffect(() => { setResults([]), setLoading(true) }, [query])
36 |
37 | if (loading) {
38 | return (
39 |
40 |
41 | {Array.from(Array(15).keys()).map((i) => )}
42 |
43 |
44 | )
45 | }
46 |
47 | if(!results.length) {
48 | return (
49 |
54 | )
55 | }
56 |
57 | return (
58 | <>
59 |
60 |
61 | <>
62 | {results.map((result, i) => (
63 |
64 | ))}
65 | >
66 |
67 | {(page < 5) &&
{setPage((page) => page + 1); setIsMoreLoading(true)}} />}
68 |
69 | >
70 | )
71 | }
72 |
73 | export default Page
--------------------------------------------------------------------------------
/app/search/videos/page.js:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import CardSkeleton from "@/components/pages/videos/CardSkeleton";
4 | import VideoCard from "@/components/pages/videos/VideoCard";
5 | import { useSearchParams } from "next/navigation";
6 | import { useState, useEffect } from "react";
7 | import styles from "./page.module.scss";
8 | import NoResults from "@/components/shared/NoResults";
9 | import LoadmoreBtn from "@/components/shared/LoadmoreBtn";
10 |
11 | const Page = (props) => {
12 |
13 | const searchParams = useSearchParams();
14 | const query = searchParams.get("q");
15 |
16 | const [results, setResults] = useState([]);
17 | const [page, setPage] = useState("");
18 | const [loading, setLoading] = useState(true);
19 | const [nextPageToken, setNextPageToken] = useState("");
20 | const [isMoreLoading, setIsMoreLoading] = useState(false);
21 | const [error, setError] = useState(false);
22 |
23 | useEffect(() => {
24 | fetch(`/api/search/videos?q=${query}&page=${page}`)
25 | .then(res => res.json())
26 | .then(data => {
27 | setLoading(false);
28 | setResults((results) => [...results, ...data?.videos || []]);
29 | setNextPageToken(data?.pageInfo?.nextPageToken || "");
30 | setIsMoreLoading(false);
31 | })
32 | }, [query, page]);
33 |
34 | useEffect(() => { setResults([]), setLoading(true) }, [query])
35 |
36 | if (error) {
37 | return (
38 | <>
39 |
40 |
41 |
Internal Server Error
42 |
43 |
44 | >
45 | )
46 | }
47 |
48 | if (loading) {
49 | return (
50 | <>
51 |
52 |
53 | <>{Array.from(Array(10).keys()).map((i) => )}>
54 |
55 |
56 | >
57 | )
58 | }
59 |
60 | if (!results.length) {
61 | return (
62 |
63 |
64 |
65 | )
66 | }
67 |
68 | return (
69 | <>
70 |
71 |
72 | <>
73 | {results.map((result, i) => (
74 |
75 | ))}
76 | >
77 |
78 |
{setPage(nextPageToken); setIsMoreLoading(true); }} />
79 |
80 | >
81 | )
82 | }
83 |
84 | export default Page
--------------------------------------------------------------------------------
/app/search/videos/page.module.scss:
--------------------------------------------------------------------------------
1 | .videos__page{
2 |
3 | }
--------------------------------------------------------------------------------
/app/variables.scss:
--------------------------------------------------------------------------------
1 | $variables-dark: (
2 | bg: #161616,
3 | bg-1: lighten(#161616, 5%),
4 | bg-2: lighten(#161616, 12%),
5 |
6 | color: #f7f7f7,
7 | color-1: darken(#f7f7f7, 10%),
8 | color-2: darken(#f7f7f7, 20%),
9 | color-3: darken(#f7f7f7, 30%),
10 | border-color: rgba(darken(#f7f7f7, 50%), 0.5),
11 | link-color: #4dafff,
12 |
13 | gradient-1:linear-gradient(to bottom right, #7b0afd 15%, #00fff0),
14 | gradient-2: linear-gradient(to bottom right, #00ef86, #008eb9),
15 |
16 | border-radius: 0.5rem,
17 | box-shadow: 2px 4px 4px rgb(0 0 0 / 25%),
18 | box-shadow-hover: 2px 4px 8px rgb(0 0 0 / 60%)
19 | );
20 |
21 | $variables-light: (
22 | bg: #fefefe,
23 | bg-1: darken(white, 2%),
24 | bg-2: darken(white, 3%),
25 |
26 | color: lighten(#000, 4%),
27 | color-1: lighten(#000, 10%),
28 | color-2: lighten(#000, 20%),
29 | color-3: lighten(#000, 25%),
30 | border-color: rgba(lighten(#000, 50%), 0.3),
31 | link-color: #006ce3,
32 |
33 | box-shadow: 1px 2px 6px rgba(0, 0, 0, 0.08),
34 | box-shadow-hover: 2px 6px 14px rgba(0, 0, 0, 0.15)
35 | );
36 |
37 | :root {
38 |
39 | @each $key,
40 | $value in $variables-dark {
41 | --#{$key}: #{$value};
42 | }
43 | }
44 |
45 | html[data-theme="light"],
46 | body[data-theme="light"] {
47 |
48 | @each $key,
49 | $value in $variables-light {
50 | --#{$key}: #{$value};
51 | }
52 | }
53 |
54 | @function Var($key) {
55 | @return var(--#{$key});
56 | }
--------------------------------------------------------------------------------
/components/pages/all/Card/card.module.scss:
--------------------------------------------------------------------------------
1 | @import '@/app/mixins';
2 |
3 | .card {
4 | // border: 2px solid var(--border-color);
5 | @include gradient-border(var(--gradient-2), 0.5rem, 2px);
6 | padding: 1rem;
7 | border-radius: 0.5rem;
8 | width: 72%;
9 | max-width: 23rem;
10 | margin-top: 1.2rem;
11 | margin-left: 7%;
12 | text-align: left;
13 | box-shadow: var(--box-shadow);
14 | position: relative;
15 | background: var(--bg);
16 |
17 |
18 | &.skeleton {
19 | border-color: transparent;
20 | box-shadow: none;
21 |
22 | &::before {
23 | opacity: 0;
24 | }
25 | }
26 |
27 | @include mobile {
28 | @include gradient-border(var(--gradient-2), 0.5rem, 1px);
29 | margin-left: 0;
30 | width: 100%;
31 | grid-row: 1;
32 | border-width: 1px;
33 | padding: 0.6rem;
34 | }
35 |
36 | img {
37 | width: 98%;
38 | display: block;
39 | margin: auto;
40 | max-height: 15rem;
41 | border-radius: 0.5rem;
42 | box-shadow: var(--box-shadow);
43 | }
44 |
45 | .title {
46 | margin-top: 0.75rem;
47 | font-weight: normal;
48 | @include gradient-text(var(--gradient-2));
49 |
50 | @include mobile {
51 | font-size: 1.5rem;
52 | }
53 | }
54 |
55 | .content {
56 | color: var(--color-2);
57 | font-size: 0.9rem;
58 | margin-top: 1rem;
59 | }
60 | }
--------------------------------------------------------------------------------
/components/pages/all/Card/index.js:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useSearchParams } from "next/navigation";
4 | import { useState, useEffect, useRef } from "react";
5 | import CardSkeleton from "../CardSkeleton";
6 | import styles from "./card.module.scss"
7 |
8 | const ResultCard = () => {
9 | const searchParams = useSearchParams();
10 | const cardRef = useRef(null);
11 | const q = searchParams.get("q");
12 |
13 | const [cardData, setCardData] = useState(null);
14 | const [loading, setLoading] = useState(true);
15 | const [error, setError] = useState(false);
16 |
17 | useEffect(() => {
18 | const fetchCardData = async () => {
19 | const response = await fetch(`/api/card?q=${q}`)
20 | .then((response) => response.json()).catch(() => setError(true));
21 |
22 | if (!response.content || response.content.length < 15) setError(true);
23 |
24 |
25 | setCardData(response);
26 | setLoading(false);
27 | }
28 |
29 | fetchCardData();
30 |
31 | return () => { setLoading(true); setError(false); }
32 | }, [q]);
33 |
34 | if (error) return;
35 |
36 | return (
37 |
38 | {!loading ? (
39 |
40 |
41 | {cardData.image &&
}
42 |
{cardData.title}
43 |
{cardData.description}
44 |
45 |
46 |
47 | ) : (
48 |
49 | )}
50 |
51 | )
52 | }
53 |
54 | export default ResultCard;
--------------------------------------------------------------------------------
/components/pages/all/CardSkeleton/index.js:
--------------------------------------------------------------------------------
1 | import styles from '../Card/card.module.scss'
2 | import 'react-loading-skeleton/dist/skeleton.css'
3 | import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'
4 | import { ThemeContext } from "@/context/ThemeContext";
5 | import { useContext } from "react";
6 |
7 | const CardSkeleton = () => {
8 |
9 | const { theme } = useContext(ThemeContext);
10 | // const baseColor = theme === "dark" ? "#343434" : "#f5f5f5";
11 | // const highlightColor = theme === "dark" ? "#565656" : "#e5e5e5";
12 |
13 | const baseColor = theme === "light" ? "#f5f5f5" : "#343434";
14 | const highlightColor = theme === "light" ? "#e5e5e5" : "#565656";
15 |
16 | return (
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | )
27 | }
28 |
29 | export default CardSkeleton;
30 |
--------------------------------------------------------------------------------
/components/pages/all/Snippet/index.js:
--------------------------------------------------------------------------------
1 | import styles from "./style.module.scss"
2 |
3 | const ResultsSnippet = ({ results }) => {
4 | const { title,link, displayLink, snippet, favicon, thumbnail } = results;
5 |
6 | return (
7 |
8 |
9 |
10 |
{displayLink}
11 |
12 |
13 |
14 |
{title}
15 |
{snippet}
16 |
17 | {thumbnail &&
}
18 |
19 |
20 | )
21 | }
22 |
23 | export default ResultsSnippet;
--------------------------------------------------------------------------------
/components/pages/all/Snippet/style.module.scss:
--------------------------------------------------------------------------------
1 | @import '@/app/mixins';
2 |
3 | .resultsSnippet {
4 | text-decoration: none;
5 | background: transparent;
6 | border-radius: 0.5rem;
7 | transition: all 0.3s ease-in-out;
8 | color: var(--color-1);
9 | text-align: left;
10 | padding: 1rem;
11 | margin: 1rem 0;
12 | cursor: pointer;
13 | display: block;
14 |
15 | @include mobile {
16 | margin: 0.75rem 0;
17 | padding: 0.5rem;
18 | }
19 |
20 | &:hover {
21 | background: var(--bg-2);
22 | box-shadow: var(--box-shadow);
23 | transform: scale(1.01);
24 | }
25 |
26 | .head {
27 | font-size: 0.8rem;
28 | margin-bottom: 0.5rem;
29 | display: flex;
30 | flex-direction: row;
31 | align-items: center;
32 |
33 | .favicon {
34 | width: 1rem;
35 | margin-right: 0.5rem;
36 | border-radius: 50%;
37 | }
38 |
39 | .url {
40 | width: 100%;
41 | color: var(--color-1);
42 | overflow: hidden;
43 | text-overflow: ellipsis;
44 | white-space: nowrap;
45 | display: -webkit-box;
46 | -webkit-line-clamp: 3;
47 | -webkit-box-orient: vertical;
48 | }
49 | }
50 | }
51 |
52 | .content {
53 | display: flex;
54 | flex-direction: row;
55 | justify-content: space-between;
56 |
57 | .body {
58 | .title {
59 | color: var(--link-color);
60 | font-weight: normal;
61 | font-size: 1.3rem;
62 | margin-bottom: 0.5rem;
63 | word-break: break-word;
64 | // @include gradient-text(var(--gradient-3));
65 |
66 | @include mobile {
67 | font-size: 1.15rem;
68 | }
69 | }
70 |
71 | .snippet {
72 | font-size: 0.9rem;
73 | color: var(--color-3);
74 | word-break: break-word;
75 | }
76 | }
77 |
78 | .cse {
79 | height: 6rem;
80 | border-radius: 1rem;
81 | margin-left: 2rem;
82 | box-shadow: var(--box-shadow);
83 |
84 | @include mobile {
85 | display: none;
86 | }
87 | }
88 | }
--------------------------------------------------------------------------------
/components/pages/all/SnippetSkeleton/index.js:
--------------------------------------------------------------------------------
1 | import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'
2 | import 'react-loading-skeleton/dist/skeleton.css'
3 | import { ThemeContext } from "@/context/ThemeContext";
4 | import { useContext } from "react";
5 |
6 | const ResultSkeleton = () => {
7 |
8 | const { theme } = useContext(ThemeContext);
9 | const baseColor = theme === "light" ? "#f5f5f5" : "#343434";
10 | const highlightColor = theme === "light" ? "#e5e5e5" : "#565656";
11 |
12 | return (
13 |
14 |
15 | { }
16 | { }
17 | { }
18 | { }
19 |
20 | )
21 | }
22 |
23 | export default ResultSkeleton;
--------------------------------------------------------------------------------
/components/pages/images/Card/index.js:
--------------------------------------------------------------------------------
1 | import styles from "./style.module.scss"
2 |
3 | const Card = ({ results }) => {
4 | const {thumbnail, link, snippet, title} = results;
5 |
6 | return (
7 |
8 |
9 |
10 |
11 |
12 |
{title}
13 |
{snippet}
14 |
15 |
16 | )
17 | }
18 |
19 | export default Card;
--------------------------------------------------------------------------------
/components/pages/images/Card/style.module.scss:
--------------------------------------------------------------------------------
1 | @import '@/app/mixins';
2 |
3 | .card {
4 | cursor: pointer;
5 | transition: all 0.3s ease-in-out;
6 | text-align: left;
7 | display: block;
8 | color: var(--color-1);
9 |
10 | &:hover {
11 | transform: scale(1.05);
12 | .card__image img {
13 | box-shadow: var(--box-shadow);
14 | }
15 | }
16 |
17 | .card__image {
18 | img {
19 | width: 100%;
20 | border-radius: 0.5rem;
21 | box-shadow: var(--box-shadow);
22 | border: 1px solid var(--border-color);
23 | }
24 | }
25 |
26 | &__content {
27 | margin-top: 0.5rem;
28 |
29 | .title {
30 | font-size: 1rem;
31 | font-weight: normal;
32 | margin-bottom: 0.3rem;
33 | overflow: hidden;
34 | text-overflow: ellipsis;
35 | display: -webkit-box;
36 | -webkit-line-clamp: 1;
37 | -webkit-box-orient: vertical;
38 | }
39 |
40 | .snippet {
41 | font-size: 0.75rem;
42 | color: var(--color-2);
43 | overflow: hidden;
44 | text-overflow: ellipsis;
45 | display: -webkit-box;
46 | -webkit-line-clamp: 3;
47 | -webkit-box-orient: vertical;
48 | }
49 | }
50 | }
--------------------------------------------------------------------------------
/components/pages/images/CardSkeleton/index.js:
--------------------------------------------------------------------------------
1 | import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'
2 | import 'react-loading-skeleton/dist/skeleton.css'
3 | import { ThemeContext } from "@/context/ThemeContext";
4 | import { useContext } from "react";
5 |
6 | const CardSkeleton = () => {
7 |
8 | const { theme } = useContext(ThemeContext);
9 | const baseColor = theme === "light" ? "#f5f5f5" : "#343434";
10 | const highlightColor = theme === "light" ? "#e5e5e5" : "#565656";
11 |
12 | return (
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | )
21 | }
22 |
23 | export default CardSkeleton;
24 |
--------------------------------------------------------------------------------
/components/pages/news/NewsCard/index.js:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import styles from "../../videos/VideoCard/videoCard.module.scss";
4 |
5 | const formatTimeStamp = (timeStamp) => {
6 | const date = new Date(timeStamp);
7 |
8 | const monthNames = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
9 |
10 | let year = date.getFullYear();
11 | let month = date.getMonth();
12 | let day = date.getDate();
13 |
14 | month = monthNames[month];
15 | day = day < 10 ? `0${day}` : day;
16 |
17 | return `${day} ${month} ${year}`;
18 | };
19 |
20 |
21 | const VideoCard = ({ results }) => {
22 | const { title, description, thumbnail, publishedBy, publishedAt, url } = results;
23 |
24 | return (
25 | { window.location.href = url }}>
26 |
27 |
28 |
{title}
29 |
{formatTimeStamp(publishedAt)}
30 |
Published by {publishedBy}
31 |
{description.substring(0, 600)}
32 |
33 |
34 | )
35 | };
36 |
37 | export default VideoCard;
--------------------------------------------------------------------------------
/components/pages/videos/CardSkeleton/index.js:
--------------------------------------------------------------------------------
1 | import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'
2 | import 'react-loading-skeleton/dist/skeleton.css'
3 | import styles from "../VideoCard/videoCard.module.scss"
4 | import { ThemeContext } from "@/context/ThemeContext";
5 | import { useContext } from "react";
6 |
7 | const CardSkeleton = () => {
8 |
9 | const { theme } = useContext(ThemeContext);
10 | const baseColor = theme === "light" ? "#f5f5f5" : "#343434";
11 | const highlightColor = theme === "light" ? "#e5e5e5" : "#565656";
12 |
13 | return (
14 |
27 | )
28 | }
29 |
30 | export default CardSkeleton;
31 |
--------------------------------------------------------------------------------
/components/pages/videos/VideoCard/index.js:
--------------------------------------------------------------------------------
1 | import styles from "./videoCard.module.scss";
2 |
3 | const formatTimeStamp = (timeStamp) => {
4 | const date = new Date(timeStamp);
5 |
6 | const monthNames = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
7 |
8 | let year = date.getFullYear();
9 | let month = date.getMonth();
10 | let day = date.getDate();
11 |
12 | month = monthNames[month];
13 | day = day < 10 ? `0${day}` : day;
14 |
15 | return `${day} ${month} ${year}`;
16 | };
17 |
18 |
19 | const VideoCard = ({ results }) => {
20 | const { title, description, thumbnail, publishedBy, publishedAt, link } = results;
21 |
22 | return (
23 |
24 |
25 |
26 |
{title}
27 |
{formatTimeStamp(publishedAt)}
28 |
Published by {publishedBy}
29 |
{description.substring(0, 600)}
30 |
31 |
32 | )
33 | };
34 |
35 | export default VideoCard;
--------------------------------------------------------------------------------
/components/pages/videos/VideoCard/videoCard.module.scss:
--------------------------------------------------------------------------------
1 | @import '@/app/mixins';
2 |
3 | .videoCard {
4 | display: grid;
5 | grid-template-columns: 1fr 2.5fr;
6 | text-align: left;
7 | margin-bottom: 1rem;
8 | gap: 1rem;
9 | border-radius: 0.5rem;
10 | padding: 0.25rem;
11 | transition: all 0.3s;
12 | cursor: pointer;
13 | color: var(--color-1);
14 |
15 | @include mobile {
16 | grid-template-columns: 2fr 3fr;
17 | gap: 0.4rem;
18 |
19 | .content {
20 |
21 | .description,
22 | .publishedBy {
23 | display: none;
24 | }
25 | }
26 | }
27 |
28 | &:hover {
29 | box-shadow: var(--box-shadow);
30 | background: var(--bg-2);
31 | transform: scale(1.03);
32 |
33 | .thumbnail {
34 | box-shadow: none;
35 | }
36 | }
37 |
38 | .thumbnail {
39 | border-radius: 0.4rem;
40 | box-shadow: 2px 2px 0.2rem rgba(0, 0, 0, 0.6);
41 | // border: 1px solid #b3b3b3;
42 | max-width: 100%;
43 |
44 | @media (max-width: 800px) {
45 | width: 100%;
46 | border: none;
47 | }
48 | }
49 |
50 | .content {
51 | padding: 0.75rem 0.5rem;
52 |
53 | @include mobile {
54 | padding: 0.1rem;
55 | }
56 |
57 | .title {
58 | color: var(--link-color);
59 | font-size: 1.3rem;
60 | font-weight: normal;
61 |
62 | @include mobile {
63 | color: var(--color-1);
64 | font-size: 1rem;
65 | border: none;
66 | // ellipsis for 2 lines
67 | display: -webkit-box;
68 | -webkit-line-clamp: 2;
69 | -webkit-box-orient: vertical;
70 | overflow: hidden;
71 | }
72 | }
73 |
74 | .publishedAt {
75 | margin-top: 0.25rem;
76 | color: var(--color-3);
77 | font-size: 0.9rem;
78 |
79 | &::before {
80 | content: 'Published at ';
81 | margin-right: 0.2rem;
82 | }
83 |
84 | @include mobile {
85 | font-size: 0.8rem;
86 | margin-top: 0.5rem;
87 |
88 | &::before {
89 | content: '';
90 | }
91 | }
92 | }
93 |
94 | .publishedBy {
95 | margin-top: 0.25rem;
96 | }
97 |
98 | .description {
99 | margin-top: 0.75rem;
100 | color: #b3b3b3;
101 | font-size: 0.85rem;
102 | word-break: break-all;
103 | // set text overflow to ellipsis for 4 lines
104 | display: -webkit-box;
105 | -webkit-line-clamp: 3;
106 | -webkit-box-orient: vertical;
107 | overflow: hidden;
108 |
109 | @include mobile {
110 | display: none;
111 | }
112 | }
113 | }
114 | }
115 |
116 | .skeleton {
117 | .publishedAt::before {
118 | content: '' !important;
119 | margin-right: 0rem !important
120 | }
121 |
122 | .thumbnail {
123 | @include mobile {
124 | height: 5rem !important;
125 | }
126 | }
127 | }
--------------------------------------------------------------------------------
/components/shared/AccountBtn/index.js:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useSession, signIn, signOut } from 'next-auth/react';
4 | import styles from "./style.module.scss"
5 | import { FaUser } from "react-icons/fa"
6 | import { useState } from 'react';
7 | import { useRouter } from 'next/navigation';
8 |
9 | export default function AccountBtn() {
10 |
11 | const { data: session, status } = useSession();
12 | const router = useRouter();
13 |
14 | if (status === 'loading') {
15 | return (
16 |
17 |
18 |
Loading...
19 |
20 | )
21 | }
22 |
23 | if (session && session.user) {
24 | const UserName = session.user.name || session.user.email.split('@')[0];
25 |
26 | return (
27 | router.push('/account')}>
28 |
29 |
{UserName}
30 |
31 | )
32 | }
33 |
34 | return (
35 | signIn()}>
36 |
37 |
Login
38 |
39 | )
40 | }
--------------------------------------------------------------------------------
/components/shared/AccountBtn/style.module.scss:
--------------------------------------------------------------------------------
1 | @import '@/app/mixins';
2 |
3 | .accountBtn {
4 | border-radius: 2rem;
5 | background: var(--bg);
6 | color: var(--color);
7 | display: inline-flex;
8 | flex-direction: row;
9 | align-items: center;
10 | box-shadow: var(--box-shadow);
11 | border: var(--border-color) solid 1px;
12 | transition: all 0.3s;
13 | cursor: pointer;
14 | float: right;
15 |
16 | &:hover {
17 | transform: scale(1.075);
18 | box-shadow: var(--box-shadow-hover);
19 | }
20 |
21 | img,
22 | svg {
23 | width: 2rem;
24 | height: 2rem;
25 | border-radius: 50%;
26 | }
27 |
28 | svg {
29 | padding: 6px;
30 | }
31 |
32 | .title {
33 | padding: 0 1rem;
34 | font-size: 1.1rem;
35 | }
36 | }
--------------------------------------------------------------------------------
/components/shared/Footer/footer.module.scss:
--------------------------------------------------------------------------------
1 | @import "@/app/mixins";
2 |
3 | html[data-theme="light"] {
4 | .footer{
5 | box-shadow: rgb(0 0 0 / 7%) 0px -2px 6px;
6 | }
7 | }
8 |
9 | .footer {
10 | box-shadow: rgb(0 0 0 / 40%) 0px -2px 6px;
11 | background: var(--bg-1);
12 | border-top: 1px solid rgba(var(--border-color), 0.6);
13 | padding: 1rem 2rem;
14 | margin-top: 2rem;
15 | color: var(--color-2);
16 |
17 | @include mobile {
18 | padding: 1rem 0.5rem;
19 | font-size: 0.85rem;
20 | }
21 |
22 | &__text {
23 | display: flex;
24 | justify-content: space-between;
25 | align-items: center;
26 |
27 | @include mobile {
28 | flex-direction: column-reverse;
29 | gap: 1rem;
30 | }
31 |
32 | .logo {
33 | background: var(--gradient-1);
34 | display: inline-block;
35 | -webkit-background-clip: text;
36 | -webkit-text-fill-color: transparent;
37 | background-clip: text;
38 | text-fill-color: transparent;
39 | transition: all 0.3s;
40 | }
41 |
42 | a {
43 | color: var(--link-color);
44 | text-decoration: none;
45 | }
46 |
47 | .location {
48 | color: var(--link-color);
49 | }
50 | }
51 | }
--------------------------------------------------------------------------------
/components/shared/Footer/index.js:
--------------------------------------------------------------------------------
1 | // "use client";
2 |
3 | import styles from "./footer.module.scss";
4 | import ThemeBtn from "../ThemeBtn";
5 | import GhStarBtn from "../GhStarBtn";
6 |
7 | export default function Footer() {
8 |
9 | return (
10 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/components/shared/GhStarBtn/ghStarBtn.module.scss:
--------------------------------------------------------------------------------
1 | .ghStarBtn {
2 | color: var(--color-2) !important;
3 | border: 1px solid var(--border-color);
4 | border-radius: 0.3rem;
5 | box-shadow: var(--box-shadow);
6 | background: var(--bg);
7 | cursor: pointer;
8 | transition: all 0.3s;
9 | overflow: hidden;
10 | display: flex;
11 |
12 | &:hover {
13 | transform: scale(1.03);
14 | }
15 |
16 | &>span {
17 | padding: 0.3rem 0.8rem;
18 |
19 | }
20 |
21 | &>span:nth-child(2) {
22 | background-color: var(--bg-2);
23 | }
24 |
25 | }
--------------------------------------------------------------------------------
/components/shared/GhStarBtn/index.js:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect, useState } from "react";
4 | import styles from "./ghStarBtn.module.scss";
5 | import { FaStar } from "react-icons/fa";
6 | import axios from "axios";
7 |
8 |
9 | const GhStarBtn = () => {
10 | const [stargazers_count, setStargazers_count] = useState('--');
11 |
12 | useEffect(() => {
13 | axios.get("https://api.github.com/repos/devxprite/searchex")
14 | .then(res => { setStargazers_count(res.data.stargazers_count || '--'); })
15 | .catch(err => { console.log(err); })
16 | }, []);
17 |
18 | return (
19 |
20 |
21 | Star Us
22 |
23 | {stargazers_count}
24 |
25 | );
26 | }
27 |
28 | export default GhStarBtn;
--------------------------------------------------------------------------------
/components/shared/Loader/index.js:
--------------------------------------------------------------------------------
1 | import styles from "./loader.module.scss"
2 |
3 | export default function Loader() {
4 | return (
5 |
8 | )
9 | }
--------------------------------------------------------------------------------
/components/shared/Loader/loader.module.scss:
--------------------------------------------------------------------------------
1 | @import "@/app/mixins";
2 |
3 | .loader {
4 | margin-top: 2rem;
5 |
6 | .roller {
7 | display: inline-block;
8 | position: relative;
9 | width: 80px;
10 | height: 80px;
11 | }
12 |
13 | .roller div {
14 | animation: roller 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
15 | transform-origin: 40px 40px;
16 | }
17 |
18 | .roller div:after {
19 | content: " ";
20 | display: block;
21 | position: absolute;
22 | width: 7px;
23 | height: 7px;
24 | border-radius: 50%;
25 | // background: var(--color-2);
26 | background: transparent;
27 | margin: -4px 0 0 -4px;
28 | }
29 |
30 | .roller div:nth-child(1) {
31 | animation-delay: -0.036s;
32 | }
33 |
34 | .roller div:nth-child(1):after {
35 | top: 63px;
36 | left: 63px;
37 | }
38 |
39 | .roller div:nth-child(2) {
40 | animation-delay: -0.072s;
41 | }
42 |
43 | .roller div:nth-child(2):after {
44 | top: 68px;
45 | left: 56px;
46 | }
47 |
48 | .roller div:nth-child(3) {
49 | animation-delay: -0.108s;
50 | }
51 |
52 | .roller div:nth-child(3):after {
53 | top: 71px;
54 | left: 48px;
55 | }
56 |
57 | .roller div:nth-child(4) {
58 | animation-delay: -0.144s;
59 | }
60 |
61 | .roller div:nth-child(4):after {
62 | top: 72px;
63 | left: 40px;
64 | }
65 |
66 | .roller div:nth-child(5) {
67 | animation-delay: -0.18s;
68 | }
69 |
70 | .roller div:nth-child(5):after {
71 | top: 71px;
72 | left: 32px;
73 | }
74 |
75 | .roller div:nth-child(6) {
76 | animation-delay: -0.216s;
77 | }
78 |
79 | .roller div:nth-child(6):after {
80 | top: 68px;
81 | left: 24px;
82 | }
83 |
84 | .roller div:nth-child(7) {
85 | animation-delay: -0.252s;
86 | }
87 |
88 | .roller div:nth-child(7):after {
89 | top: 63px;
90 | left: 17px;
91 | }
92 |
93 | .roller div:nth-child(8) {
94 | animation-delay: -0.288s;
95 | }
96 |
97 | .roller div:nth-child(8):after {
98 | top: 56px;
99 | left: 12px;
100 | }
101 |
102 |
103 | @keyframes roller {
104 | 0% {
105 | transform: rotate(0deg);
106 | }
107 |
108 | 100% {
109 | transform: rotate(360deg);
110 | }
111 | }
112 | }
--------------------------------------------------------------------------------
/components/shared/LoadmoreBtn/index.js:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import styles from "./loadmorebtn.module.scss";
4 |
5 | export default function LoadmoreBtn({ onClick, isLoading }) {
6 | return (
7 |
8 |
9 | {isLoading ? 'Loading...' : 'Load more Results'}
10 |
11 |
12 | )
13 | }
--------------------------------------------------------------------------------
/components/shared/LoadmoreBtn/loadmorebtn.module.scss:
--------------------------------------------------------------------------------
1 | @import '@/app/mixins';
2 |
3 | .loadmoreBtn {
4 |
5 | button {
6 | padding: 0.5rem 2.5rem;
7 | font-size: 1.1rem;
8 | border-radius: 2rem;
9 | background: rgba(var(--link-color), 0.05);
10 | border: 2px solid var(--link-color);
11 | color: var(--link-color);
12 | margin-top: 1.5rem;
13 | transition: all 0.3s;
14 | box-shadow: var(--box-shadow);
15 | cursor: pointer;
16 |
17 | @include mobile {
18 | padding: 0.35rem 1rem;
19 | font-size: 1rem;
20 | }
21 |
22 | &:hover {
23 | transform: scale(1.05);
24 | background: rgba(var(--link-color), 0.2);
25 | box-shadow: var(--box-shadow-hover);
26 | }
27 |
28 | &:focus {
29 | outline: none;
30 | }
31 | }
32 | }
--------------------------------------------------------------------------------
/components/shared/NoResults/index.js:
--------------------------------------------------------------------------------
1 | import styles from "./noResults.module.scss"
2 |
3 | export default function NoResults({ query, type="documents" }) {
4 | return (
5 |
6 |
Your search - {query} - did not match any {type}.
7 |
Suggestions:
8 |
9 | Make sure that all words are spelled correctly.
10 | Try different keywords.
11 | Try more general keywords.
12 |
13 |
14 |
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/components/shared/NoResults/noResults.module.scss:
--------------------------------------------------------------------------------
1 | @import "@/app/mixins";
2 |
3 | .noResults{
4 | text-align: left;
5 | color: var(--color);
6 |
7 | h1{
8 | font-weight: normal;
9 | font-weight: normal;
10 | font-size: 1.1rem;
11 | margin: 2rem 0 1rem 0;
12 |
13 | @include mobile{
14 | font-size: 1rem;
15 | }
16 | }
17 |
18 | p{
19 | @include mobile{
20 | font-size: 0.95rem;
21 | }
22 | }
23 |
24 | ul{
25 | color: var(--color-2);
26 | font-size: 0.95rem;
27 | margin-top: 1rem;
28 | margin-left: 1rem;
29 |
30 | @include mobile{
31 | font-size: 0.8rem;
32 | }
33 | }
34 |
35 |
36 | }
--------------------------------------------------------------------------------
/components/shared/SearchBar/index.js:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import styles from "./style.module.scss"
4 | import { FaSearch } from "react-icons/fa";
5 | import { useRouter, usePathname, useSearchParams } from 'next/navigation';
6 | import { useState, useEffect, useRef } from 'react';
7 | import Autosuggest from 'react-autosuggest';
8 |
9 |
10 | const fetchSuggestions = async (search, signal) => {
11 |
12 | const res = await fetch(`https://auto-suggest-queries.p.rapidapi.com/suggestqueries?query=${search}`, {
13 | "method": "GET",
14 | "headers": {
15 | "X-RapidAPI-Key": process.env.NEXT_PUBLIC_RAPID_API_KEY,
16 | "X-RapidAPI-Host": process.env.NEXT_PUBLIC_RAPID_API_HOST
17 | },
18 | signal
19 | });
20 |
21 | const data = await res.json() || [];
22 | return data;
23 | }
24 |
25 | const renderSuggestion = suggestion => (
26 |
27 | {suggestion}
28 |
29 | );
30 |
31 | const renderInputComponent = inputProps => {
32 | delete inputProps.key;
33 | return (
34 |
35 |
36 |
37 |
38 |
39 |
40 | );
41 | }
42 |
43 |
44 | const SearchBar = (props) => {
45 |
46 | const router = useRouter();
47 | const pathname = usePathname();
48 | const searchParams = useSearchParams();
49 | const inputRef = useRef();
50 |
51 | const [search, setSearch] = useState(props.value || "");
52 | const [suggestions, setSuggestions] = useState([]);
53 |
54 |
55 | useEffect(() => {
56 | setSearch(searchParams.get("q") || "");
57 | }, [pathname]);
58 |
59 | useEffect(() => {
60 | const controller = new AbortController();
61 | const { signal } = controller;
62 |
63 | fetchSuggestions(search, signal).then(data => {
64 | setSuggestions(data);
65 | }).catch(err => false);
66 |
67 | return () => { controller.abort(); }
68 | }, [search]);
69 |
70 | const handleSearch = (e) => {
71 | e.preventDefault();
72 | inputRef.current.blur();
73 |
74 | if (pathname === "/") {
75 | router.push(`/search?q=${search}`);
76 | } else {
77 | router.push(`${pathname}?q=${search}`);
78 | }
79 |
80 | fetch(`/api/history?q=${search}&p=${pathname}&t=${Date.now()}`).catch(err => false);
81 | }
82 |
83 | const onSuggestionSelected = (e, { suggestion }) => {
84 | setSearch(suggestion);
85 | inputRef.current.blur();
86 |
87 | if (pathname === "/") {
88 | router.push(`/search?q=${suggestion}`);
89 | } else {
90 | router.push(`${pathname}?q=${suggestion}`);
91 | }
92 |
93 | }
94 |
95 | return (
96 |
123 | );
124 | }
125 |
126 | export default SearchBar;
--------------------------------------------------------------------------------
/components/shared/SearchBar/style.module.scss:
--------------------------------------------------------------------------------
1 | @import '@/app/mixins';
2 |
3 | .searchBar {
4 | width: 95%;
5 | margin: 0 auto;
6 | max-width: 45rem;
7 | position: relative;
8 | border-radius: 5rem;
9 | background: transparent;
10 | border: 2px solid rgba(#808080, 0.65);
11 | box-shadow: var(--box-shadow);
12 | transition: all 0.3s;
13 | background: var(--bg-1);
14 |
15 | &:focus-within {
16 | $border-color: #4dafff;
17 | border-color: darken($border-color, 10%);
18 |
19 | button {
20 | background: darken($border-color, 20%);
21 |
22 | &:hover {
23 | background: darken($border-color, 30%);
24 | }
25 |
26 | svg {
27 | fill: var(--color);
28 | }
29 | }
30 | }
31 |
32 | &__open:focus-within {
33 | border-radius: 1rem 1rem 0 0;
34 |
35 | button{
36 | border-radius: 0.125rem 0.75rem 0.125rem 0.125rem;
37 | }
38 | }
39 |
40 | &__container {
41 | display: flex;
42 | flex-direction: row;
43 | justify-content: space-evenly;
44 |
45 | input {
46 | width: 100%;
47 | font-size: 1.5rem;
48 | color: var(--color);
49 | padding: 0.65rem 0.65rem .65rem 2rem;
50 | background: transparent;
51 | border: none;
52 |
53 | @include mobile {
54 | font-size: 1.125rem;
55 | padding: 0.5rem 0.5rem .5rem 1.5rem;
56 | }
57 |
58 | &:focus {
59 | outline: none;
60 | }
61 | }
62 |
63 | button {
64 | border: none;
65 | border-radius: 5rem;
66 | display: flex;
67 | justify-content: center;
68 | align-items: center;
69 | background: transparent;
70 | overflow: hidden;
71 | margin: 0.1rem;
72 | transition: all .3s;
73 | cursor: pointer;
74 |
75 | svg {
76 | width: 3rem;
77 | height: 1.25rem;
78 | fill: var(--color-3);
79 |
80 | @include mobile {
81 | width: 2.8rem;
82 | height: 1.15rem;
83 | }
84 | }
85 | }
86 | }
87 |
88 | .suggestionsContainer {
89 | background: var(--bg-1);
90 | max-height: 14rem;
91 | overflow: hidden scroll;
92 | position: absolute;
93 | top: 101%;
94 | width: 100%;
95 | left: 0;
96 | border-radius: 0 0 0.5rem 0.5rem;
97 | z-index: 2;
98 | box-shadow: var(--box-shadow-hover);
99 | transition: all 0.2s;
100 |
101 | &::-webkit-scrollbar-track {
102 | background: transparent;
103 | }
104 |
105 | &__open {
106 | border: 1px solid var(--border-color);
107 | }
108 |
109 | .suggestion {
110 | font-size: 1.2rem;
111 | padding: 0.5rem 1rem;
112 | text-align: left;
113 | cursor: pointer;
114 | color: var(--color-2);
115 |
116 | @include mobile {
117 | font-size: 1rem;
118 | }
119 |
120 | .searchIcon {
121 | font-size: 1rem;
122 | fill: var(--color-3);
123 | margin-right: 0.5rem;
124 | }
125 | }
126 |
127 | .suggestionHighlighted {
128 | background: #000;
129 | color: var(--link-color);
130 | }
131 | }
132 | }
--------------------------------------------------------------------------------
/components/shared/ThemeBtn/index.js:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { BsMoon, BsSun } from "react-icons/bs";
4 | import styles from "./themeBtn.module.scss";
5 | import { ThemeContext } from "@/context/ThemeContext";
6 | import { useContext } from "react";
7 |
8 | export default function ThemeBtn() {
9 | const { toggleTheme, theme } = useContext(ThemeContext);
10 |
11 | return (
12 |
13 |
14 | {theme === "light" ? : } {theme === "light" ? "Dark" : "Light"} Theme
15 |
16 |
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/components/shared/ThemeBtn/themeBtn.module.scss:
--------------------------------------------------------------------------------
1 | @import "@/app/mixins";
2 |
3 | .theme {
4 | padding: 0.3rem 0.8rem;
5 | border: 1px solid var(--border-color);
6 | border-radius: 0.3rem;
7 | box-shadow: var(--box-shadow);
8 | background: var(--bg);
9 | cursor: pointer;
10 | transition: all 0.3s;
11 |
12 | &:hover {
13 | transform: scale(1.03);
14 | }
15 |
16 | @include mobile {
17 | // display: none;
18 | }
19 | }
--------------------------------------------------------------------------------
/context/AuthContext.js:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { SessionProvider } from "next-auth/react";
4 | import { Session } from "next-auth";
5 |
6 | export default function AuthContext({ session, children }) {
7 | return (
8 |
9 | {children}
10 |
11 | );
12 | }
--------------------------------------------------------------------------------
/context/ThemeContext.js:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { createContext, useState, useEffect } from 'react';
4 |
5 | export const ThemeContext = createContext();
6 |
7 | export const ThemeProvider = ({ children }) => {
8 | const [theme, setTheme] = useState();
9 |
10 | useEffect(() => {
11 | const localTheme = localStorage.getItem('theme');
12 | console.log('Get LocalStorage theme', localTheme);
13 |
14 | setTheme(localTheme || 'dark');
15 | }, []);
16 |
17 | const toggleTheme = () => {
18 | setTheme(theme === 'light' ? 'dark' : 'light');
19 | };
20 |
21 | useEffect(() => {
22 | // if theme available and not equal to undefined or null
23 | if (theme && theme !== 'undefined' && theme !== 'null') {
24 | localStorage.setItem('theme', theme);
25 | console.log('Set LocalStorage theme', theme);
26 | document.documentElement.setAttribute('data-theme', theme);
27 | }
28 | }, [theme]);
29 |
30 | return (
31 |
32 | {children}
33 |
34 | );
35 | };
36 |
--------------------------------------------------------------------------------
/hooks/useLocalStorage.js:
--------------------------------------------------------------------------------
1 |
2 | import { useState, useEffect } from "react";
3 |
4 | const useLocalStorage = (key, defaultValue) => {
5 | const [value, setValue] = useState(() => {
6 | let currentValue;
7 |
8 | try {
9 | currentValue = JSON.parse(
10 | localStorage.getItem(key) || String(defaultValue)
11 | );
12 | } catch (error) {
13 | currentValue = defaultValue;
14 | }
15 |
16 | return currentValue;
17 | });
18 |
19 | useEffect(() => {
20 | localStorage.setItem(key, JSON.stringify(value));
21 | }, [value, key]);
22 |
23 | return [value, setValue];
24 | };
25 |
26 | export default useLocalStorage;
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "paths": {
5 | "@/*": ["./*"]
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | experimental: {
4 | appDir: true,
5 | },
6 | }
7 |
8 | module.exports = nextConfig
9 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "searchex",
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-auth/mongodb-adapter": "^1.1.1",
13 | "axios": "^1.4.0",
14 | "lodash": "^4.17.21",
15 | "mongodb": "^5.0.1",
16 | "next": "^13.4.7",
17 | "next-auth": "^4.22.1",
18 | "react": "18.2.0",
19 | "react-autosuggest": "^10.1.0",
20 | "react-dom": "18.2.0",
21 | "react-icons": "^4.7.1",
22 | "react-loading-skeleton": "^3.1.1",
23 | "sass": "^1.58.0"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/pages/api/auth/[...nextauth].js:
--------------------------------------------------------------------------------
1 | import NextAuth from "next-auth"
2 | import GithubProvider from "next-auth/providers/github";
3 | import Auth0Provider from "next-auth/providers/auth0";
4 |
5 | import { MongoDBAdapter } from "@next-auth/mongodb-adapter"
6 | import clientPromise from "@/utils/db"
7 |
8 | export const authOptions = {
9 | secret: process.env.NEXTAUTH_SECRET,
10 | providers: [
11 | GithubProvider({
12 | clientId: process.env.GITHUB_ID,
13 | clientSecret: process.env.GITHUB_SECRET
14 | }),
15 | Auth0Provider({
16 | clientId: process.env.AUTH0_CLIENT_ID,
17 | clientSecret: process.env.AUTH0_CLIENT_SECRET,
18 | issuer: process.env.AUTH0_ISSUER_BASE_URL,
19 | name: "Email and Password"
20 | }),
21 | ],
22 | adapter: MongoDBAdapter(clientPromise),
23 | theme: {
24 | colorScheme: "dark",
25 | }
26 | }
27 |
28 | export default NextAuth(authOptions)
--------------------------------------------------------------------------------
/pages/api/card.js:
--------------------------------------------------------------------------------
1 | export default async (req, res) => {
2 | const q = req.query.q;
3 |
4 | console.log(`Calling Cards API: q=${q}`);
5 |
6 | try {
7 |
8 | const wikipediaResult = await fetch(`https://en.wikipedia.org/api/rest_v1/page/summary/${q}`)
9 | .then((response) => response.json())
10 |
11 | const isWikipedia = wikipediaResult && wikipediaResult?.description != "Topics referred to by the same term" && wikipediaResult.title !== 'Not found.';
12 |
13 | if (isWikipedia) {
14 | res.status(200).json({
15 | image: wikipediaResult?.thumbnail?.source || null,
16 | title: wikipediaResult.title,
17 | description: wikipediaResult.description,
18 | content: wikipediaResult.extract_html,
19 | type: "wiki"
20 | });
21 |
22 | return;
23 | }
24 |
25 | const openAIResult = await fetch("https://api.openai.com/v1/completions", {
26 | method: 'POST',
27 | headers: {
28 | 'Content-Type': 'application/json',
29 | 'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`
30 | },
31 | body: JSON.stringify({
32 | "model": "text-davinci-003",
33 | "prompt": `Write a answer for this query if any answer not available then return na. Query:\n ${q}\n Answer: \n`,
34 | "temperature": 0.7,
35 | "max_tokens": 256,
36 | "top_p": 1,
37 | "frequency_penalty": 0,
38 | "presence_penalty": 0
39 | }),
40 | redirect: 'follow'
41 | })
42 | .then((response) => response.json())
43 | .catch((error) => false);
44 |
45 | if (openAIResult?.choices?.length > 0) {
46 |
47 | if (openAIResult.choices[0].text.toLowerCase() === "na") {
48 | res.status(404).json({
49 | message: "Not found!"
50 | })
51 | return;
52 | }
53 |
54 | res.status(200).json({
55 | image: null,
56 | title: q,
57 | content: openAIResult.choices[0].text.replace(/(?:\r\n|\r|\n)/g, ' '),
58 | description: null,
59 | type: "ai"
60 | });
61 | return;
62 | }
63 |
64 | res.status(404).json({
65 | message: "Not found!"
66 | })
67 |
68 | } catch (error) {
69 | res.status(500).json({
70 | message: "Internal server error!"
71 | })
72 | }
73 | }
--------------------------------------------------------------------------------
/pages/api/history.js:
--------------------------------------------------------------------------------
1 | import { getServerSession } from "next-auth/next";
2 | import { authOptions } from "./auth/[...nextauth]";
3 | import mongoClint from "@/utils/db";
4 |
5 | export default async function handler(req, res) {
6 |
7 | const session = await getServerSession(req, res, authOptions);
8 |
9 | if (!session) {
10 | res.status(401).json({ message: 'Not authenticated' })
11 | return
12 | }
13 |
14 | const { email } = session.user;
15 | const { q: query, t: localTimestamp,p: path } = req.query;
16 |
17 | if (!query || !localTimestamp || !email) {
18 | res.status(400).json({ message: 'Bad request' })
19 | return
20 | }
21 |
22 | console.log(`Calling History API with email: ${email}, query: ${query}, path: ${path}, localTimestamp: ${localTimestamp}`);
23 |
24 | const client = await mongoClint;
25 | const db = client.db();
26 |
27 | await db.collection('history').insertOne({
28 | email,
29 | query,
30 | path,
31 | localTimestamp
32 | })
33 |
34 | res.status(204).end()
35 | }
36 |
--------------------------------------------------------------------------------
/pages/api/search/images.js:
--------------------------------------------------------------------------------
1 | import _ from "lodash";
2 | import fetchImageResults from "@/utils/fetchImageResults";
3 | import fetchVideosResults from "@/utils/fetchVideosResults";
4 |
5 | export default async (req, res) => {
6 | const q = req.query.q;
7 | const page = Number(req.query.page) || 1;
8 |
9 | console.log(`Calling Image API: q=${q}, page=${page}`);
10 |
11 | const [imageResults, videosResults] = await Promise.all([
12 | fetchImageResults(q, page),
13 | fetchVideosResults(q, page, 4)
14 | ]);
15 |
16 | const RESULTS = [
17 | ...imageResults,
18 | ...videosResults
19 | ];
20 |
21 | if (RESULTS.length == 0) {
22 | res.status(200).json([]);
23 | return;
24 | }
25 |
26 | res.status(200).json(_.shuffle(RESULTS));
27 | }
28 |
--------------------------------------------------------------------------------
/pages/api/search/index.js:
--------------------------------------------------------------------------------
1 | import _ from "lodash";
2 | import fetchSearchResults from "@/utils/fetchSearchResults";
3 | import fetchNewsResults from "@/utils/fetchNewsResults";
4 | import fetchVideosResults from "@/utils/fetchVideosResults";
5 |
6 | export default async (req, res) => {
7 | const q = req.query.q;
8 | const page = Number(req.query.page || 1)
9 | const start = (page - 1) * 10 + 1;
10 |
11 | console.log(`Calling Search API: q=${q}, start=${start}`);
12 |
13 | const [newsResults, searchResults, videosResults] = await Promise.all([
14 | fetchNewsResults(q, page, 7),
15 | fetchSearchResults(q, start),
16 | fetchVideosResults(q, page, 4)
17 | ]);
18 |
19 | const RESULTS = [
20 | ...newsResults,
21 | ...searchResults,
22 | ...videosResults
23 | ];
24 |
25 | if (RESULTS.length == 0) {
26 | res.status(200).json([]);
27 | return;
28 | }
29 |
30 | res.status(200).json(_.shuffle(RESULTS));
31 | }
--------------------------------------------------------------------------------
/pages/api/search/news.js:
--------------------------------------------------------------------------------
1 | export default async (req, res) => {
2 | const q = req.query.q;
3 | const page = Number(req.query.page) || 1;
4 |
5 | console.log(`Calling News API: q=${q}, start=${page}`);
6 |
7 | const response = await fetch(`https://newsapi.org/v2/everything?q=${q}&apiKey=${process.env.NEWS_API_KEY}&page=${page}`);
8 |
9 | const repsonseJson = await response.json();
10 |
11 | const articles = repsonseJson.articles.map((item,i ) => {
12 | return {
13 | title: item.title,
14 | description: item.content,
15 | thumbnail: item.urlToImage,
16 | publishedBy: item.source.name,
17 | publishedAt: item.publishedAt,
18 | url: item.url,
19 | };
20 | });
21 |
22 | // const RESULTS = await fetchNewsResults(q, page);
23 |
24 | res.status(200).json(articles);
25 | }
--------------------------------------------------------------------------------
/pages/api/search/videos.js:
--------------------------------------------------------------------------------
1 | export default async (req, res) => {
2 | const q = req.query.q;
3 | const page = req.query.page || "";
4 |
5 | console.log(`Calling Videos API: q=${q}, start=${page}`);
6 |
7 | const response = await fetch(`https://www.googleapis.com/youtube/v3/search?part=snippet&maxResults=200&q=${q}&type=video&key=${process.env.YOUTUBE_API_KEY}&pageToken=${page}`).catch((err) => {
8 | console.log(err);
9 | res.status(500).json({ error: "Internal Server Error" });
10 | });
11 |
12 | const repsonseJson = await response.json();
13 |
14 | // console.log(repsonseJson);
15 |
16 | if (repsonseJson?.items?.length === 0) {
17 | res.status(200).json({
18 | videos: [],
19 | pageInfo: {},
20 | });
21 | return;
22 | }
23 |
24 | const videos = repsonseJson.items.map((item) => {
25 | return {
26 | id: item.id.videoId,
27 | title: item.snippet.title,
28 | description: item.snippet.description,
29 | thumbnail: item.snippet.thumbnails.medium.url,
30 | publishedBy: item.snippet.channelTitle,
31 | publishedAt: item.snippet.publishedAt,
32 | link: `https://www.youtube.com/watch?v=${item.id.videoId}`,
33 | };
34 | });
35 |
36 | const pageInfo = {
37 | nextPageToken: repsonseJson.nextPageToken,
38 | totalResults: repsonseJson.pageInfo.totalResults,
39 | resultsPerPage: repsonseJson.pageInfo.resultsPerPage,
40 | };
41 |
42 | res.status(200).json({
43 | videos,
44 | pageInfo,
45 | });
46 |
47 | }
--------------------------------------------------------------------------------
/public/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devxprite/SearchEx/0bddb9f67f2833eee71b61258caf6c6406cd3916/public/android-chrome-192x192.png
--------------------------------------------------------------------------------
/public/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devxprite/SearchEx/0bddb9f67f2833eee71b61258caf6c6406cd3916/public/android-chrome-512x512.png
--------------------------------------------------------------------------------
/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devxprite/SearchEx/0bddb9f67f2833eee71b61258caf6c6406cd3916/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devxprite/SearchEx/0bddb9f67f2833eee71b61258caf6c6406cd3916/public/favicon-16x16.png
--------------------------------------------------------------------------------
/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devxprite/SearchEx/0bddb9f67f2833eee71b61258caf6c6406cd3916/public/favicon-32x32.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devxprite/SearchEx/0bddb9f67f2833eee71b61258caf6c6406cd3916/public/favicon.ico
--------------------------------------------------------------------------------
/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devxprite/SearchEx/0bddb9f67f2833eee71b61258caf6c6406cd3916/public/favicon.png
--------------------------------------------------------------------------------
/public/images/home.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devxprite/SearchEx/0bddb9f67f2833eee71b61258caf6c6406cd3916/public/images/home.png
--------------------------------------------------------------------------------
/public/images/home_old.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devxprite/SearchEx/0bddb9f67f2833eee71b61258caf6c6406cd3916/public/images/home_old.png
--------------------------------------------------------------------------------
/public/images/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/public/images/page_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devxprite/SearchEx/0bddb9f67f2833eee71b61258caf6c6406cd3916/public/images/page_1.png
--------------------------------------------------------------------------------
/public/images/page_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devxprite/SearchEx/0bddb9f67f2833eee71b61258caf6c6406cd3916/public/images/page_2.png
--------------------------------------------------------------------------------
/public/images/page_3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devxprite/SearchEx/0bddb9f67f2833eee71b61258caf6c6406cd3916/public/images/page_3.png
--------------------------------------------------------------------------------
/public/images/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devxprite/SearchEx/0bddb9f67f2833eee71b61258caf6c6406cd3916/public/images/screenshot.png
--------------------------------------------------------------------------------
/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devxprite/SearchEx/0bddb9f67f2833eee71b61258caf6c6406cd3916/public/logo.png
--------------------------------------------------------------------------------
/public/site.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "SearchEx",
3 | "short_name": "SearchEx",
4 | "icons": [
5 | {
6 | "src": "/android-chrome-192x192.png",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "/android-chrome-512x512.png",
12 | "sizes": "512x512",
13 | "type": "image/png"
14 | }
15 | ],
16 | "theme_color": "#161616",
17 | "background_color": "#ffffff",
18 | "display": "standalone"
19 | }
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/utils/db.js:
--------------------------------------------------------------------------------
1 | import { MongoClient } from 'mongodb'
2 |
3 | if (!process.env.MONGODB_HOST) {
4 | throw new Error('Invalid/Missing environment variable: "MONGODB_HOST"')
5 | }
6 |
7 | const db_user = process.env.MONGODB_USERNAME;
8 | const db_pass = process.env.MONGODB_PASSWORD;
9 | const db_name = process.env.MONGODB_DB;
10 | const db_host = process.env.MONGODB_HOST;
11 |
12 | const uri = `mongodb+srv://${db_user}:${db_pass}@${db_host}/${db_name}?retryWrites=true&w=majority`
13 | const options = {}
14 |
15 | let client = null;
16 | let clientPromise = null;
17 |
18 | if (process.env.NODE_ENV === 'development') {
19 | if (!global._mongoClientPromise) {
20 | client = new MongoClient(uri, options)
21 | global._mongoClientPromise = client.connect()
22 | }
23 | clientPromise = global._mongoClientPromise
24 | } else {
25 | client = new MongoClient(uri, options)
26 | clientPromise = client.connect()
27 | }
28 |
29 | export default clientPromise
--------------------------------------------------------------------------------
/utils/fetchImageResults.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import _ from "lodash";
3 |
4 | const fetchImageResults = (q, page) => {
5 | const GOOGLE_API_KEY = _.sample(process.env.GOOGLE_API_KEY.split(';'));
6 | const start = (page - 1) * 10 + 1;
7 |
8 | return new Promise((resolve, reject) => {
9 | axios.get(`https://www.googleapis.com/customsearch/v1?key=${GOOGLE_API_KEY}&cx=${process.env.GOOGLE_API_CX}&q=${q}&start=${start}&searchType=image`)
10 | .then(res => {
11 | const responseItems = res.data.items;
12 |
13 | if (!responseItems) {
14 | resolve([]);
15 | return;
16 | }
17 |
18 | const items = responseItems.map(item => {
19 | return {
20 | link: item.link,
21 | title: item.title,
22 | snippet: item.snippet,
23 | thumbnail: item.image?.thumbnailLink || item.link,
24 | htmlSnippet: item.htmlSnippet,
25 | htmlTitle: item.htmlTitle,
26 | }
27 | })
28 |
29 | resolve(items);
30 | })
31 | .catch(error => {
32 | console.log(error);
33 | }).finally(() => {
34 | resolve([])
35 | });
36 | })
37 | }
38 |
39 | export default fetchImageResults;
--------------------------------------------------------------------------------
/utils/fetchNewsResults.js:
--------------------------------------------------------------------------------
1 | import axios from "axios"
2 |
3 | const fetchNewsResults = (q, page, max = 60) => {
4 | return new Promise((resolve, reject) => {
5 |
6 | axios
7 | .get(`https://newsapi.org/v2/everything?q=${q}&page=${page}&apiKey=${process.env.NEWS_API_KEY}`)
8 | .then(res => {
9 | const responseItems = res.data.articles;
10 |
11 | if (!responseItems) {
12 | resolve([]);
13 | return;
14 | }
15 |
16 | const items =
17 | responseItems.slice(0, max)
18 | .map((item, i) => ({
19 | title: item.title.length > 70 ? item.title.substring(0, 70) + "..." : item.title,
20 | link: item.url,
21 | displayLink: item.url.replace(/(^\w+:|^)\/\//, '').split('/')[0],
22 | snippet: item.content.length > 150 ? item.content.substring(0, 150) + "..." : item.description,
23 | thumbnail: item.urlToImage,
24 | favicon: `https://www.google.com/s2/favicons?domain=${item.url}&sz=${256}`,
25 | }));
26 | resolve(items);
27 | })
28 | .catch(error => {
29 | console.log(error);
30 | resolve([]);
31 | });
32 | })
33 | }
34 |
35 | export default fetchNewsResults
--------------------------------------------------------------------------------
/utils/fetchSearchResults.js:
--------------------------------------------------------------------------------
1 | import _ from "lodash";
2 |
3 | const fetchSearchResults = (q, start) => {
4 | const GOOGLE_API_KEY = _.sample(process.env.GOOGLE_API_KEY.split(';'));
5 |
6 | return new Promise((resolve, reject) => {
7 | fetch(`https://www.googleapis.com/customsearch/v1?key=${GOOGLE_API_KEY}&cx=${process.env.GOOGLE_API_CX}&q=${q}&start=${start}`)
8 | .then(response => response.json())
9 | .then(responseJson => {
10 | const responseItems = responseJson.items;
11 |
12 | if (!responseItems) {
13 | resolve([]);
14 | return;
15 | }
16 |
17 | const items = responseItems.map((item, i) => ({
18 | title: item.title,
19 | link: item.formattedUrl,
20 | displayLink: item.displayLink,
21 | snippet: item.snippet,
22 | thumbnail: (i == 0 && item.pagemap?.cse_thumbnail?.[0]?.src || null),
23 | favicon: `https://www.google.com/s2/favicons?domain=${item.link}&sz=${256}`
24 | }));
25 | resolve(items);
26 | })
27 | .catch(error => {
28 | console.log(error);
29 | reject([]);
30 | });
31 | })
32 | }
33 |
34 | export default fetchSearchResults;
--------------------------------------------------------------------------------
/utils/fetchVideosResults.js:
--------------------------------------------------------------------------------
1 | const fetchVideosResults = (q, page, max = 60) => {
2 | if (page > 8) return Promise.resolve([]);
3 |
4 | return new Promise((resolve, reject) => {
5 | fetch(`https://www.googleapis.com/youtube/v3/search?part=snippet&maxResults=50&q=${q}&type=video&order=viewCount&key=${process.env.YOUTUBE_API_KEY}&safeSearch=moderate`)
6 | .then(response => response.json())
7 | .then(responseJson => {
8 | const responseItems = responseJson.items;
9 |
10 | if (!responseItems) {
11 | resolve([]);
12 | return;
13 | }
14 |
15 | const restrictedWords = ['#shorts', '#shorts'];
16 | const items = responseItems
17 | .filter(item => !restrictedWords.some(word => item.snippet.title.toLowerCase().includes(word)))
18 | .slice((page - 1) * max, (page - 1) * max + max)
19 | .map((item, i) => ({
20 | title: item.snippet.title,
21 | link: `https://www.youtube.com/watch?v=${item.id.videoId}`,
22 | displayLink: "youtube.com",
23 | snippet: item.snippet.description,
24 | thumbnail: item.snippet.thumbnails.medium.url,
25 | favicon: `https://www.google.com/s2/favicons?domain=https://www.youtube.com&sz=${256}`,
26 | }));
27 | resolve(items);
28 | })
29 | .catch(error => {
30 | console.log(error);
31 | reject([]);
32 | });
33 | })
34 | }
35 |
36 | export default fetchVideosResults;
--------------------------------------------------------------------------------
/utils/getInitialColorMode.js:
--------------------------------------------------------------------------------
1 | function getInitialColorMode() {
2 | const persistedColorPreference = window.localStorage.getItem('color-mode');
3 | const hasPersistedPreference = typeof persistedColorPreference === 'string';
4 |
5 | return persistedColorPreference || 'dark'
6 | }
--------------------------------------------------------------------------------