├── .github
├── PULL_REQUEST_TEMPLATE.md
├── copilot-instructions.md
└── workflows
│ └── autoFormatCode.yml
├── .gitignore
├── .idea
└── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LEARN.md
├── LICENSE
├── README.md
├── backend
├── .gitignore
├── .prettierrc
├── configs
│ ├── EmailConfig.js
│ └── dbConfig.js
├── controllers
│ ├── adminControllers
│ │ ├── adminForgotPassword.js
│ │ ├── adminLogin.js
│ │ ├── adminResetPassword.js
│ │ ├── adminSignup.js
│ │ ├── adminVerifyEmail.js
│ │ └── manageReviews.js
│ ├── authControllers
│ │ ├── changeEmail.js
│ │ ├── changeName.js
│ │ ├── forgotPassword.js
│ │ ├── getCurrentUser.js
│ │ ├── login.js
│ │ ├── resetPassword.js
│ │ ├── signup.js
│ │ └── verifyEmail.js
│ ├── domainControllers
│ │ └── addCustomDomain.js
│ ├── feedbackControllers
│ │ ├── feedback.js
│ │ └── reviews.js
│ └── urlControllers
│ │ ├── category.js
│ │ ├── deleteUrl.js
│ │ ├── exportGeneratedUrls.js
│ │ ├── generateCustomBackHalf.js
│ │ ├── generateShortUrl.js
│ │ ├── getHistory.js
│ │ └── redirectToURL.js
├── extras
│ ├── Constants.js
│ └── passport.js
├── index.js
├── jobs
│ └── customDomainJobs.js
├── middlewares
│ ├── ValidatorErrorHandler.js
│ ├── authMiddleware.js
│ └── isAdmin.js
├── models
│ ├── AdminModel.js
│ ├── CustomDomainsModel.js
│ ├── Feedback.js
│ ├── LinkInBioPageModel.js
│ ├── Tokenmodel.js
│ ├── UrlModel.js
│ └── UserModel.js
├── nodemon.json
├── package.json
├── routes
│ ├── admin.route.js
│ ├── adminAuth.route.js
│ ├── domain.route.js
│ ├── url.route.js
│ └── userAuth.route.js
├── swagger.yml
├── test
│ ├── mainTest.js
│ └── urlController.test.js
├── utils
│ ├── dns.js
│ ├── helperfunc.js
│ └── mailSend.js
├── validators
│ ├── authValidators.js
│ ├── feedbackValidator.js
│ └── urlValidator.js
└── views
│ ├── reset_password_email_template.ejs
│ └── welcome_email_template.ejs
├── chrome-extension
├── extension.html
├── history.html
├── history.js
├── icon.png
├── logo.png
├── manifest.json
└── script.js
├── designs
├── User-Profile-Page
│ ├── Desktop - User Page - Favorites.png
│ ├── Desktop - User Page - Links.png
│ ├── Desktop - User Page - Profile edit Settings.png
│ ├── Desktop - User Page - Profile edit Settings_2.png
│ ├── FigmaLink.txt
│ ├── Mobile - User Page - Favourites.png
│ ├── Mobile - User Page - Links.png
│ └── Mobile - User Page - Profile Edit.png
├── profile-finder-page
│ ├── desktop
│ │ ├── desktop 1 (no description).pdf
│ │ ├── desktop 1 (no description).png
│ │ ├── desktop 1.pdf
│ │ ├── desktop 1.png
│ │ ├── desktop 2 (no description).pdf
│ │ ├── desktop 2 (no description).png
│ │ ├── desktop 2.pdf
│ │ └── desktop 2.png
│ └── mobile
│ │ ├── mobile 1 (no description).pdf
│ │ ├── mobile 1 (no description).png
│ │ ├── mobile 1.pdf
│ │ ├── mobile 1.png
│ │ ├── mobile 2 (no description).pdf
│ │ ├── mobile 2 (no description).png
│ │ ├── mobile 2.pdf
│ │ └── mobile 2.png
└── public-profile-page
│ ├── desktop 1.pdf
│ ├── desktop
│ ├── desktop 1.pdf
│ └── desktop2.pdf
│ ├── desktop2.pdf
│ ├── mobile
│ ├── mobile1.pdf
│ ├── mobile1.png
│ ├── mobile2.pdf
│ └── mobile2.png
│ ├── mobile1.pdf
│ ├── mobile1.png
│ ├── mobile2.pdf
│ └── mobile2.png
├── frontend
├── .eslintrc.json
├── .gitignore
├── .prettierrc
├── README.md
├── index.html
├── package.json
├── public
│ ├── _redirects
│ ├── favicon.ico
│ ├── logo.png
│ └── manifest.json
├── src
│ ├── App.css
│ ├── App.jsx
│ ├── assets
│ │ ├── cont1.json
│ │ └── images
│ │ │ └── feedback.png
│ ├── components
│ │ ├── About.jsx
│ │ ├── AxiosFetch.jsx
│ │ ├── Contributers.css
│ │ ├── Contributers.jsx
│ │ ├── Contributors.css
│ │ ├── Contributors.jsx
│ │ ├── ExportToExcel.jsx
│ │ ├── FeedbackForm.css
│ │ ├── FeedbackForm.jsx
│ │ ├── Footer.css
│ │ ├── Footer.jsx
│ │ ├── ForgotPassword.jsx
│ │ ├── History.jsx
│ │ ├── HistoryCard.jsx
│ │ ├── Home.jsx
│ │ ├── LandingPage
│ │ │ ├── Hero.jsx
│ │ │ ├── HomePage.jsx
│ │ │ ├── Images
│ │ │ │ ├── ShortenDesktop.svg
│ │ │ │ ├── ShortenMobile.svg
│ │ │ │ ├── desktop.svg
│ │ │ │ ├── facebook.svg
│ │ │ │ ├── img.svg
│ │ │ │ ├── instagram.svg
│ │ │ │ ├── mobile.svg
│ │ │ │ ├── pinterest.svg
│ │ │ │ ├── twitter.svg
│ │ │ │ └── working_img.svg
│ │ │ ├── LandingPage.css
│ │ │ ├── LandingPage.jsx
│ │ │ ├── appTheme.js
│ │ │ ├── footer_segment
│ │ │ │ ├── BoostBox.jsx
│ │ │ │ ├── Footer.jsx
│ │ │ │ ├── FooterLink.jsx
│ │ │ │ ├── FooterLinkList.jsx
│ │ │ │ └── FooterSegment.jsx
│ │ │ └── form
│ │ │ │ ├── BackHalfForm.jsx
│ │ │ │ ├── LinkForm.jsx
│ │ │ │ ├── LinkList.jsx
│ │ │ │ ├── LinkParent.jsx
│ │ │ │ ├── Schema.jsx
│ │ │ │ └── SingleOutput.jsx
│ │ ├── Linkinbio.jsx
│ │ ├── Login.jsx
│ │ ├── Logout.jsx
│ │ ├── Navbar.jsx
│ │ ├── Pagination.jsx
│ │ ├── ProfileForm.jsx
│ │ ├── ProfilePage.jsx
│ │ ├── ResetPassword.jsx
│ │ ├── Sharepage.css
│ │ ├── Sharepage.jsx
│ │ ├── Signup.jsx
│ │ └── linkinbio.css
│ ├── context
│ │ └── UserContext.js
│ └── index.jsx
└── vite.config.js
├── netlify.toml
└── package.json
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## What does this PR do?
2 |
3 |
4 | Fixes # (issue)
5 |
6 | ## Requirement/Documentation
7 |
8 |
9 |
10 | - If there is a requirement document, please, share it here.
11 | - If there is ab UI/UX design document, please, share it here.
12 |
13 | ## Type of change
14 |
15 |
16 |
17 | - [ ] Bug fix (non-breaking change which fixes an issue)
18 | - [ ] Chore (refactoring code, workflow improvements)
19 | - [ ] New feature (non-breaking change which adds functionality)
20 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
21 | - [ ] This change requires a documentation update
22 |
23 | ## How should this be tested?
24 |
25 |
26 |
27 | - Are there environment variables that should be set?
28 | - What are the minimal test data to have?
29 | - Any other important info that could help to test that PR
30 |
31 | ## Checklist
32 |
33 |
34 |
35 | - I haven't read the [contributing guide](https://github.com/DhananjayThomble/URL-Shortener-App/blob/main/CONTRIBUTING.md)
36 | - My code doesn't follow the style guidelines of this project
37 | - I haven't commented my code, particularly in hard-to-understand areas
38 | - I haven't checked if my PR needs changes to the documentation
39 | - I haven't checked if my changes generate no new warnings
40 |
--------------------------------------------------------------------------------
/.github/workflows/autoFormatCode.yml:
--------------------------------------------------------------------------------
1 | name: Auto Format Code with Prettier
2 | # This action automatically formats code with Prettier when a push is made to the main branch.
3 |
4 | on:
5 | push:
6 | branches:
7 | - main
8 |
9 | jobs:
10 | format:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - name: Checkout code
14 | uses: actions/checkout@v2
15 |
16 | - name: Setup Node.js
17 | uses: actions/setup-node@v2
18 | with:
19 | node-version: "14.x"
20 |
21 | - name: Install dependencies
22 | run: npm install --prefix backend && npm install --prefix frontend
23 |
24 | - name: Run Prettier in /backend
25 | run: npm run format --prefix backend
26 |
27 | - name: Run Prettier in /frontend
28 | run: npm run format --prefix frontend
29 |
30 | - name: Check for changes
31 | id: check_changes
32 | run: |
33 | git diff --exit-code --quiet || echo "::set-output name=changes::true"
34 |
35 | - name: Configure Git
36 | run: |
37 | git config user.name "github-actions[bot]"
38 | git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
39 |
40 | - name: Commit files
41 | if: steps.check_changes.outputs.changes == 'true'
42 | run: |
43 | git add .
44 | git commit -m "Auto formatting with Prettier"
45 | git push
46 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .vscode/*
2 | .env
3 | node_modules/
4 | resources/
5 | extras/todo
6 | .idea
7 | # Local Netlify folder
8 | .netlify
9 | package-lock.json
10 | yarn.lock
11 | npm-debug.log
12 | yarn-error.log
13 | build/
14 | dist/
15 | .db
16 | .vscode/
17 | .idea/
18 | .DS_Store
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Editor-based HTTP Client requests
5 | /httpRequests/
6 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to URL-Shortener-App
2 |
3 | Thank you for your interest in contributing to URL-Shortener-App! We welcome contributions from the community to make this project better. Before you get started, please take a moment to review the following guidelines.
4 |
5 | ## Table of Contents
6 | - [Code of Conduct](#code-of-conduct)
7 | - [How to Contribute](#how-to-contribute)
8 | - [Reporting Bugs](#reporting-bugs)
9 | - [Adding Features](#adding-features)
10 | - [Code Style](#code-style)
11 | - [Documentation](#documentation)
12 | - [Pull Requests](#pull-requests)
13 | - [Getting Started](#getting-started)
14 | - [License](#license)
15 |
16 | ## Code of Conduct
17 |
18 | Please review our [Code of Conduct](CODE_OF_CONDUCT.md) before participating in our community.
19 |
20 | ## How to Contribute
21 |
22 | ### Reporting Bugs
23 |
24 | If you encounter a bug while using URL-Shortener-App, please [open an issue](../../issues) on our GitHub repository. Make sure to include:
25 |
26 | - A clear and descriptive title.
27 | - A detailed description of the issue, including steps to reproduce it.
28 | - Information about your environment, such as operating system and browser (if applicable).
29 |
30 | ### Adding Features
31 |
32 | If you have an idea for a new feature or improvement, feel free to suggest it by [opening an issue](../../issues). We appreciate well-documented feature proposals that include:
33 |
34 | - A clear and descriptive title.
35 | - A detailed explanation of the feature.
36 | - Any relevant design or implementation details.
37 |
38 | ### Code Style
39 |
40 | Please adhere to our [code style guidelines](CODE_STYLE.md) when contributing code to this project.
41 |
42 | ### Documentation
43 |
44 | Improvements to documentation are always welcome. If you find areas where the documentation can be enhanced, or if you want to contribute new documentation, please do so.
45 |
46 | ### Pull Requests
47 |
48 | When submitting pull requests, please follow these guidelines:
49 |
50 | 1. Fork the repository and create a feature branch from the `main` branch.
51 | 2. Ensure your code adheres to the code style guidelines.
52 | 3. Write clear, concise commit messages.
53 | 4. Test your changes thoroughly.
54 | 5. Ensure that your pull request addresses an open issue or clearly states its purpose.
55 | 6. Reference any related issues in your pull request description.
56 |
57 | ## Getting Started
58 |
59 | If you're new to contributing to open-source projects, you can start by checking out the list of open issues labeled as "good first issue." These issues are typically beginner-friendly.
60 |
61 | ## License
62 |
63 | By contributing to [URL-Shorter-App](https://app.snapurl.in/) , you agree that your contributions will be licensed under the project's [LICENSE](LICENSE).
64 |
65 | Thank you for your contributions! 🚀
66 |
--------------------------------------------------------------------------------
/LEARN.md:
--------------------------------------------------------------------------------
1 | # Building a URL Shortener with Node.js, React, and MongoDB
2 |
3 | This project demonstrates how to build a URL shortener using Node.js, React, and MongoDB. The application allows users to create shortened URLs that redirect to the original URL. It also tracks the number of visits to each shortened URL.
4 |
5 | ## Project Structure
6 |
7 | The project consists of a backend API and a frontend web application.
8 |
9 | ### Backend
10 |
11 | The backend is built using Node.js and Express.js. It provides the following functionalities:
12 |
13 | - User authentication and authorization using JSON Web Tokens (JWT)
14 | - URL shortening using a random string of characters
15 | - Visit count tracking for shortened URLs
16 | - CRUD operations for URLs
17 | - Exporting URL history to an Excel sheet
18 |
19 | The backend API is secured using CORS and rate limiting is implemented using Express Rate Limit.
20 |
21 | ### Frontend
22 |
23 | The frontend is built using React and Bootstrap. It provides the following functionalities:
24 |
25 | - User registration and login
26 | - URL shortening form
27 | - Displaying all shortened URLs generated by a user
28 | - Exporting URL history to an Excel sheet
29 | - Deleting shortened URLs
30 | - Navigation using react-router-dom
31 | - Integration with the backend API with Axios
32 |
33 | ## GitHub Actions:
34 |
35 | [//]: # (formating code whenever a push is made to the master branch)
36 |
37 | ```yml
38 | name: Auto Format Code with Prettier
39 | # This action automatically formats code with Prettier when a push is made to the main branch.
40 |
41 | on:
42 | push:
43 | branches: [main]
44 |
45 | jobs:
46 | format:
47 | runs-on: ubuntu-latest
48 | steps:
49 | - uses: actions/checkout@v2
50 | - uses: actions/setup-node@v1
51 | with:
52 | node-version: "14.x"
53 | - run: npm install
54 | - run: npm run format
55 |
56 | # if there are changes, commit and push them
57 | - name: Commit files
58 | run: |
59 | git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
60 | git config --local user.name "github-actions[bot]"
61 | git add .
62 | git commit -m "Auto formatting with Prettier"
63 | git push
64 |
65 | ```
66 |
67 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Dhananjay Thomble
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/backend/.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 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/backend/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "all",
3 | "tabWidth": 2,
4 | "semi": true,
5 | "singleQuote": true
6 | }
7 |
--------------------------------------------------------------------------------
/backend/configs/EmailConfig.js:
--------------------------------------------------------------------------------
1 | const dotenv = require('dotenv');
2 | dotenv.config();
3 |
4 | const nodemailer = require('nodemailer');
5 |
6 | let transporter = nodemailer.createTransport({
7 | host: process.env.EMAIL_HOST,
8 | port: process.env.EMAIL_PORT,
9 | secure: false,
10 | auth: {
11 | user: process.env.EMAIL_USER,
12 | pass: process.env.EMAIL_PASS,
13 | },
14 | });
15 |
16 | module.exports = transporter;
17 |
--------------------------------------------------------------------------------
/backend/configs/dbConfig.js:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose';
2 | import { configDotenv } from 'dotenv';
3 | configDotenv();
4 |
5 | mongoose.set('strictQuery', false);
6 | mongoose.set('debug', process.env.NODE_ENV === 'development'); // if dev mode, log all mongoose query
7 |
8 | async function mongoConnect() {
9 | try {
10 | await mongoose.connect(process.env.DB_URL);
11 | console.log('Connected to DB');
12 | } catch (err) {
13 | console.error(err);
14 | }
15 | }
16 |
17 | export default mongoConnect;
18 |
--------------------------------------------------------------------------------
/backend/controllers/adminControllers/adminForgotPassword.js:
--------------------------------------------------------------------------------
1 | import Admin from '../../models/AdminModel.js';
2 | import crypto from 'crypto';
3 | import TokenModel from '../../models/Tokenmodel.js';
4 | import { sendEmail } from '../../utils/mailSend.js';
5 | import dotenv from 'dotenv';
6 | dotenv.config();
7 |
8 | const generatePasswordResetToken = async (email) => {
9 | // get user from database
10 | const user = await Admin.findOne({ email });
11 |
12 | if (!user) {
13 | throw new Error('User not found');
14 | }
15 |
16 | // generate password reset token
17 | const token = crypto.randomBytes(32).toString('hex');
18 | const expiration = Date.now() + 3600000;
19 |
20 | const resetToken = new TokenModel({
21 | userId: user._id,
22 | token,
23 | expiration,
24 | });
25 |
26 | // save password reset token in database
27 | await resetToken.save();
28 |
29 | // send password reset token
30 | const resetLink = `${process.env.FRONTEND_URL}/reset-password?token=${token}`;
31 | const emailOptions = {
32 | from: 'SnapURL@dturl.live',
33 | subject: 'Password Reset for SnapURL',
34 | recipient: email,
35 | html: `Click here to reset your password.`,
36 | };
37 |
38 | await sendEmail(emailOptions);
39 | };
40 |
41 | // Forgot Password controller
42 | export const adminForgotPassword = async (req, res) => {
43 | try {
44 | const { email } = req.body;
45 | // check that email existis in database or not
46 | const result = await Admin.findOne({ email });
47 | if (!result) {
48 | return res.status(404).json({ message: 'Email not found' });
49 | }
50 |
51 | await generatePasswordResetToken(email);
52 | return res.status(200).json({ message: 'Password reset email sent' });
53 | } catch (error) {
54 | console.error(error);
55 | res.status(500).json({ error: 'Server error' });
56 | }
57 | };
58 |
--------------------------------------------------------------------------------
/backend/controllers/adminControllers/adminLogin.js:
--------------------------------------------------------------------------------
1 | import Admin from '../../models/AdminModel.js';
2 | import jwt from 'jsonwebtoken';
3 | import dotenv from 'dotenv';
4 | dotenv.config();
5 |
6 | export const adminLogin = async (req, res) => {
7 | try {
8 | // get email and password from request body
9 | const { email, password } = req.body;
10 |
11 | const user = await Admin.findOne({ email });
12 |
13 | // if user not found, 401 status for unauthorized
14 | if (!user) return res.status(401).json({ error: 'Email does not exist' });
15 |
16 | // check password using passport
17 | user.comparePassword(password, (err, isMatch) => {
18 | if (!isMatch)
19 | return res.status(401).json({ error: 'Invalid credentials' });
20 |
21 | // password matched, create a token
22 | const token = jwt.sign({ _id: user._id }, process.env.JWT_SECRET, {
23 | expiresIn: '7d',
24 | });
25 |
26 | // send token in response
27 | const { _id, name, email } = user;
28 | return res.status(200).json({ token, user: { _id, name, email } });
29 | });
30 | } catch (error) {
31 | console.error(error);
32 | res.status(500).json({ error: 'Server error' });
33 | }
34 | };
35 |
--------------------------------------------------------------------------------
/backend/controllers/adminControllers/adminResetPassword.js:
--------------------------------------------------------------------------------
1 | import Admin from '../../models/AdminModel.js';
2 | import TokenModel from '../../models/Tokenmodel.js';
3 | import dotenv from 'dotenv';
4 | dotenv.config();
5 |
6 | export const adminResetPassword = async (req, res) => {
7 | try {
8 | const { token, password } = req.body;
9 |
10 | // get reset password token from database
11 | const resetToken = await TokenModel.findOne({ token });
12 |
13 | // verify expiration date of reset password token
14 | if (!resetToken || resetToken.expiration < Date.now()) {
15 | return res.status(400).json({ error: 'Invalid or expired token' });
16 | }
17 |
18 | // update user Password if token is valid
19 | const user = await Admin.findById(resetToken.userId);
20 | user.password = password;
21 | await user.save();
22 |
23 | // remove password reset token from database
24 | await resetToken.remove();
25 |
26 | return res.status(200).json({ message: 'Password reset successful' });
27 | } catch (error) {
28 | console.error(error);
29 | res.status(500).json({ error: 'Server error' });
30 | }
31 | };
32 |
--------------------------------------------------------------------------------
/backend/controllers/adminControllers/adminSignup.js:
--------------------------------------------------------------------------------
1 | import Admin from '../../models/AdminModel.js';
2 | import { sendWelcomeEmail } from '../../utils/mailSend.js';
3 | import dotenv from 'dotenv';
4 | dotenv.config();
5 |
6 | export const adminSignup = async (req, res) => {
7 | try {
8 | // get name and pasword fields from request body
9 | const { name, password } = req.body;
10 |
11 | // Validate that name is at least three characters long
12 | if (!name || name.length < 3)
13 | return res.status(400).json({
14 | error: 'Name is required and should be at least 3 characters long',
15 | });
16 |
17 | // Validate that password is at least six characters long
18 | if (!password || password.length < 6)
19 | return res.status(400).json({
20 | error: 'Password is required and should be at least 6 characters long',
21 | });
22 |
23 | // register the user in the database
24 | const user = new Admin(req.body);
25 |
26 | await user.save();
27 | console.log('User saved');
28 |
29 | // Sending the welcome email
30 | await sendWelcomeEmail(user.name, user.email);
31 |
32 | return res.status(200).json({ ok: true });
33 | } catch (err) {
34 | console.log('CREATE USER FAILED', err);
35 | return res
36 | .status(500)
37 | .json({ error: 'Error saving user in database. Try later' });
38 | }
39 | };
40 |
--------------------------------------------------------------------------------
/backend/controllers/adminControllers/adminVerifyEmail.js:
--------------------------------------------------------------------------------
1 | import Admin from '../../models/AdminModel.js';
2 | import TokenModel from '../../models/Tokenmodel.js';
3 | import dotenv from 'dotenv';
4 | dotenv.config();
5 |
6 | export const adminVerifyEmail = async (req, res) => {
7 | try {
8 | // token validation
9 | query('token').notEmpty().trim().escape().withMessage('Token is required');
10 | // get token from request
11 | const { token } = req.query;
12 |
13 | const verificationToken = await TokenModel.findOne({ token });
14 |
15 | // verify token
16 | if (!verificationToken) {
17 | return res.status(400).json({ error: 'Invalid or expired token' });
18 | }
19 |
20 | // get user by token id
21 | const user = await Admin.findById(verificationToken.userId);
22 |
23 | if (!user) {
24 | return res.status(400).json({ error: 'User not found' });
25 | }
26 |
27 | if (user.isEmailVerified) {
28 | return res.status(400).json({ error: 'Email already verified' });
29 | }
30 |
31 | user.isEmailVerified = true;
32 |
33 | //save user
34 | await user.save();
35 |
36 | await verificationToken.remove();
37 |
38 | console.log('Email verification done');
39 |
40 | return res.status(200).json({ message: 'Email verification successful' });
41 | } catch (error) {
42 | console.error('Error verifying email:', error);
43 | res.status(500).json({ error: 'Server error' });
44 | }
45 | };
46 |
--------------------------------------------------------------------------------
/backend/controllers/adminControllers/manageReviews.js:
--------------------------------------------------------------------------------
1 | import Feedback from '../../models/Feedback.js';
2 |
3 | export const deleteReview = async (req, res) => {
4 | const reviewId = req.params;
5 |
6 | try {
7 | const deletedReview = await Feedback.deleteOne(reviewId);
8 |
9 | if (!deletedReview) {
10 | return res.status(404).json({ error: 'Review not found' });
11 | }
12 |
13 | return res.status(200).json({ message: 'Review deleted successfully' });
14 | } catch (error) {
15 | console.error(error);
16 | return res.status(500).json({ error: 'Server error' });
17 | }
18 | };
19 |
20 | export const deleteAllFeedback = async (req, res) => {
21 | try {
22 | const result = await Feedback.deleteMany({});
23 | if (result) {
24 | res.status(200).json({ message: 'All feedback deleted successfully' });
25 | }
26 | console.log(`Deleted ${result.deletedCount} documents`);
27 | } catch (error) {
28 | console.error(error);
29 | }
30 | };
31 |
--------------------------------------------------------------------------------
/backend/controllers/authControllers/changeEmail.js:
--------------------------------------------------------------------------------
1 | import jwt from 'jsonwebtoken';
2 | import User from '../../models/UserModel.js';
3 | import dotenv from 'dotenv';
4 | import { isValidEmail } from '../../utils/helperfunc.js';
5 | dotenv.config();
6 |
7 | export const changeEmail = async (req, res) => {
8 | try {
9 | const token = req.headers.authorization?.split(' ')[1];
10 |
11 | jwt.verify(token, process.env.JWT_SECRET, async (err, decoded) => {
12 | if (err) {
13 | return res.status(401).json({ message: 'Unauthorized' });
14 | }
15 |
16 | const userId = decoded._id;
17 |
18 | try {
19 | // Retrieve the user from the database using the user ID
20 | const user = await User.findById(userId);
21 |
22 | if (!user) {
23 | return res.status(401).json({ message: 'Unauthorized' });
24 | }
25 |
26 | // Update the user's email
27 | user.email = newEmail;
28 |
29 | // Save the updated user
30 | await user.save();
31 |
32 | return res.status(200).json({ message: 'Email updated successfully' });
33 | } catch (err) {
34 | console.log(err);
35 | return res.status(500).json({ error: 'Failed to update email' });
36 | }
37 | });
38 | } catch (err) {
39 | console.log(err);
40 | res.status(500).json({ error: 'Server error' });
41 | }
42 | };
43 |
--------------------------------------------------------------------------------
/backend/controllers/authControllers/changeName.js:
--------------------------------------------------------------------------------
1 | import jwt from 'jsonwebtoken';
2 | import User from '../../models/UserModel.js';
3 | import dotenv from 'dotenv';
4 | dotenv.config();
5 |
6 | export const changeName = async (req, res) => {
7 | try {
8 | const token = req.headers.authorization?.split(' ')[1];
9 | jwt.verify(token, process.env.JWT_SECRET, async (err, decoded) => {
10 | if (err) {
11 | return res.status(401).json({ message: 'Unauthorized' });
12 | }
13 |
14 | const userId = decoded._id;
15 |
16 | try {
17 | // Retrieve the user from the database using the user ID
18 | const user = await User.findById(userId);
19 |
20 | if (!user) {
21 | return res.status(401).json({ message: 'Unauthorized' });
22 | }
23 |
24 | const newName = req.body.name;
25 |
26 | // Update the user's name
27 | user.name = newName;
28 |
29 | // Save the updated user
30 | await user.save();
31 |
32 | return res.status(200).json({ message: 'Name updated successfully' });
33 | } catch (err) {
34 | console.log(err);
35 | return res.status(500).json({ error: 'Failed to update Name' });
36 | }
37 | });
38 | } catch (err) {
39 | console.log(err);
40 | res.status(500).json({ error: 'Server error' });
41 | }
42 | };
43 |
--------------------------------------------------------------------------------
/backend/controllers/authControllers/forgotPassword.js:
--------------------------------------------------------------------------------
1 | import User from '../../models/UserModel.js';
2 | import crypto from 'crypto';
3 | import TokenModel from '../../models/Tokenmodel.js';
4 | import { sendPasswordResetEmail } from '../../utils/mailSend.js';
5 | import dotenv from 'dotenv';
6 | dotenv.config();
7 |
8 | const generatePasswordResetToken = async (userId) => {
9 | // generate password reset token
10 | const token = crypto.randomBytes(32).toString('hex');
11 | const expiration = Date.now() + 3600000; // 1 hour from now
12 |
13 | const resetToken = new TokenModel({
14 | userId,
15 | token,
16 | expiration,
17 | });
18 |
19 | // save password reset token in database
20 | await resetToken.save();
21 |
22 | return token;
23 | };
24 |
25 | // Forgot Password controller
26 | export const forgotPassword = async (req, res) => {
27 | try {
28 | const { email } = req.body;
29 | // check that email existis in database or not
30 | const user = await User.findOne({ email });
31 | if (!user) {
32 | return res.status(404).json({ message: 'Email not found' });
33 | }
34 |
35 | const token = await generatePasswordResetToken(user._id);
36 |
37 | // send password reset token
38 | const resetLink = `${process.env.FRONTEND_URL}/reset-password?token=${token}`;
39 |
40 | await sendPasswordResetEmail(email, resetLink);
41 |
42 | return res.status(200).json({ message: 'Password reset email sent' });
43 | } catch (error) {
44 | console.error(error);
45 | res.status(500).json({ error: 'Server error' });
46 | }
47 | };
48 |
--------------------------------------------------------------------------------
/backend/controllers/authControllers/getCurrentUser.js:
--------------------------------------------------------------------------------
1 | import jwt from 'jsonwebtoken';
2 | import User from '../../models/UserModel.js';
3 | import dotenv from 'dotenv';
4 | dotenv.config();
5 |
6 | export const getCurrentUser = (req, res) => {
7 | // Check if a token is present in the request headers
8 | // split the token from the "Bearer" string
9 | const token = req.headers.authorization?.split(' ')[1];
10 |
11 | if (!token) {
12 | return res.status(401).json({ message: 'Unauthorized' });
13 | }
14 |
15 | // Verify and decode the token
16 | jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => {
17 | if (err) {
18 | return res.status(401).json({ message: 'Unauthorized' });
19 | }
20 |
21 | // The decoded object contains the user's information
22 | const userId = decoded._id;
23 |
24 | // Retrieve the user from the database using the user ID
25 | User.findById(userId, (err, user) => {
26 | if (err || !user) {
27 | return res.status(401).json({ message: 'Unauthorized' });
28 | }
29 |
30 | // Send the user's information in the response
31 | return res.status(200).json({ user });
32 | });
33 | });
34 | };
35 |
--------------------------------------------------------------------------------
/backend/controllers/authControllers/login.js:
--------------------------------------------------------------------------------
1 | import User from '../../models/UserModel.js';
2 | import jwt from 'jsonwebtoken';
3 | import dotenv from 'dotenv';
4 | dotenv.config();
5 |
6 | export const login = async (req, res) => {
7 | try {
8 | // get email and password from request body
9 | const { email, password } = req.body;
10 |
11 | const user = await User.findOne({ email });
12 |
13 | // if user not found, 401 status for unauthorized
14 | if (!user) return res.status(401).json({ error: 'Email does not exist' });
15 |
16 | // check password using passport
17 | user.comparePassword(password, (err, isMatch) => {
18 | if (!isMatch)
19 | return res.status(401).json({ error: 'Invalid credentials' });
20 |
21 | // password matched, create a token
22 | const token = jwt.sign({ _id: user._id }, process.env.JWT_SECRET, {
23 | expiresIn: '7d',
24 | });
25 |
26 | // send token in response
27 | const { _id, name, email } = user;
28 | return res.status(200).json({ token, user: { _id, name, email } });
29 | });
30 | } catch (error) {
31 | console.error(error);
32 | res.status(500).json({ error: 'Server error' });
33 | }
34 | };
35 |
--------------------------------------------------------------------------------
/backend/controllers/authControllers/resetPassword.js:
--------------------------------------------------------------------------------
1 | import User from '../../models/UserModel.js';
2 | import TokenModel from '../../models/Tokenmodel.js';
3 | import dotenv from 'dotenv';
4 | dotenv.config();
5 |
6 | export const resetPassword = async (req, res) => {
7 | try {
8 | const { token, password } = req.body;
9 |
10 | // get reset password token from database
11 | const resetToken = await TokenModel.findOne({ token });
12 |
13 | // verify expiration date of reset password token
14 | if (!resetToken || resetToken.expiration < Date.now()) {
15 | return res.status(400).json({ error: 'Invalid or expired token' });
16 | }
17 |
18 | // update user Password if token is valid
19 | const user = await User.findById(resetToken.userId);
20 | user.password = password;
21 | await user.save();
22 |
23 | // remove password reset token from database
24 | await resetToken.remove();
25 |
26 | return res.status(200).json({ message: 'Password reset successful' });
27 | } catch (error) {
28 | console.error(error);
29 | res.status(500).json({ error: 'Server error' });
30 | }
31 | };
32 |
--------------------------------------------------------------------------------
/backend/controllers/authControllers/signup.js:
--------------------------------------------------------------------------------
1 | import User from '../../models/UserModel.js';
2 | import { sendWelcomeEmail } from '../../utils/mailSend.js';
3 | import dotenv from 'dotenv';
4 |
5 | dotenv.config();
6 |
7 | export const signup = async (req, res) => {
8 | try {
9 | // all the fields are validated by the middleware in the route
10 | const { name, email, password } = req.body;
11 | // register the user in the database
12 | const user = new User({ name, email, password });
13 |
14 | await user.save();
15 |
16 | // send welcome email to the user
17 | await sendWelcomeEmail(name, email, user._id);
18 |
19 | res.status(200).json({ ok: true });
20 | } catch (err) {
21 | console.error(err);
22 | res.status(500).json({ error: 'Error saving user in database. Try later' });
23 | }
24 | };
25 |
--------------------------------------------------------------------------------
/backend/controllers/authControllers/verifyEmail.js:
--------------------------------------------------------------------------------
1 | import User from '../../models/UserModel.js';
2 | import TokenModel from '../../models/Tokenmodel.js';
3 | import dotenv from 'dotenv';
4 | dotenv.config();
5 |
6 | export const verifyEmail = async (req, res) => {
7 | try {
8 | // get token from request
9 | const { token } = req.query;
10 |
11 | // token is already validated by the middleware
12 |
13 | const verificationToken = await TokenModel.findOne({ token });
14 |
15 | // verify token
16 | if (!verificationToken) {
17 | return res.status(400).json({ error: 'Invalid or expired token' });
18 | }
19 |
20 | // get user by token id
21 | const user = await User.findById(verificationToken.userId);
22 |
23 | if (!user) {
24 | return res.status(400).json({ error: 'User not found' });
25 | }
26 |
27 | if (user.isEmailVerified) {
28 | return res.status(400).json({ error: 'Email already verified' });
29 | }
30 |
31 | user.isEmailVerified = true;
32 |
33 | //save user
34 | await user.save();
35 |
36 | await verificationToken.remove();
37 |
38 | console.log('Email verification done');
39 |
40 | return res.redirect('https://app.snapurl.in/login');
41 | } catch (error) {
42 | console.error('Error verifying email:', error);
43 | res.status(500).json({ error: 'Server error' });
44 | }
45 | };
46 |
--------------------------------------------------------------------------------
/backend/controllers/domainControllers/addCustomDomain.js:
--------------------------------------------------------------------------------
1 | import { randomBytes } from 'crypto';
2 | import CustomDomain from '../../models/CustomDomainsModel.js';
3 | import User from '../../models/UserModel.js';
4 |
5 | export const addCustomDomain = async (req, res) => {
6 | try {
7 | const user = await User.findById(req.user._id);
8 |
9 | if (!user) {
10 | res.send('User does not exist');
11 | return;
12 | }
13 |
14 | const { domain } = req.body;
15 |
16 | if (!domain) {
17 | res.send('Please provide a domain');
18 | return;
19 | }
20 |
21 | const dnsVerificationCode = randomBytes(32).toString('hex');
22 |
23 | const customDomain = await CustomDomain.create({
24 | url: domain,
25 | dnsVerificationCode,
26 | user: req.user,
27 | });
28 |
29 | // TODO: send email for verification process for the DNS
30 | user.customDomain = customDomain;
31 | await user.save();
32 |
33 | res.status(200).json({
34 | message:
35 | 'Successfully added domain! Please refer to your email to verify it.',
36 | });
37 | } catch (error) {
38 | console.error(error);
39 | res.status(500).json({ error: 'Server error' });
40 | }
41 | };
42 |
--------------------------------------------------------------------------------
/backend/controllers/feedbackControllers/feedback.js:
--------------------------------------------------------------------------------
1 | // Route for submitting feedback
2 | import { validationResult } from 'express-validator';
3 | import Feedback from '../../models/Feedback.js';
4 | import User from '../../models/UserModel.js';
5 | export const submitFeedback = async (req, res) => {
6 | try {
7 | // Implement validation using express-validator
8 | const errors = validationResult(req);
9 | if (!errors.isEmpty()) {
10 | return res.status(400).json({ errors: errors.array() });
11 | }
12 | //search for userid which is refering to user model to extract email and name from user model
13 | const user = await User.findById(req.user._id);
14 |
15 | if (!user) {
16 | return res.status(404).json({ error: 'User not found' });
17 | }
18 | // Destructure name and email from user
19 | const { name, email } = user;
20 | // Destructure data from the request body
21 |
22 | const { message, rating } = req.body;
23 |
24 | // Create a new feedback instance with email, username, message, and rating
25 | const feedback = new Feedback({ name, email, message, rating });
26 | await feedback.save();
27 |
28 | return res.status(200).send('Feedback submitted successfully.');
29 | } catch (error) {
30 | console.error(error);
31 | return res.status(500).send('Feedback submission failed.');
32 | }
33 | };
34 |
--------------------------------------------------------------------------------
/backend/controllers/feedbackControllers/reviews.js:
--------------------------------------------------------------------------------
1 | import Feedback from '../../models/Feedback.js';
2 | //get all reviews
3 | export const getReviews = async (req, res) => {
4 | try {
5 | const reviews = await Feedback.find({});
6 | console.log(reviews);
7 | res.status(200).json(reviews);
8 | } catch (error) {
9 | console.error(error);
10 | res.status(500).json({ error: 'Server error' });
11 | }
12 | };
13 |
--------------------------------------------------------------------------------
/backend/controllers/urlControllers/category.js:
--------------------------------------------------------------------------------
1 | import UrlModel from '../../models/UrlModel.js';
2 |
3 | export const getFilteredCategory = async (req, res) => {
4 | const userId = req.user._id;
5 | const category = req.params.category;
6 | try {
7 | const urls = await UrlModel.find({
8 | userId,
9 | category,
10 | });
11 | // console.log(urls);
12 | res.status(200).json(urls);
13 | } catch (error) {
14 | console.error(error);
15 | res.status(500).json({ error: 'Server error' });
16 | }
17 | };
18 |
19 | export const updateCategory = async (req, res) => {
20 | const userId = req.user._id;
21 | const category = req.body.category;
22 | const shortUrl = req.body.shortUrl;
23 | try {
24 | const updatedUrl = await UrlModel.findOneAndUpdate(
25 | { userId, shortUrl }, // query
26 | { category }, // update
27 | );
28 |
29 | if (!updatedUrl) {
30 | return res.status(404).json({ error: 'URL not found' });
31 | }
32 |
33 | res.status(200).json({ message: 'Category updated' });
34 | } catch (error) {
35 | console.error(error);
36 | res.status(500).json({ error: 'Server error' });
37 | }
38 | };
39 |
--------------------------------------------------------------------------------
/backend/controllers/urlControllers/deleteUrl.js:
--------------------------------------------------------------------------------
1 | import UrlModel from '../../models/UrlModel.js';
2 |
3 | // Delete a URL associated with a user
4 | export const deleteUrl = async (req, res) => {
5 | const id = req.params.id;
6 | const userId = req.user._id;
7 | try {
8 | // Delete the URL from the database
9 | const deletedUrl = await UrlModel.findOneAndDelete({
10 | userId,
11 | shortUrl: id,
12 | });
13 |
14 | // if the URL does not exist
15 | if (!deletedUrl) {
16 | return res.status(400).json({
17 | error: 'URL does not exist',
18 | });
19 | }
20 |
21 | // send response with the success message
22 | res.status(200).json({
23 | message: 'URL deleted',
24 | });
25 | } catch (error) {
26 | console.error(error);
27 | res.status(500).json({ error: 'Server error' });
28 | }
29 | };
30 |
--------------------------------------------------------------------------------
/backend/controllers/urlControllers/exportGeneratedUrls.js:
--------------------------------------------------------------------------------
1 | import ExcelJS from 'exceljs';
2 | import UrlModel from '../../models/UrlModel.js';
3 |
4 | export const exportGeneratedUrls = async (req, res) => {
5 | try {
6 | const urls = await UrlModel.find({ userId: req.user._id });
7 |
8 | if (!urls.length) {
9 | return res.status(404).json({ error: 'No Generated URL found' });
10 | }
11 |
12 | const workbook = new ExcelJS.Workbook();
13 | workbook.creator = 'SnapURL';
14 | workbook.created = new Date();
15 |
16 | const worksheet = workbook.addWorksheet('Generated_URLs Sheet');
17 |
18 | worksheet.columns = [
19 | { header: 'Short URL', key: 'shortUrl', width: 50 },
20 | { header: 'Original URL', key: 'originalUrl', width: 50 },
21 | { header: 'Category', key: 'category', width: 20 },
22 | { header: 'Custom Back Half', key: 'customBackHalf', width: 20 },
23 | { header: 'Visits', key: 'visits', width: 10 },
24 | ];
25 |
26 | worksheet.getRow(1).font = { bold: true };
27 |
28 | urls.forEach((url) => {
29 | worksheet.addRow({
30 | shortUrl: `${process.env.SHORT_URL_PREFIX}/${url.shortUrl}`,
31 | originalUrl: url.originalUrl,
32 | category: url.category,
33 | customBackHalf: url.customBackHalf,
34 | visits: url.visitCount,
35 | });
36 | });
37 |
38 | const buffer = await workbook.xlsx.writeBuffer();
39 |
40 | res.setHeader(
41 | 'Content-Type',
42 | 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
43 | );
44 | res.setHeader(
45 | 'Content-Disposition',
46 | 'attachment; filename=Generated_URLs.xlsx',
47 | );
48 | res.status(200).send(buffer);
49 | } catch (error) {
50 | console.error(error);
51 | res.status(500).json({ error: 'Server error' });
52 | }
53 | };
54 |
--------------------------------------------------------------------------------
/backend/controllers/urlControllers/generateCustomBackHalf.js:
--------------------------------------------------------------------------------
1 | import UrlModel from '../../models/UrlModel.js';
2 | import { generate } from 'random-words';
3 |
4 | // Generate a custom back-half of a short URL for a given short URL
5 | export const generateCustomBackHalf = async (req, res) => {
6 | try {
7 | const { backHalf, shortUrl } = req.body;
8 | const userId = req.user._id;
9 |
10 | // check if the custom back-half already exists
11 | const existingUrl = await UrlModel.findOne({ customBackHalf: backHalf });
12 |
13 | if (existingUrl) {
14 | // generate a random back-half, if custom one already exists
15 | const randomBackHalf = generate({ minLength: 3, maxLength: 10 });
16 |
17 | return res.status(400).json({
18 | error: 'Custom back-half already exists',
19 | suggestion: randomBackHalf,
20 | });
21 | }
22 |
23 | // update the URL with the custom back-half
24 | const updatedUrl = await UrlModel.findOneAndUpdate(
25 | { userId, shortUrl },
26 | { customBackHalf: backHalf },
27 | { new: true },
28 | );
29 | // console.log('Updated url data:', updatedUrl);
30 |
31 | // If the URL does not exist, return an error
32 | if (!updatedUrl) {
33 | return res.status(400).json({
34 | error: 'URL does not exist',
35 | });
36 | }
37 |
38 | // Send response with the success message
39 | res.status(200).json({
40 | message: 'Custom back-half generated',
41 | customBackHalf: updatedUrl.customBackHalf,
42 | });
43 | } catch (error) {
44 | console.error(error);
45 | res.status(500).json({ error: 'Server Error' });
46 | }
47 | };
48 |
--------------------------------------------------------------------------------
/backend/controllers/urlControllers/generateShortUrl.js:
--------------------------------------------------------------------------------
1 | import { nanoid } from 'nanoid';
2 | import UrlModel from '../../models/UrlModel.js';
3 |
4 | // Generate a short URL for a given long URL
5 | export const generateShortUrl = async (req, res) => {
6 | const SHORT_URL_PREFIX = process.env.SHORT_URL_PREFIX;
7 | try {
8 | const { url } = req.body;
9 |
10 | // Get the user ID from the request
11 | const userId = req.user._id;
12 |
13 | // Get the URL details from the database
14 | const urlDetails = await UrlModel.findOne(
15 | { originalUrl: url, userId },
16 | { shortUrl: 1, customBackHalf: 1, _id: 0 },
17 | );
18 | // console.log(`urlDetails: ${urlDetails}`);
19 |
20 | if (urlDetails) {
21 | // if the URL has already been shortened for the user, return the existing short URL
22 | const { shortUrl, customBackHalf } = urlDetails;
23 | return res.status(200).json({
24 | shortUrl: `${SHORT_URL_PREFIX}/${shortUrl}`,
25 | customBackHalf: customBackHalf,
26 | });
27 | }
28 |
29 | // If the URL has not been shortened for the user, generate a new short URL
30 | const id = nanoid(10);
31 |
32 | await UrlModel.create({
33 | userId,
34 | shortUrl: id,
35 | originalUrl: url,
36 | });
37 |
38 | // Send response with the generated short URL
39 | res.status(200).json({
40 | shortUrl: `${SHORT_URL_PREFIX}/${id}`,
41 | customBackHalf: null,
42 | });
43 | } catch (error) {
44 | console.error(error);
45 | res.status(500).json({ error: 'Server error' });
46 | }
47 | };
48 |
--------------------------------------------------------------------------------
/backend/controllers/urlControllers/getHistory.js:
--------------------------------------------------------------------------------
1 | import UrlModel from '../../models/UrlModel.js';
2 |
3 | // Get the URL history for a user
4 | export const getHistory = async (req, res) => {
5 | try {
6 | // const urlObj = await UrlModel.findOne({ userId: req.user._id });
7 | const urlArray = await UrlModel.find({ userId: req.user._id });
8 |
9 | if (!urlArray.length) {
10 | return res.status(404).json({ error: 'No Generated URL found' });
11 | }
12 |
13 | res.status(200).json(urlArray);
14 | } catch (error) {
15 | console.error(error);
16 | res.status(500).json({ error: 'Server error' });
17 | }
18 | };
19 |
--------------------------------------------------------------------------------
/backend/controllers/urlControllers/redirectToURL.js:
--------------------------------------------------------------------------------
1 | import UrlModel from '../../models/UrlModel.js';
2 |
3 | // Redirect to the original URL associated with a short URL
4 | export const redirectToOriginalUrl = async (req, res) => {
5 | try {
6 | const { short } = req.params;
7 |
8 | const url = await UrlModel.findOne({ shortUrl: short });
9 |
10 | // if the URL does not exist
11 | if (!url) {
12 | return res.status(404).json({ error: 'Url not found' });
13 | }
14 |
15 | // increment the URL visit count
16 | const visitCount = url.visitCount || 0;
17 | await url.updateOne({ visitCount: visitCount + 1 });
18 |
19 | // Perform a 302 redirect to the original URL
20 | res.status(302).redirect(url.originalUrl);
21 | } catch (error) {
22 | console.error(error);
23 | res.status(500).json({ error: 'Server error' });
24 | }
25 | };
26 |
27 | export const redirectViaCustomBackHalf = async (req, res) => {
28 | try {
29 | const { backHalf } = req.params;
30 |
31 | const url = await UrlModel.findOne({ customBackHalf: backHalf });
32 |
33 | // if the URL does not exist
34 | if (!url) {
35 | return res.status(404).json({ error: 'Url not found' });
36 | }
37 |
38 | // increment the URL visit count
39 | const visitCount = url.visitCount || 0;
40 | await url.updateOne({ visitCount: visitCount + 1 });
41 |
42 | // Perform a 302 redirect to the original URL
43 | res.status(302).redirect(url.originalUrl);
44 | } catch (error) {
45 | console.error(error);
46 | res.status(500).json({ error: 'Server error' });
47 | }
48 | };
49 |
--------------------------------------------------------------------------------
/backend/extras/Constants.js:
--------------------------------------------------------------------------------
1 | import { config } from 'dotenv';
2 |
3 | config();
4 |
5 | export const SHORT_URL_PREFIX = process.env.SHORT_URL_PREFIX;
6 |
--------------------------------------------------------------------------------
/backend/extras/passport.js:
--------------------------------------------------------------------------------
1 | import passport from 'passport';
2 | import LocalStrategy from 'passport-local';
3 | import User from '../models/UserModel.js';
4 |
5 | // Configure the local strategy for passport
6 | passport.use(
7 | new LocalStrategy(
8 | { usernameField: 'email' }, // use the email field as the username
9 | (email, password, done) => {
10 | // Find the user with the given email
11 | User.findOne({ email: email }, (err, user) => {
12 | if (err) {
13 | return done(err);
14 | }
15 | if (!user) {
16 | return done(null, false, { message: 'Incorrect email.' });
17 | }
18 | // Check if the password is correct
19 | user.comparePassword(password, (err, isMatch) => {
20 | if (err) {
21 | return done(err);
22 | }
23 | if (!isMatch) {
24 | return done(null, false, { message: 'Incorrect password.' });
25 | }
26 | return done(null, user);
27 | });
28 | });
29 | },
30 | ),
31 | );
32 |
33 | passport.serializeUser(function (user, done) {
34 | done(null, user.id);
35 | });
36 |
37 | passport.deserializeUser(function (id, done) {
38 | User.findById(id, function (err, user) {
39 | done(err, user);
40 | });
41 | });
42 |
43 | export default passport;
44 |
--------------------------------------------------------------------------------
/backend/jobs/customDomainJobs.js:
--------------------------------------------------------------------------------
1 | import cron from 'node-cron';
2 | import CustomDomain from '../models/CustomDomainsModel.js';
3 | import { checkDNSTxtVerification } from '../utils/dns.js';
4 |
5 | const fetchAndVerifyDomains = () => {
6 | (async () => {
7 | try {
8 | const domains = await CustomDomain.find({ isVerified: { $eq: false } });
9 |
10 | for (let i = 0; i < domains.length; i++) {
11 | const domain = domains[i];
12 |
13 | const domainVerification = await checkDNSTxtVerification(
14 | domain.url,
15 | domain.dnsVerificationCode,
16 | );
17 |
18 | if (domainVerification.status === true) {
19 | // send mail to inform domain verification is successful
20 | domain.isVerified = true;
21 | domain.save();
22 | }
23 | }
24 | } catch (error) {
25 | if (error.message) {
26 | console.error(error);
27 | }
28 | }
29 | })();
30 | };
31 |
32 | const initCustomDomainJobs = () => {
33 | cron.schedule('0 * * * *', fetchAndVerifyDomains);
34 | };
35 |
36 | export default initCustomDomainJobs;
37 |
--------------------------------------------------------------------------------
/backend/middlewares/ValidatorErrorHandler.js:
--------------------------------------------------------------------------------
1 | import { validationResult } from 'express-validator';
2 |
3 | export const validationErrorHandler = (req, res, next) => {
4 | const errors = validationResult(req);
5 | if (!errors.isEmpty()) {
6 | return res.status(400).json({ errors: errors.array() });
7 | }
8 | next();
9 | };
10 |
--------------------------------------------------------------------------------
/backend/middlewares/authMiddleware.js:
--------------------------------------------------------------------------------
1 | import jwt from 'jsonwebtoken';
2 |
3 | export const isApiAuthenticated = async (req, res, next) => {
4 | // check if token is present in headers
5 | if (!req.headers.authorization) {
6 | res.status(401).json({ error: 'Unauthorized' });
7 | return;
8 | }
9 | // verify the token
10 | const token = req.headers.authorization.split(' ')[1];
11 | jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => {
12 | if (err) {
13 | res.status(401).json({ error: 'Invalid token' });
14 | } else {
15 | req.user = decoded;
16 | next();
17 | }
18 | });
19 | };
20 |
--------------------------------------------------------------------------------
/backend/middlewares/isAdmin.js:
--------------------------------------------------------------------------------
1 | import Admin from '../models/AdminModel.js';
2 | export const isAdmin = async (req, res, next) => {
3 | try {
4 | const user = await Admin.findById(req.user._id);
5 |
6 | if (!user) {
7 | return res
8 | .status(401)
9 | .json({ error: 'adminControllers resource. Access denied' });
10 | }
11 |
12 | // If user is an adminControllers, set it in req.profile
13 | req.profile = user;
14 | next();
15 | } catch (error) {
16 | console.error(error);
17 | return res.status(500).json({ error: 'Server error' });
18 | }
19 | };
20 |
--------------------------------------------------------------------------------
/backend/models/AdminModel.js:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose';
2 | import bcrypt from 'bcrypt';
3 |
4 | const saltRounds = 10; // Affects the performance and password security level
5 |
6 | const AdminSchema = new mongoose.Schema(
7 | {
8 | email: {
9 | type: String,
10 | required: true,
11 | unique: true,
12 | },
13 |
14 | password: {
15 | type: String,
16 | required: true,
17 | },
18 |
19 | name: {
20 | type: String,
21 | required: true,
22 | },
23 |
24 | isEmailVerified: {
25 | type: Boolean,
26 | default: false, // Initially, the email is not verified
27 | },
28 | },
29 | { timestamps: true },
30 | );
31 |
32 | // Hash the password before saving
33 | AdminSchema.pre('save', function (next) {
34 | const user = this;
35 | if (!user.isModified('password')) return next();
36 |
37 | bcrypt.genSalt(saltRounds, function (err, salt) {
38 | if (err) return next(err);
39 |
40 | bcrypt.hash(user.password, salt, (err, hash) => {
41 | if (err) return next(err);
42 |
43 | user.password = hash;
44 | next();
45 | });
46 | });
47 | });
48 |
49 | // Compare the password
50 | AdminSchema.methods.comparePassword = function (password, callback) {
51 | bcrypt.compare(password, this.password, (err, isMatch) => {
52 | if (err) return callback(err);
53 | callback(null, isMatch);
54 | });
55 | };
56 |
57 | const Admin = mongoose.model('Admin', AdminSchema);
58 |
59 | export default Admin;
60 |
--------------------------------------------------------------------------------
/backend/models/CustomDomainsModel.js:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose';
2 |
3 | const customDomainSchema = new mongoose.Schema({
4 | url: {
5 | type: String,
6 | required: [true, 'Please provide a valid url value'],
7 | },
8 | dnsVerificationCode: {
9 | type: String,
10 | default: null,
11 | },
12 | isVerified: {
13 | type: Boolean,
14 | default: false,
15 | },
16 | user: {
17 | type: mongoose.Schema.Types.ObjectId,
18 | ref: 'User',
19 | },
20 | });
21 |
22 | const CustomDomain = mongoose.model('CustomDomain', customDomainSchema);
23 |
24 | export default CustomDomain;
25 |
--------------------------------------------------------------------------------
/backend/models/Feedback.js:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose';
2 | const feedbackSchema = new mongoose.Schema(
3 | {
4 | email: {
5 | type: String,
6 | required: true,
7 | },
8 | name: {
9 | type: String,
10 | required: true,
11 | },
12 | message: String,
13 | rating: Number,
14 | },
15 | {
16 | timestamps: true,
17 | },
18 | );
19 |
20 | const Feedback = mongoose.model('Feedback', feedbackSchema);
21 |
22 | // Middleware to parse JSON data
23 |
24 | export default Feedback;
25 |
--------------------------------------------------------------------------------
/backend/models/LinkInBioPageModel.js:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose';
2 |
3 | const LinkInBioPageSchema = new mongoose.Schema({
4 | userId: {
5 | type: mongoose.Schema.Types.ObjectId,
6 | ref: 'User',
7 | required: true,
8 | index: true,
9 | },
10 | pageTitle: {
11 | type: String,
12 | required: true,
13 | },
14 | links: [
15 | {
16 | title: String,
17 | url: String,
18 | description: String,
19 | },
20 | ],
21 | });
22 |
23 | const LinkInBioPage = mongoose.model('LinkInBioPage', LinkInBioPageSchema);
24 |
25 | export default LinkInBioPage;
26 |
--------------------------------------------------------------------------------
/backend/models/Tokenmodel.js:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose';
2 |
3 | const tokenSchema = new mongoose.Schema(
4 | {
5 | userId: {
6 | type: mongoose.Schema.Types.ObjectId,
7 | ref: 'User', // Reference to the User model
8 | required: true,
9 | },
10 | token: {
11 | type: String,
12 | required: true,
13 | },
14 | expiration: {
15 | type: Date,
16 | required: true,
17 | },
18 | },
19 | { timestamps: true },
20 | );
21 |
22 | const TokenModel = mongoose.model('Token', tokenSchema);
23 |
24 | export default TokenModel;
25 |
--------------------------------------------------------------------------------
/backend/models/UrlModel.js:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose';
2 |
3 | const urlSchema = new mongoose.Schema(
4 | {
5 | userId: {
6 | type: mongoose.Schema.Types.ObjectId,
7 | ref: 'User',
8 | index: true,
9 | },
10 |
11 | shortUrl: {
12 | type: String,
13 | index: true,
14 | uniuque: true,
15 | },
16 | originalUrl: { type: String },
17 | category: {
18 | type: String,
19 | index: true,
20 | },
21 | visitCount: {
22 | type: Number,
23 | default: 0,
24 | index: { order: -1 }, // this will help to get top visited urls, for sorting
25 | },
26 | customBackHalf: {
27 | type: String,
28 | },
29 | },
30 | {
31 | timestamps: true,
32 | },
33 | );
34 |
35 | const UrlModel = new mongoose.model('url_collection', urlSchema);
36 |
37 | export default UrlModel;
38 |
--------------------------------------------------------------------------------
/backend/models/UserModel.js:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose';
2 | import bcrypt from 'bcrypt';
3 |
4 | const saltRounds = 10; // Affects the performance and password security level
5 |
6 | const UserSchema = new mongoose.Schema(
7 | {
8 | email: {
9 | type: String,
10 | required: true,
11 | unique: true,
12 | },
13 |
14 | password: {
15 | type: String,
16 | required: true,
17 | },
18 |
19 | name: {
20 | type: String,
21 | required: true,
22 | },
23 |
24 | isEmailVerified: {
25 | type: Boolean,
26 | default: false, // Initially, the email is not verified
27 | },
28 | customDomain: {
29 | type: mongoose.Schema.Types.ObjectId,
30 | ref: 'CustomDomain',
31 | default: null,
32 | select: false,
33 | },
34 | },
35 |
36 | { timestamps: true },
37 | );
38 |
39 | // Hash the password before saving
40 | UserSchema.pre('save', function (next) {
41 | const user = this;
42 | if (!user.isModified('password')) return next();
43 |
44 | bcrypt.genSalt(saltRounds, function (err, salt) {
45 | if (err) return next(err);
46 |
47 | bcrypt.hash(user.password, salt, (err, hash) => {
48 | if (err) return next(err);
49 |
50 | user.password = hash;
51 | next();
52 | });
53 | });
54 | });
55 |
56 | // Compare the password
57 | UserSchema.methods.comparePassword = function (password, callback) {
58 | bcrypt.compare(password, this.password, (err, isMatch) => {
59 | if (err) return callback(err);
60 | callback(null, isMatch);
61 | });
62 | };
63 |
64 | // Add a reference to the LinkInBioPage model
65 | UserSchema.virtual('linkInBioPage', {
66 | ref: 'LinkInBioPage',
67 | localField: '_id',
68 | foreignField: 'userId',
69 | justOne: true,
70 | });
71 |
72 | const User = mongoose.model('User', UserSchema);
73 |
74 | export default User;
75 |
--------------------------------------------------------------------------------
/backend/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "restartable": "rs",
3 | "ignore": [
4 | ".git",
5 | "node_modules/**/node_modules"
6 | ],
7 | "verbose": true,
8 | "env": {
9 | "NODE_ENV": "development"
10 | },
11 | "ext": "js,json,html,css, yml",
12 | "delay": "2500"
13 | }
--------------------------------------------------------------------------------
/backend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "url-shortener",
3 | "version": "1.6.0",
4 | "description": "URL Shortener using Node and Express",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "nodemon index.js",
8 | "dev": "nodemon index.js",
9 | "format": "prettier --write \"**/*.{js,jsx}\"",
10 | "test": "mocha --timeout 10000"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git+https://github.com/DhananjayThomble/NodeJS-URL-Shortener-Microservice.git"
15 | },
16 | "keywords": [
17 | "urlshort",
18 | "shorturl",
19 | "urlshortener"
20 | ],
21 | "author": "Dhananjay Thomble",
22 | "bugs": {
23 | "url": "https://github.com/DhananjayThomble/NodeJS-URL-Shortener-Microservice/issues"
24 | },
25 | "homepage": "https://github.com/DhananjayThomble/NodeJS-URL-Shortener-Microservice#readme",
26 | "dependencies": {
27 | "aws-sdk": "^2.1691.0",
28 | "bcrypt": "^5.1.0",
29 | "bcryptjs": "^2.4.3",
30 | "body-parser": "^1.20.1",
31 | "cors": "^2.8.5",
32 | "crypto": "^1.0.1",
33 | "dotenv": "^16.0.3",
34 | "ejs": "^3.1.9",
35 | "exceljs": "^4.3.0",
36 | "express": "^4.18.2",
37 | "express-rate-limit": "^6.7.0",
38 | "express-session": "^1.17.3",
39 | "express-validator": "^7.0.1",
40 | "jsonwebtoken": "^9.0.0",
41 | "mongoose": "^6.8.0",
42 | "nanoid": "^4.0.0",
43 | "node-cron": "^3.0.2",
44 | "nodemailer": "^6.9.5",
45 | "passport": "^0.6.0",
46 | "passport-jwt": "^4.0.1",
47 | "passport-local": "^1.0.0",
48 | "random-words": "^2.0.0",
49 | "swagger-ui-express": "^4.6.0",
50 | "winston": "^3.14.2",
51 | "winston-cloudwatch": "^6.3.0",
52 | "yamljs": "^0.3.0"
53 | },
54 | "type": "module",
55 | "devDependencies": {
56 | "chai": "^4.3.10",
57 | "chai-http": "^4.4.0",
58 | "mocha": "^10.2.0",
59 | "nodemon": "^3.0.1",
60 | "prettier": "^3.0.3",
61 | "supertest": "^6.3.3"
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/backend/routes/admin.route.js:
--------------------------------------------------------------------------------
1 | import Router from 'express';
2 |
3 | const router = Router();
4 |
5 | import { isApiAuthenticated } from '../middlewares/authMiddleware.js';
6 | import { isAdmin } from '../middlewares/isAdmin.js';
7 | import { getReviews } from '../controllers/feedbackControllers/reviews.js';
8 | import {
9 | deleteAllFeedback,
10 | deleteReview,
11 | } from '../controllers/adminControllers/manageReviews.js';
12 |
13 | // Feedback Routes
14 | router.get('/reviews', isApiAuthenticated, isAdmin, getReviews);
15 | router.delete('/reviews/:_id', isApiAuthenticated, isAdmin, deleteReview);
16 | router.delete('/reviews', isApiAuthenticated, isAdmin, deleteAllFeedback);
17 |
18 | export default router;
19 |
--------------------------------------------------------------------------------
/backend/routes/adminAuth.route.js:
--------------------------------------------------------------------------------
1 | import Router from 'express';
2 |
3 | const router = Router();
4 |
5 | import {
6 | forgetPasswordValidator,
7 | validateLogin,
8 | validateSignup,
9 | } from '../validators/authValidators.js';
10 |
11 | import { validationErrorHandler } from '../middlewares/ValidatorErrorHandler.js';
12 | import { adminLogin } from '../controllers/adminControllers/adminLogin.js';
13 | import { adminSignup } from '../controllers/adminControllers/adminSignup.js';
14 | import { adminForgotPassword } from '../controllers/adminControllers/adminForgotPassword.js';
15 | import { adminResetPassword } from '../controllers/adminControllers/adminResetPassword.js';
16 | import { adminVerifyEmail } from '../controllers/adminControllers/adminVerifyEmail.js';
17 | import { isApiAuthenticated } from '../middlewares/authMiddleware.js';
18 |
19 | // Auth Routes
20 | router.post('/login', validateLogin, validationErrorHandler, adminLogin);
21 | router.post('/signup', validateSignup, validationErrorHandler, adminSignup);
22 | router.post(
23 | '/forgot-password',
24 | forgetPasswordValidator,
25 | validationErrorHandler,
26 | adminForgotPassword,
27 | ); //For Sending the password reset request
28 | router.post('/reset-password', adminResetPassword); // For Reseting the password
29 | router.get('/verify-email', adminVerifyEmail);
30 |
31 | export default router;
32 |
--------------------------------------------------------------------------------
/backend/routes/domain.route.js:
--------------------------------------------------------------------------------
1 | import { Router } from 'express';
2 | import { isApiAuthenticated } from '../middlewares/authMiddleware.js';
3 | import { addCustomDomain } from '../controllers/domainControllers/addCustomDomain.js';
4 |
5 | const router = Router();
6 |
7 | router.use(isApiAuthenticated);
8 |
9 | router.post('/add-custom-domain', addCustomDomain);
10 |
11 | export default router;
12 |
--------------------------------------------------------------------------------
/backend/routes/url.route.js:
--------------------------------------------------------------------------------
1 | import Router from 'express';
2 |
3 | const router = Router();
4 |
5 | import { redirectToOriginalUrl } from '../controllers/urlControllers/redirectToURL.js';
6 | import { deleteUrl } from '../controllers/urlControllers/deleteUrl.js';
7 | import { getHistory } from '../controllers/urlControllers/getHistory.js';
8 | import { generateShortUrl } from '../controllers/urlControllers/generateShortUrl.js';
9 | import { exportGeneratedUrls } from '../controllers/urlControllers/exportGeneratedUrls.js';
10 | import { generateCustomBackHalf } from '../controllers/urlControllers/generateCustomBackHalf.js';
11 | import { validateUrl } from '../validators/urlValidator.js';
12 |
13 | import { isApiAuthenticated } from '../middlewares/authMiddleware.js';
14 | import { validationErrorHandler } from '../middlewares/ValidatorErrorHandler.js';
15 | import {
16 | getFilteredCategory,
17 | updateCategory,
18 | } from '../controllers/urlControllers/category.js';
19 |
20 | // old route for backward compatibility
21 | router.get('/url/:short', redirectToOriginalUrl);
22 |
23 | router.post(
24 | '/url',
25 | isApiAuthenticated,
26 | validateUrl,
27 | validationErrorHandler,
28 | generateShortUrl,
29 | );
30 |
31 | router.post('/url/custom', isApiAuthenticated, generateCustomBackHalf);
32 |
33 | router.get('/history', isApiAuthenticated, getHistory);
34 |
35 | router.delete('/delete/:id', isApiAuthenticated, deleteUrl);
36 |
37 | router.get('/export', isApiAuthenticated, exportGeneratedUrls);
38 |
39 | router.get('/filter/:category', isApiAuthenticated, getFilteredCategory);
40 |
41 | router.put('/filter', isApiAuthenticated, updateCategory);
42 |
43 | export default router;
44 |
--------------------------------------------------------------------------------
/backend/routes/userAuth.route.js:
--------------------------------------------------------------------------------
1 | import Router from 'express';
2 |
3 | const router = Router();
4 |
5 | import { isApiAuthenticated } from '../middlewares/authMiddleware.js';
6 |
7 | import { login } from '../controllers/authControllers/login.js';
8 | import { signup } from '../controllers/authControllers/signup.js';
9 | import { forgotPassword } from '../controllers/authControllers/forgotPassword.js';
10 | import { resetPassword } from '../controllers/authControllers/resetPassword.js';
11 | import { verifyEmail } from '../controllers/authControllers/verifyEmail.js';
12 | import {
13 | validateLogin,
14 | validateSignup,
15 | forgetPasswordValidator,
16 | } from '../validators/authValidators.js';
17 |
18 | import { validationErrorHandler } from '../middlewares/ValidatorErrorHandler.js';
19 |
20 | import { validateFeedback } from '../validators/feedbackValidator.js';
21 | import { submitFeedback } from '../controllers/feedbackControllers/feedback.js';
22 |
23 | import { changeEmail } from '../controllers/authControllers/changeEmail.js';
24 | import { getCurrentUser } from '../controllers/authControllers/getCurrentUser.js';
25 | import { changeName } from '../controllers/authControllers/changeName.js';
26 | import { validateEmail } from '../validators/authValidators.js';
27 | import { validateName } from '../validators/authValidators.js';
28 | import { validateToken } from '../validators/authValidators.js';
29 |
30 | // Auth Routes
31 | router.post('/login', validateLogin, validationErrorHandler, login);
32 | router.post('/signup', validateSignup, validationErrorHandler, signup);
33 | router.post(
34 | '/forgot-password',
35 | forgetPasswordValidator,
36 | validationErrorHandler,
37 | forgotPassword,
38 | ); //For Sending the password reset request
39 | router.post('/reset-password', resetPassword); // For Reseting the password
40 | router.get('/verify-email', validateToken, validationErrorHandler, verifyEmail);
41 |
42 | // Profile Routes
43 | router.get('/current-user', getCurrentUser);
44 | router.patch(
45 | '/change-email',
46 | validateEmail,
47 | validationErrorHandler,
48 | changeEmail,
49 | );
50 | router.patch('/change-name', validateName, validationErrorHandler, changeName);
51 |
52 | /*Feedback Routes*/
53 | router.post(
54 | '/feedback',
55 | isApiAuthenticated,
56 | validateFeedback,
57 | validationErrorHandler,
58 | submitFeedback,
59 | );
60 |
61 | export default router;
62 |
--------------------------------------------------------------------------------
/backend/test/mainTest.js:
--------------------------------------------------------------------------------
1 | import { app } from '../index.js';
2 |
3 | export { app };
4 |
--------------------------------------------------------------------------------
/backend/test/urlController.test.js:
--------------------------------------------------------------------------------
1 | import chaiHttp from 'chai-http';
2 | import chai from 'chai';
3 | import { expect } from 'chai';
4 | import dotenv from 'dotenv';
5 | import { app } from './mainTest.js';
6 |
7 | dotenv.config();
8 |
9 | chai.use(chaiHttp);
10 |
11 | const MOCK_TOKEN =
12 | 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2NTFlZTBlOGQ4NTQ3NDkyNGVlYTU2ZmIiLCJpYXQiOjE2OTcxMjYzMDIsImV4cCI6MTY5NzczMTEwMn0.FbNfFFiG0biv_4kD16h8OVXqUcvsEksh5iRFusACKhI';
13 |
14 | const EXPIRED_TOKEN =
15 | 'abchbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2NTFlZTBlOGQ4NTQ3NDkyNGVlYTU2ZmIiLCJpYXQiOjE2OTcxMjYzMDIsImV4cCI6MTY5NzczMTEwMn0.FbNfFFiG0biv_4kD16h8OVXqUcvsEksh5iRFusACKhI';
16 |
17 | describe('URL Controller Tests', () => {
18 | it('should generate a short URL with a valid URL and token', (done) => {
19 | chai
20 | .request(app)
21 | .post(`/api/url`)
22 | .set('Authorization', 'Bearer ' + MOCK_TOKEN)
23 | .send({ url: 'https://dturl.live/' })
24 | .end((err, res) => {
25 | expect(err).to.be.null; // Check for no errors
26 | expect(res).to.have.status(200); // Check for the expected HTTP status
27 | expect(res.body).to.have.property('shortUrl'); // Check for the expected response properties
28 | done(); // call done() to indicate that the test is complete
29 | });
30 | });
31 |
32 | it('should return an error for an invalid URL', (done) => {
33 | chai
34 | .request(app)
35 | .post(`/api/url`)
36 | .set('Authorization', 'Bearer ' + MOCK_TOKEN)
37 | .send({ url: 'invalid-url' })
38 | .end((err, res) => {
39 | expect(err).to.be.null;
40 | expect(res).to.have.status(400);
41 | // we can check for an error response, e.g., `expect(res.body).to.have.property("error")`
42 | done();
43 | });
44 | });
45 |
46 | it('should return an error if no token is provided', (done) => {
47 | chai
48 | .request(app)
49 | .post(`/api/url`)
50 | .send({ url: 'https://dturl.live/' })
51 | .end((err, res) => {
52 | expect(err).to.be.null;
53 | expect(res).to.have.status(401);
54 | done();
55 | });
56 | });
57 |
58 | it('should return an error if an expired token is used', (done) => {
59 | chai
60 | .request(app)
61 | .post(`/api/url`)
62 | .set('Authorization', 'Bearer ' + EXPIRED_TOKEN)
63 | .send({ url: 'https://dturl.live/' })
64 | .end((err, res) => {
65 | expect(err).to.be.null;
66 | expect(res).to.have.status(401);
67 | done();
68 | });
69 | });
70 | });
71 |
72 | // dummy test
73 | describe('Dummy Test', () => {
74 | it('should return true', (done) => {
75 | expect(true).to.be.true;
76 | done();
77 | });
78 | });
79 |
--------------------------------------------------------------------------------
/backend/utils/dns.js:
--------------------------------------------------------------------------------
1 | import dns from 'node:dns';
2 |
3 | /**
4 | * Verifies DNS records for a given hostname against a verification code
5 | *
6 | * @param {String} hostname - hostname to check the verification against
7 | * @param {String} text - verification code for the domain
8 | * @returns Promise. The resolved object has the following structure:
9 | * - status (boolean): Indicates if verification was successful
10 | * - message (string): A message describing the verification status
11 | */
12 | export const checkDNSTxtVerification = (hostname, text) => {
13 | return new Promise((resolve, reject) => {
14 | if (!hostname) {
15 | return reject(new Error('Please provide a hostname'));
16 | }
17 |
18 | dns.resolveTxt(hostname, (error, results) => {
19 | if (error) {
20 | return reject(error);
21 | }
22 |
23 | if (!results || results.length === 0) {
24 | reject({
25 | status: false,
26 | message: 'Verification code not present in the DNS records',
27 | });
28 | }
29 |
30 | let containsCode = false;
31 |
32 | results.forEach((result) => {
33 | if (result.includes(text)) {
34 | containsCode = true;
35 | }
36 | });
37 |
38 | if (containsCode) {
39 | resolve({ status: true, message: 'Verification successful' });
40 | }
41 |
42 | reject({
43 | status: false,
44 | message: 'Wrong verification code provided',
45 | });
46 | });
47 | });
48 | };
49 |
--------------------------------------------------------------------------------
/backend/utils/helperfunc.js:
--------------------------------------------------------------------------------
1 | export const isValidEmail = (email) => {
2 | validEmailRegex = /\b[\w\.-]+@[\w\.-]+\.\w{2,4}\b/gi;
3 | return validEmailRegex.test(email);
4 | };
5 |
--------------------------------------------------------------------------------
/backend/utils/mailSend.js:
--------------------------------------------------------------------------------
1 | import nodemailer from 'nodemailer';
2 | import TokenModel from '../models/Tokenmodel.js';
3 | import dotenv from 'dotenv';
4 | import crypto from 'crypto';
5 | import fs from 'fs';
6 | import ejs from 'ejs';
7 | dotenv.config();
8 |
9 | const transporter = nodemailer.createTransport({
10 | host: process.env.EMAIL_HOST,
11 | port: process.env.EMAIL_PORT,
12 | secure: true,
13 | requireTLS: true,
14 | auth: {
15 | user: process.env.EMAIL_HOST_USER,
16 | pass: process.env.EMAIL_HOST_PASSWORD,
17 | },
18 | });
19 |
20 | export const sendEmail = async (options) => {
21 | return new Promise((resolve, reject) => {
22 | try {
23 | const subject = options.subject;
24 | const recipient = options.recipient;
25 | const html = options.html;
26 |
27 | const mailOptions = {
28 | from: options.from,
29 | to: recipient,
30 | subject: subject,
31 | html: html,
32 | };
33 |
34 | transporter.sendMail(mailOptions, (error, info) => {
35 | if (error) {
36 | console.error(`Error from mailSender ${error}`);
37 | reject(error);
38 | } else {
39 | console.log(`Email sent: ${info.response}`);
40 | resolve(info);
41 | }
42 | });
43 | } catch (error) {
44 | console.error(`Error from mailSender ${error}`);
45 | reject(error);
46 | }
47 | });
48 | };
49 |
50 | // <--------------------Sending email to the user ----------------------------------->
51 |
52 | export const sendWelcomeEmail = async (name, email, userID) => {
53 | try {
54 | console.log('Sending welcome email to', email);
55 | const verifyEmailTemplate = fs.readFileSync(
56 | './views/welcome_email_template.ejs',
57 | 'utf-8',
58 | );
59 |
60 | const dataToRender = {
61 | name: name,
62 | verificationLink: await getVerificationLink(userID),
63 | };
64 |
65 | const htmlTemplate = ejs.render(verifyEmailTemplate, dataToRender);
66 |
67 | const options = {
68 | from: 'SnapURL@snapurl.in',
69 | subject: 'Welcome to SnapURL🔗',
70 | recipient: email,
71 | html: htmlTemplate,
72 | };
73 |
74 | await sendEmail(options);
75 | } catch (err) {
76 | console.log('Error sending welcome email to', email);
77 | console.error(err);
78 | }
79 | };
80 |
81 | const getVerificationLink = async (userId) => {
82 | try {
83 | const token = crypto.randomBytes(32).toString('hex'); // Generate a random token
84 | const expiration = Date.now() + 24 * 60 * 60 * 1000; // Token expires in 24 hours
85 |
86 | const verificationToken = new TokenModel({
87 | userId,
88 | token,
89 | expiration,
90 | });
91 |
92 | await verificationToken.save();
93 |
94 | // const verificationLink = `http://localhost:4001/auth/verify-email?token=${token}`;
95 | const verificationLink = `${process.env.BASE_URL}/auth/verify-email?token=${token}`;
96 | return verificationLink;
97 | } catch (error) {
98 | console.error(error);
99 | return null;
100 | }
101 | };
102 |
103 | export const sendPasswordResetEmail = async (email, resetLink) => {
104 | try {
105 | const resetPasswordTemplate = fs.readFileSync(
106 | './views/reset_password_email_template.ejs',
107 | 'utf-8',
108 | );
109 |
110 | const dataToRender = {
111 | resetLink: resetLink,
112 | };
113 |
114 | const htmlTemplate = ejs.render(resetPasswordTemplate, dataToRender);
115 |
116 | const options = {
117 | from: 'SnapURL@snapurl.in',
118 | subject: 'Reset your SnapURL password',
119 | recipient: email,
120 | html: htmlTemplate,
121 | };
122 |
123 | await sendEmail(options);
124 | } catch (err) {
125 | console.log('Error sending password reset email to', email);
126 | console.error(err);
127 | }
128 | };
129 |
--------------------------------------------------------------------------------
/backend/validators/authValidators.js:
--------------------------------------------------------------------------------
1 | import { body, query } from 'express-validator';
2 | import User from '../models/UserModel.js';
3 |
4 | export const validateLogin = [
5 | body('email')
6 | .notEmpty()
7 | .trim()
8 | .escape()
9 | .isEmail()
10 | .withMessage('Invalid email'),
11 | body('password')
12 | .notEmpty()
13 | .trim()
14 | .escape()
15 | .withMessage('Password is required'),
16 | ];
17 |
18 | export const validateSignup = [
19 | body('name')
20 | .notEmpty()
21 | .trim()
22 | .escape()
23 | .withMessage('Name is required')
24 | .bail()
25 | .isLength({ min: 3 })
26 | .withMessage('Name must be at least 3 characters long'),
27 |
28 | body('email')
29 | .notEmpty()
30 | .trim()
31 | .escape()
32 | .isEmail()
33 | .withMessage('Invalid email format')
34 | .bail()
35 | .custom(async (value) => {
36 | const user = await User.findOne({ email: value });
37 | if (user) {
38 | throw new Error('Email already in use');
39 | }
40 | }),
41 | // body("mobileNumber")
42 | // .notEmpty()
43 | // .trim()
44 | // .escape()
45 | // .isMobilePhone("en-IN")
46 | // .withMessage("Invalid mobile number format")
47 | // .bail()
48 | // .custom(async (value) => {
49 | // const user = await User.findOne({ mobileNumber: value });
50 | // if (user) {
51 | // throw new Error("Mobile number already in use");
52 | // }
53 | // }),
54 | body('password')
55 | .notEmpty()
56 | .trim()
57 | .escape()
58 | .withMessage('Password is required')
59 | .isLength({ min: 6 })
60 | .withMessage('Password must be at least 6 characters long'),
61 | // body("confirmPassword")
62 | // .notEmpty()
63 | // .trim()
64 | // .escape()
65 | // .withMessage("Confirm password is required")
66 | // .bail()
67 | // .custom((value, { req }) => {
68 | // if (value !== req.body.password) {
69 | // throw new Error("Passwords do not match");
70 | // }
71 | // return true;
72 | // }),
73 | ];
74 |
75 | // validator for the forgot password
76 | export const forgetPasswordValidator = [
77 | body('email')
78 | .notEmpty()
79 | .withMessage('Email is required')
80 | .isEmail()
81 | .withMessage('Invalid Email Address'),
82 | ];
83 |
84 | export const validateName = [
85 | body('name')
86 | .notEmpty()
87 | .trim()
88 | .escape()
89 | .withMessage('Name is required')
90 | .bail()
91 | .isLength({ min: 3 })
92 | .withMessage('Name must be at least 3 characters long'),
93 | ];
94 |
95 | export const validateEmail = [
96 | body('email')
97 | .notEmpty()
98 | .trim()
99 | .escape()
100 | .withMessage('Email is required')
101 | .bail()
102 | .isEmail()
103 | .withMessage('Invalid email format')
104 | .bail()
105 | .custom(async (value) => {
106 | const user = await User.findOne({ email: value });
107 | if (user) {
108 | throw new Error('Email already in use');
109 | }
110 | }),
111 | ];
112 |
113 | export const validateToken = [
114 | query('token').notEmpty().trim().escape().withMessage('Token is required'),
115 | ];
116 |
--------------------------------------------------------------------------------
/backend/validators/feedbackValidator.js:
--------------------------------------------------------------------------------
1 | import { body } from 'express-validator';
2 | export const validateFeedback = [
3 | body('message').notEmpty().trim().escape().withMessage('Message is required'),
4 | body('rating')
5 | .notEmpty()
6 | .trim()
7 | .escape()
8 | .withMessage('Rating is required')
9 | .isInt({ min: 1, max: 5 })
10 | .withMessage('Rating must be between 1 and 5'),
11 | ];
12 |
--------------------------------------------------------------------------------
/backend/validators/urlValidator.js:
--------------------------------------------------------------------------------
1 | import { check } from 'express-validator';
2 | export const validateUrl = [check('url').isURL().withMessage('Invalid URL')];
3 |
4 | export const validateShortId = [
5 | check('short').isLength({ min: 10, max: 10 }).withMessage('Invalid URL'),
6 | ];
7 |
--------------------------------------------------------------------------------
/backend/views/reset_password_email_template.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Reset Your Password
9 |
10 |
49 |
50 |
51 |
52 |
53 |
55 |
56 |
57 |
Reset Your Password
58 |
You are receiving this email because we received a password reset request for your SnapURL account. If
59 | you
60 | did not request this change, please ignore this email.
61 |
Click the button below to reset your password. If you're having trouble, copy and paste the URL below
62 | into
63 | your web browser:
64 |
Reset
66 | Your Password
67 |
68 |
69 | © <%= new Date().getFullYear() %> SnapURL. All rights reserved.
70 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/backend/views/welcome_email_template.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Welcome to SnapURL
8 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
Welcome to SnapURL, <%= name %>!
55 |
Your URL Shortening Platform 🚀
56 |
57 | Thank you for joining SnapURL. We're thrilled to have you on board. Get started now by shortening your URLs
58 | and managing your links with ease.
59 |
60 |
61 | To complete your registration, please verify your email address by clicking the following link:
62 | Verify
63 | Email
64 |
65 |
66 |
67 | © <%= new Date().getFullYear() %> SnapURL. All rights reserved.
68 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/chrome-extension/extension.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
10 |
16 |
17 |
78 |
79 |
80 |
121 |
122 |
123 |
124 |
125 |
--------------------------------------------------------------------------------
/chrome-extension/history.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
13 |
14 | URL Shortener History
15 |
16 |
17 | URL Shortener History
18 |
19 |
20 |
21 | Short URL
22 | Original URL
23 | Visit Count
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/chrome-extension/history.js:
--------------------------------------------------------------------------------
1 | const token =
2 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2NTFjZDJiNTI1YTUwOGIwODQ0NmQzYWIiLCJpYXQiOjE2OTYzODc3ODQsImV4cCI6MTY5Njk5MjU4NH0.j1ubpVSrqfm9DhTUYQ4xBhZJj1WYHY1E5WmAYX4HnBM";
3 |
4 | document.addEventListener("DOMContentLoaded", async function () {
5 | const historyTable = document.getElementById("historyTable");
6 |
7 | try {
8 | // Fetch history data from your server
9 | const response = await fetch("https://dturl.live/api/history", {
10 | method: "GET",
11 | headers: {
12 | "Content-Type": "application/json",
13 | Authorization: `Bearer ${token}`, // Replace with your actual authorization token
14 | },
15 | });
16 |
17 | if (!response.ok) {
18 | throw new Error("Error fetching history data.");
19 | }
20 |
21 | const historyData = await response.json();
22 | console.log(historyData.urlArray);
23 | // Function to populate the table with history data
24 | function populateTable() {
25 | historyData.urlArray.forEach((entry) => {
26 | const row = document.createElement("tr");
27 |
28 | const shortUrlCell = document.createElement("td");
29 | shortUrlCell.textContent = `https://dturl.live/u/${entry.shortUrl}`;
30 |
31 | const originalUrlCell = document.createElement("td");
32 | originalUrlCell.textContent = entry.originalUrl;
33 |
34 | const visitCountCell = document.createElement("td");
35 | visitCountCell.textContent = entry.visitCount;
36 |
37 | row.appendChild(shortUrlCell);
38 | row.appendChild(originalUrlCell);
39 | row.appendChild(visitCountCell);
40 |
41 | historyTable.appendChild(row);
42 | });
43 | }
44 |
45 | // Call the populateTable function when the page loads
46 | populateTable();
47 | } catch (error) {
48 | console.error(error);
49 | alert("An error occurred while fetching history data.");
50 | }
51 | });
52 |
--------------------------------------------------------------------------------
/chrome-extension/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/chrome-extension/icon.png
--------------------------------------------------------------------------------
/chrome-extension/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/chrome-extension/logo.png
--------------------------------------------------------------------------------
/chrome-extension/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Quick URL Shortener",
3 | "description": "Quickly shorten URLs without leaving the page you're on.",
4 | "version": "1.0",
5 | "manifest_version": 3,
6 | "action": {
7 | "default_popup": "extension.html",
8 | "default_icon": "icon.png"
9 | },
10 | "permissions": [
11 | "activeTab"
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/chrome-extension/script.js:
--------------------------------------------------------------------------------
1 | document.addEventListener("DOMContentLoaded", function () {
2 | const form = document.querySelector("form");
3 | const urlInput = form.querySelector("#inputUrl");
4 | const shortUrl = form.querySelector("#shortUrl");
5 | const copyBtn = form.querySelector("#copyBtn");
6 | const token =
7 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2NTFjZDJiNTI1YTUwOGIwODQ0NmQzYWIiLCJpYXQiOjE2OTYzODc3ODQsImV4cCI6MTY5Njk5MjU4NH0.j1ubpVSrqfm9DhTUYQ4xBhZJj1WYHY1E5WmAYX4HnBM";
8 | form.addEventListener("submit", async (e) => {
9 | e.preventDefault();
10 | const longUrl = urlInput.value.trim(); // Trim any leading/trailing spaces
11 |
12 | if (!longUrl) {
13 | // Check if the URL input is empty
14 | alert("Please enter a valid URL.");
15 | return;
16 | }
17 |
18 | const data = { url: longUrl };
19 |
20 | try {
21 | const response = await fetch("https://dturl.live/api/url", {
22 | method: "POST",
23 | headers: {
24 | "Content-Type": "application/json",
25 | Authorization: `Bearer ${token}`,
26 | },
27 | body: JSON.stringify(data),
28 | });
29 |
30 | if (!response.ok) {
31 | throw new Error("Error shortening URL.");
32 | }
33 |
34 | const responseData = await response.json();
35 |
36 | shortUrl.value = responseData.shortUrl;
37 | shortUrl.style.display = "block";
38 | copyBtn.style.display = "block";
39 | } catch (error) {
40 | console.error(error);
41 | alert("An error occurred while shortening the URL.");
42 | }
43 | });
44 |
45 | // Copy to clipboard functionality (no changes needed)
46 | copyBtn.addEventListener("click", () => {
47 | shortUrl.select();
48 | document.execCommand("copy");
49 | copyBtn.innerHTML = "Copied to clipboard";
50 | });
51 |
52 | // Get current tab URL
53 | chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
54 | const url = tabs[0].url;
55 | urlInput.value = url;
56 | });
57 |
58 | // Add a click event listener to the historyBtn
59 | historyBtn.addEventListener("click", async () => {
60 | // try {
61 | // const response = await fetch("https://dturl.live/api/history", {
62 | // method: "GET",
63 | // headers: {
64 | // "Content-Type": 'application/json',
65 | // Authorization: `Bearer ${token}`,
66 | // },
67 | // });
68 |
69 | // if (!response.ok) {
70 | // throw new Error("Error fetching history.");
71 | // }
72 |
73 | // const responseData = await response.json();
74 | // console.log(responseData)
75 | // // Open the history in a new tab
76 | chrome.tabs.create({ url: chrome.runtime.getURL('history.html')});
77 | // } catch (error) {
78 | // console.error(error);
79 | // alert("An error occurred while fetching the history.");
80 | // }
81 | });
82 | });
83 |
--------------------------------------------------------------------------------
/designs/User-Profile-Page/Desktop - User Page - Favorites.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/User-Profile-Page/Desktop - User Page - Favorites.png
--------------------------------------------------------------------------------
/designs/User-Profile-Page/Desktop - User Page - Links.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/User-Profile-Page/Desktop - User Page - Links.png
--------------------------------------------------------------------------------
/designs/User-Profile-Page/Desktop - User Page - Profile edit Settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/User-Profile-Page/Desktop - User Page - Profile edit Settings.png
--------------------------------------------------------------------------------
/designs/User-Profile-Page/Desktop - User Page - Profile edit Settings_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/User-Profile-Page/Desktop - User Page - Profile edit Settings_2.png
--------------------------------------------------------------------------------
/designs/User-Profile-Page/FigmaLink.txt:
--------------------------------------------------------------------------------
1 | https://www.figma.com/file/3rm1By9lxmClZZaaWaU7tf/URL-Shortener-App?type=design&node-id=0%3A1&mode=design&t=tW1Yh7TdGDrBApbB-1
2 |
--------------------------------------------------------------------------------
/designs/User-Profile-Page/Mobile - User Page - Favourites.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/User-Profile-Page/Mobile - User Page - Favourites.png
--------------------------------------------------------------------------------
/designs/User-Profile-Page/Mobile - User Page - Links.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/User-Profile-Page/Mobile - User Page - Links.png
--------------------------------------------------------------------------------
/designs/User-Profile-Page/Mobile - User Page - Profile Edit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/User-Profile-Page/Mobile - User Page - Profile Edit.png
--------------------------------------------------------------------------------
/designs/profile-finder-page/desktop/desktop 1 (no description).pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/profile-finder-page/desktop/desktop 1 (no description).pdf
--------------------------------------------------------------------------------
/designs/profile-finder-page/desktop/desktop 1 (no description).png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/profile-finder-page/desktop/desktop 1 (no description).png
--------------------------------------------------------------------------------
/designs/profile-finder-page/desktop/desktop 1.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/profile-finder-page/desktop/desktop 1.pdf
--------------------------------------------------------------------------------
/designs/profile-finder-page/desktop/desktop 1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/profile-finder-page/desktop/desktop 1.png
--------------------------------------------------------------------------------
/designs/profile-finder-page/desktop/desktop 2 (no description).pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/profile-finder-page/desktop/desktop 2 (no description).pdf
--------------------------------------------------------------------------------
/designs/profile-finder-page/desktop/desktop 2 (no description).png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/profile-finder-page/desktop/desktop 2 (no description).png
--------------------------------------------------------------------------------
/designs/profile-finder-page/desktop/desktop 2.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/profile-finder-page/desktop/desktop 2.pdf
--------------------------------------------------------------------------------
/designs/profile-finder-page/desktop/desktop 2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/profile-finder-page/desktop/desktop 2.png
--------------------------------------------------------------------------------
/designs/profile-finder-page/mobile/mobile 1 (no description).pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/profile-finder-page/mobile/mobile 1 (no description).pdf
--------------------------------------------------------------------------------
/designs/profile-finder-page/mobile/mobile 1 (no description).png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/profile-finder-page/mobile/mobile 1 (no description).png
--------------------------------------------------------------------------------
/designs/profile-finder-page/mobile/mobile 1.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/profile-finder-page/mobile/mobile 1.pdf
--------------------------------------------------------------------------------
/designs/profile-finder-page/mobile/mobile 1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/profile-finder-page/mobile/mobile 1.png
--------------------------------------------------------------------------------
/designs/profile-finder-page/mobile/mobile 2 (no description).pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/profile-finder-page/mobile/mobile 2 (no description).pdf
--------------------------------------------------------------------------------
/designs/profile-finder-page/mobile/mobile 2 (no description).png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/profile-finder-page/mobile/mobile 2 (no description).png
--------------------------------------------------------------------------------
/designs/profile-finder-page/mobile/mobile 2.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/profile-finder-page/mobile/mobile 2.pdf
--------------------------------------------------------------------------------
/designs/profile-finder-page/mobile/mobile 2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/profile-finder-page/mobile/mobile 2.png
--------------------------------------------------------------------------------
/designs/public-profile-page/desktop 1.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/public-profile-page/desktop 1.pdf
--------------------------------------------------------------------------------
/designs/public-profile-page/desktop/desktop 1.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/public-profile-page/desktop/desktop 1.pdf
--------------------------------------------------------------------------------
/designs/public-profile-page/desktop/desktop2.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/public-profile-page/desktop/desktop2.pdf
--------------------------------------------------------------------------------
/designs/public-profile-page/desktop2.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/public-profile-page/desktop2.pdf
--------------------------------------------------------------------------------
/designs/public-profile-page/mobile/mobile1.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/public-profile-page/mobile/mobile1.pdf
--------------------------------------------------------------------------------
/designs/public-profile-page/mobile/mobile1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/public-profile-page/mobile/mobile1.png
--------------------------------------------------------------------------------
/designs/public-profile-page/mobile/mobile2.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/public-profile-page/mobile/mobile2.pdf
--------------------------------------------------------------------------------
/designs/public-profile-page/mobile/mobile2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/public-profile-page/mobile/mobile2.png
--------------------------------------------------------------------------------
/designs/public-profile-page/mobile1.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/public-profile-page/mobile1.pdf
--------------------------------------------------------------------------------
/designs/public-profile-page/mobile1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/public-profile-page/mobile1.png
--------------------------------------------------------------------------------
/designs/public-profile-page/mobile2.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/public-profile-page/mobile2.pdf
--------------------------------------------------------------------------------
/designs/public-profile-page/mobile2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/public-profile-page/mobile2.png
--------------------------------------------------------------------------------
/frontend/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es2021": true,
5 | "node": true
6 | },
7 | "extends": [
8 | "eslint:recommended",
9 | "plugin:react/recommended",
10 | "plugin:react-hooks/recommended"
11 | ],
12 | "parserOptions": {
13 | "ecmaVersion": 12,
14 | "sourceType": "module",
15 | "ecmaFeatures": {
16 | "jsx": true
17 | }
18 | },
19 | "plugins": ["react", "react-hooks"],
20 | "rules": {
21 | "react/prop-types": "off",
22 | "react/no-unescaped-entities": "off"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/frontend/.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 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/frontend/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "all",
3 | "tabWidth": 2,
4 | "semi": true,
5 | "singleQuote": true
6 | }
7 |
--------------------------------------------------------------------------------
/frontend/README.md:
--------------------------------------------------------------------------------
1 | ## Front-End for URL Shortener
--------------------------------------------------------------------------------
/frontend/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
14 |
20 |
21 | SnapURL
22 |
23 |
24 |
25 |
32 |
33 | You need to enable JavaScript to run this app.
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "snapurl-frontend",
3 | "version": "1.0.0",
4 | "private": true,
5 | "author": "SnapURL Team",
6 | "dependencies": {
7 | "@emotion/react": "^11.13.3",
8 | "@emotion/styled": "^11.13.0",
9 | "@fortawesome/free-brands-svg-icons": "^6.4.2",
10 | "@fortawesome/react-fontawesome": "^0.2.0",
11 | "@mui/icons-material": "^5.14.12",
12 | "@mui/material": "^5.16.7",
13 | "@testing-library/jest-dom": "^5.16.5",
14 | "@testing-library/react": "^13.4.0",
15 | "@testing-library/user-event": "^13.5.0",
16 | "axios": "^1.5.1",
17 | "file-saver": "^2.0.5",
18 | "formik": "^2.4.5",
19 | "lottie-react": "^2.4.0",
20 | "prettier": "^2.8.3",
21 | "prop-types": "^15.8.1",
22 | "qrcode.react": "^3.1.0",
23 | "react": "^18.2.0",
24 | "react-bootstrap": "^2.7.0",
25 | "react-copy-to-clipboard": "^5.1.0",
26 | "react-dom": "^18.2.0",
27 | "react-icons": "^4.7.1",
28 | "react-router-bootstrap": "^0.26.2",
29 | "react-router-dom": "^6.7.0",
30 | "react-toastify": "^9.1.3",
31 | "shortid": "^2.2.16",
32 | "web-vitals": "^2.1.4",
33 | "yup": "^1.3.2"
34 | },
35 | "scripts": {
36 | "dev": "vite",
37 | "start": "vite dev",
38 | "build": "vite build",
39 | "test": "vite test",
40 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
41 | "preview": "vite preview",
42 | "format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,css,scss,md,json}\""
43 | },
44 | "eslintConfig": {
45 | "extends": [
46 | "react-app",
47 | "react-app/jest"
48 | ]
49 | },
50 | "browserslist": {
51 | "production": [
52 | ">0.2%",
53 | "not dead",
54 | "not op_mini all"
55 | ],
56 | "development": [
57 | "last 1 chrome version",
58 | "last 1 firefox version",
59 | "last 1 safari version"
60 | ]
61 | },
62 | "devDependencies": {
63 | "@vitejs/plugin-react-swc": "^3.4.0",
64 | "eslint": "^8.52.0",
65 | "eslint-plugin-react": "^7.33.2",
66 | "eslint-plugin-react-hooks": "^4.6.0",
67 | "vite": "^4.5.0"
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/frontend/public/_redirects:
--------------------------------------------------------------------------------
1 | /* /index.html 200
--------------------------------------------------------------------------------
/frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/frontend/public/favicon.ico
--------------------------------------------------------------------------------
/frontend/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/frontend/public/logo.png
--------------------------------------------------------------------------------
/frontend/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "SnapURL",
3 | "name": "URL Shortener App by SnapURL Team",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo.png",
12 | "type": "image/png",
13 | "sizes": "512x512"
14 | }
15 | ],
16 | "start_url": ".",
17 | "display": "standalone",
18 | "theme_color": "#000000",
19 | "background_color": "#ffffff"
20 | }
21 |
--------------------------------------------------------------------------------
/frontend/src/App.css:
--------------------------------------------------------------------------------
1 | body {
2 | background-color: #f5f5f5;
3 | }
4 |
5 | .card {
6 | box-shadow: 0px 0px 10px #8c7a7a;
7 | }
8 |
9 | .card-header {
10 | background-color: #007bff;
11 | color: white;
12 | }
13 |
14 | .card-footer a {
15 | color: #007bff;
16 | }
17 |
18 | .card-footer a:hover {
19 | color: #0056b3;
20 | text-decoration: none;
21 | }
22 |
23 | .btn-info {
24 | background-color: #007bff;
25 | border-color: #007bff;
26 | color: whitesmoke;
27 | }
28 |
29 | .btn-info:hover {
30 | background-color: #0056b3;
31 | border-color: #0056b3;
32 | color: whitesmoke;
33 | }
34 |
35 | .jumbotron {
36 | background-color: #007bff;
37 | color: white;
38 | }
39 |
40 | .jumbotron h1,
41 | .jumbotron p {
42 | text-align: center;
43 | }
44 |
45 | .form-control {
46 | background-color: #f8f9fa;
47 | border-radius: 0;
48 | }
49 |
50 | .form-control:focus {
51 | box-shadow: 0px 0px 10px #8c7a7a;
52 | }
53 |
54 | /* footer, make it always stick to bottom of the screen */
55 | footer {
56 | position: fixed;
57 | bottom: 0;
58 | width: 100%;
59 | height: 60px;
60 | background-color: #f5f5f5;
61 | }
62 | a:hover {
63 | color: blue;
64 | }
65 |
--------------------------------------------------------------------------------
/frontend/src/App.jsx:
--------------------------------------------------------------------------------
1 | import { BrowserRouter, Routes, Route } from 'react-router-dom';
2 | import MyNavbar from './components/Navbar';
3 | import LandingPage from './components/LandingPage/LandingPage';
4 | import About from './components/About';
5 | import Login from './components/Login';
6 | import Signup from './components/Signup';
7 | import Logout from './components/Logout';
8 | // import Footer from "./components/Footer";
9 | import History from './components/History';
10 | import './App.css';
11 | import UserContext from './context/UserContext';
12 | import Contributors from './components/Contributors';
13 | import React, { useState } from 'react';
14 | import Linkinbio from './components/Linkinbio';
15 | import ProfilePage from './components/ProfilePage';
16 | import ResetPassword from './components/ResetPassword';
17 | // import LandingPage from "./components/LandingPage/LandingPage";
18 | // for react-toastify
19 | import 'react-toastify/dist/ReactToastify.css';
20 | import { ToastContainer } from 'react-toastify';
21 | import SharePage from './components/Sharepage';
22 | import ForgotPassword from './components/ForgotPassword';
23 | import FeedbackForm from './components/FeedbackForm';
24 |
25 | function App() {
26 | const [user, setUser] = useState(localStorage.getItem('token') || null);
27 |
28 | return (
29 |
30 |
31 |
32 |
33 |
34 |
35 | } />
36 | } />
37 | } />
38 | } />
39 | } />
40 | } />
41 | } />
42 | } />
43 | } />{' '}
44 | {/* Use /linkinbio/* */}
45 | } />{' '}
46 | {/* Use /linkinbio/* */}
47 | } />
48 | } />
49 | } />
50 | } />
51 |
52 | {/* */}
53 |
54 |
55 |
56 | );
57 | }
58 |
59 | export default App;
60 |
--------------------------------------------------------------------------------
/frontend/src/assets/images/feedback.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/frontend/src/assets/images/feedback.png
--------------------------------------------------------------------------------
/frontend/src/components/About.jsx:
--------------------------------------------------------------------------------
1 | import Accordion from 'react-bootstrap/Accordion';
2 |
3 | import React from 'react';
4 |
5 | function About() {
6 | return (
7 |
8 |
9 |
About SnapURL
10 |
11 | SnapURL is an open-source URL shortener that makes converting long,
12 | cumbersome URLs into short, easy-to-share links simple. It provides
13 | user-friendly features as well as tracking capabilities to improve
14 | your web experience.
15 |
16 |
Why Use SnapURL?
17 |
18 | SnapURL is an open-source URL shortener that allows you to create,
19 | track, and share short links for free. It is a self-hosted alternative
20 | to paid URL shorteners such as Bitly and TinyURL. SnapURL is simple to
21 | use and has a clean, modern interface. It's built with React, Node,
22 | Express, and MongoDB.
23 |
24 |
Features
25 |
26 | SnapURL provides a variety of features to improve your web experience.
27 | These include:
28 |
29 |
30 | Custom URLs
31 | Link Password Protection
32 | Link Analytics
33 | QR Code Generation
34 | Link Deletion
35 | Export generated URL's to excel file
36 |
37 |
38 |
Frequently Asked Questions
39 |
40 |
41 |
42 | 1. What is SnapURL?
43 |
44 | SnapURL is an open-source URL shortener that makes converting
45 | long, cumbersome URLs into short, easy-to-share links simple. It
46 | provides user-friendly features as well as tracking capabilities
47 | to improve your web experience.
48 |
49 |
50 |
51 |
52 | 2. How do I get started with SnapURL?
53 |
54 |
55 | Simply create a SnapURL account to get started. After registering,
56 | you can begin shortening URLs and taking advantage of our
57 | services.
58 |
59 |
60 |
61 | 3. Is SnapURL free to use?
62 |
63 | SnapURL is, indeed, completely free to use. We do not charge any
64 | fees for our services.
65 |
66 |
67 |
68 |
69 | 4. Is my data secure with SnapURL?
70 |
71 |
72 | Yes, the security of your data is a top priority for us. For added
73 | security, we use strong password hashing with Bcrypt and provide
74 | email verification and password reset via email.
75 |
76 |
77 |
78 |
79 |
80 | );
81 | }
82 |
83 | export default About;
84 |
--------------------------------------------------------------------------------
/frontend/src/components/AxiosFetch.jsx:
--------------------------------------------------------------------------------
1 | // import { set } from "mongoose";
2 | import { useState, useEffect } from 'react';
3 | import axios from 'axios';
4 |
5 | const AxiosFetch = (url) => {
6 | const [data, setData] = useState(null);
7 | const [isPending, setIsPending] = useState(true);
8 | const [error, setError] = useState(null);
9 | // 'http://localhost:8000/blogs'
10 | useEffect(() => {
11 | const abortCont = new AbortController();
12 | axios
13 | .get(url)
14 | .then((data) => {
15 | // console.log("axios fetch",data.data);
16 | setData(data.data);
17 | setIsPending(false);
18 | setError(null);
19 | })
20 | .catch((err) => {
21 | if (err.name === 'AbortError') {
22 | console.log('fetch aborted');
23 | } else {
24 | setIsPending(false);
25 | setError(err.message);
26 | }
27 | });
28 | return () => abortCont.abort();
29 | }, [url]); //empty dependency array prevents the useEffect from running for every change
30 | // console.log('axios',data);
31 | return { data, isPending, error };
32 | };
33 |
34 | export default AxiosFetch;
35 |
--------------------------------------------------------------------------------
/frontend/src/components/Contributers.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
2 | .contributers {
3 | font-family: 'poppins';
4 | }
5 |
6 | .contributers-main {
7 | display: flex;
8 | align-items: center;
9 | padding: 0px 0px;
10 | width: 80vw;
11 | margin: auto;
12 | justify-content: space-around;
13 | }
14 | .contributers-main .contributers-animation {
15 | width: 100%;
16 | min-width: 300px;
17 | flex: 1;
18 | }
19 | .contributers .contributrers-main-text {
20 | flex: 1;
21 | }
22 | .contributers .contributrers-main-text h1 {
23 | text-align: left;
24 | font-size: 60px;
25 | font-weight: 800;
26 | }
27 | .contributers .contributrers-main-text h2 {
28 | font-size: 20px;
29 | font-weight: 500;
30 | }
31 | .contributers .contributrers-main-text h3 {
32 | font-size: 15px;
33 | color: grey;
34 | font-weight: 400;
35 | }
36 |
37 | .contributers .title {
38 | font-size: 50px;
39 | font-weight: 800;
40 | width: 90vw;
41 | margin: auto;
42 | display: flex;
43 | align-items: center;
44 | }
45 |
46 | .contributers .title .circle {
47 | font-size: 30px;
48 | margin-right: 20px;
49 | color: rgb(75, 63, 107);
50 | }
51 |
52 | .contributers .contributer-container {
53 | width: 90vw;
54 | margin: auto;
55 | margin-top: 40px;
56 | display: grid;
57 | grid-template-columns: auto auto auto auto;
58 | grid-auto-flow: row;
59 | gap: 20px;
60 | }
61 |
62 | .profile {
63 | display: flex;
64 | flex-direction: column;
65 | align-items: center;
66 | padding: 10px;
67 | border-radius: 20px;
68 | }
69 |
70 | .profile:hover {
71 | cursor: pointer;
72 | background-color: rgba(75, 63, 107, 0.235);
73 | }
74 |
75 | .profile img {
76 | height: auto;
77 | width: 10rem;
78 | border-radius: 50%;
79 | }
80 |
81 | .profile h1 {
82 | font-size: 1rem;
83 | margin-top: 20px;
84 | color: black;
85 | text-align: center;
86 | }
87 |
88 | .loading {
89 | width: 50px;
90 | height: 50px;
91 | margin: 20px auto;
92 | border: 4px solid #f3f3f3;
93 | border-top: 4px solid rgb(75, 63, 107);
94 | border-radius: 50%;
95 | animation: spin 1.5s linear infinite;
96 | }
97 |
98 | @keyframes spin {
99 | 0% {
100 | transform: rotate(0deg);
101 | }
102 |
103 | 100% {
104 | transform: rotate(360deg);
105 | }
106 | }
107 |
108 | @media screen and (max-width: 800px) {
109 | .contributers-main {
110 | /* background-color: #ff5722; */
111 | flex-direction: column-reverse;
112 | justify-content: center;
113 | width: 90vw;
114 | }
115 |
116 | .contributers-main .contributers-animation {
117 | min-width: 300px;
118 | width: 50%;
119 | /* background-color: red; */
120 | }
121 | .contributers-main .contributrers-main-text {
122 | padding-top: 40px;
123 | text-align: center;
124 | }
125 | .contributers-main .contributrers-main-text h1 {
126 | font-size: 30px;
127 | text-align: center;
128 | }
129 | .contributers .title {
130 | font-size: 25px;
131 | margin-right: 10px;
132 | }
133 | .contributers .title .circle {
134 | font-size: 20px;
135 | margin-right: 10px;
136 | color: rgb(75, 63, 107);
137 | }
138 | .contributers .contributer-container {
139 | /* background-color: red; */
140 | width: 100vw;
141 | max-width: 99vw;
142 | margin: auto;
143 | margin-top: 40px;
144 | display: grid;
145 | grid-template-columns: auto auto;
146 | grid-auto-flow: row;
147 | gap: 0px;
148 | overflow: hidden;
149 | padding: 0px 10px;
150 | }
151 | .profile img {
152 | width: 7rem;
153 | }
154 | .profile h1 {
155 | font-size: 0.8rem;
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/frontend/src/components/Contributers.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import AxiosFetch from './AxiosFetch';
3 | import Lottie from 'lottie-react';
4 | import './Contributers.css';
5 |
6 | import Circle from '@mui/icons-material/Circle';
7 | import cont1 from '../assets/cont1.json';
8 |
9 | function Contributers() {
10 | const { data, isPending, error } = AxiosFetch(
11 | 'https://api.github.com/repos/DhananjayThomble/URL-Shortener-App/contributors',
12 | );
13 | return (
14 |
15 |
16 |
21 |
22 |
Github Contributers,
23 |
24 | Collaboration in Code: Where Ideas Converge and Innovations Thrive
25 | 🚀
26 |
27 | #OpenSource #CodeUnity
28 |
29 |
30 |
31 |
32 |
33 | The Contributers
34 |
35 |
36 | {error &&
{error}
}
37 | {isPending &&
}
38 |
54 |
55 | );
56 | }
57 |
58 | export default Contributers;
59 |
--------------------------------------------------------------------------------
/frontend/src/components/Contributors.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
2 | .contributors {
3 | font-family: 'poppins';
4 | }
5 |
6 | .contributors-main {
7 | display: flex;
8 | align-items: center;
9 | padding: 0px 0px;
10 | width: 80vw;
11 | margin: auto;
12 | justify-content: space-around;
13 | }
14 | .contributors-main .contributors-animation {
15 | width: 100%;
16 | min-width: 300px;
17 | flex: 1;
18 | }
19 | .contributors .contributrers-main-text {
20 | flex: 1;
21 | }
22 | .contributors .contributrers-main-text h1 {
23 | text-align: left;
24 | font-size: 60px;
25 | font-weight: 800;
26 | }
27 | .contributors .contributrers-main-text h2 {
28 | font-size: 20px;
29 | font-weight: 500;
30 | }
31 | .contributors .contributrers-main-text h3 {
32 | font-size: 15px;
33 | color: grey;
34 | font-weight: 400;
35 | }
36 |
37 | .contributors .title {
38 | font-size: 50px;
39 | font-weight: 800;
40 | width: 90vw;
41 | margin: auto;
42 | display: flex;
43 | align-items: center;
44 | }
45 |
46 | .contributors .title .circle {
47 | font-size: 30px;
48 | margin-right: 20px;
49 | color: rgb(75, 63, 107);
50 | }
51 |
52 | .contributors .contributer-container {
53 | width: 90vw;
54 | margin: auto;
55 | margin-top: 40px;
56 | display: grid;
57 | grid-template-columns: auto auto auto auto;
58 | grid-auto-flow: row;
59 | gap: 20px;
60 | }
61 |
62 | .profile {
63 | display: flex;
64 | flex-direction: column;
65 | align-items: center;
66 | padding: 10px;
67 | border-radius: 20px;
68 | }
69 |
70 | .profile:hover {
71 | cursor: pointer;
72 | background-color: rgba(75, 63, 107, 0.235);
73 | }
74 |
75 | .profile img {
76 | height: auto;
77 | width: 10rem;
78 | border-radius: 50%;
79 | }
80 |
81 | .profile h1 {
82 | font-size: 1rem;
83 | margin-top: 20px;
84 | color: black;
85 | text-align: center;
86 | }
87 |
88 | .loading {
89 | width: 50px;
90 | height: 50px;
91 | margin: 20px auto;
92 | border: 4px solid #f3f3f3;
93 | border-top: 4px solid rgb(75, 63, 107);
94 | border-radius: 50%;
95 | animation: spin 1.5s linear infinite;
96 | }
97 |
98 | @keyframes spin {
99 | 0% {
100 | transform: rotate(0deg);
101 | }
102 |
103 | 100% {
104 | transform: rotate(360deg);
105 | }
106 | }
107 |
108 | @media screen and (max-width: 800px) {
109 | .contributors-main {
110 | /* background-color: #ff5722; */
111 | flex-direction: column-reverse;
112 | justify-content: center;
113 | width: 90vw;
114 | }
115 |
116 | .contributors-main .contributors-animation {
117 | min-width: 300px;
118 | width: 50%;
119 | /* background-color: red; */
120 | }
121 | .contributors-main .contributrers-main-text {
122 | padding-top: 40px;
123 | text-align: center;
124 | }
125 | .contributors-main .contributrers-main-text h1 {
126 | font-size: 30px;
127 | text-align: center;
128 | }
129 | .contributors .title {
130 | font-size: 25px;
131 | margin-right: 10px;
132 | }
133 | .contributors .title .circle {
134 | font-size: 20px;
135 | margin-right: 10px;
136 | color: rgb(75, 63, 107);
137 | }
138 | .contributors .contributer-container {
139 | /* background-color: red; */
140 | width: 100vw;
141 | max-width: 99vw;
142 | margin: auto;
143 | margin-top: 40px;
144 | display: grid;
145 | grid-template-columns: auto auto;
146 | grid-auto-flow: row;
147 | gap: 0px;
148 | overflow: hidden;
149 | padding: 0px 10px;
150 | }
151 | .profile img {
152 | width: 7rem;
153 | }
154 | .profile h1 {
155 | font-size: 0.8rem;
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/frontend/src/components/Contributors.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import AxiosFetch from './AxiosFetch';
3 | import Lottie from 'lottie-react';
4 | import './Contributors.css';
5 |
6 | import Circle from '@mui/icons-material/Circle';
7 | import cont1 from '../assets/cont1.json';
8 |
9 | function Contributors() {
10 | const { data, isPending, error } = AxiosFetch(
11 | 'https://api.github.com/repos/DhananjayThomble/URL-Shortener-App/contributors',
12 | );
13 | return (
14 |
15 |
16 |
21 |
22 |
Github Contributors,
23 |
24 | Collaboration in Code: Where Ideas Converge and Innovations Thrive
25 | 🚀
26 |
27 | #OpenSource #CodeUnity
28 |
29 |
30 |
31 |
32 |
33 | The Contributors
34 |
35 |
36 | {error &&
{error}
}
37 | {isPending &&
}
38 |
54 |
55 | );
56 | }
57 |
58 | export default Contributors;
59 |
--------------------------------------------------------------------------------
/frontend/src/components/ExportToExcel.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Button, Col, Container } from 'react-bootstrap';
3 | import axios from 'axios';
4 | import { saveAs } from 'file-saver';
5 | import { toast } from 'react-toastify';
6 |
7 | const ExportToExcel = () => {
8 | const [isLoading, setIsLoading] = useState(false);
9 |
10 | const handleExport = async () => {
11 | setIsLoading(true);
12 | toast.info('Exporting to Excel...');
13 | try {
14 | // Make a request to the backend to fetch the Excel file
15 | const URL = `${import.meta.env.VITE_API_ENDPOINT}/api/export`;
16 | const response = await axios.get(URL, {
17 | responseType: 'blob',
18 | headers: { Authorization: `Bearer ${localStorage.getItem('token')}` },
19 | });
20 |
21 | // Save the Excel file using FileSaver.js
22 | saveAs(response.data, 'Generated_URLs.xlsx');
23 | toast.success('Exported to Excel');
24 | setIsLoading(false);
25 | } catch (error) {
26 | console.error(error);
27 | toast.error('Error exporting to Excel');
28 | }
29 | };
30 |
31 | return (
32 |
33 |
34 |
35 |
41 | {isLoading ? 'Exporting...' : 'Export to Excel'}
42 |
43 |
44 |
45 |
46 | );
47 | };
48 |
49 | export default ExportToExcel;
50 |
--------------------------------------------------------------------------------
/frontend/src/components/FeedbackForm.css:
--------------------------------------------------------------------------------
1 | .feedback__form__container {
2 | display: flex;
3 | flex-direction: column;
4 | align-items: center;
5 | }
6 |
7 | .feedback__form {
8 | display: flex;
9 | flex-direction: row;
10 | justify-content: space-between;
11 | align-items: center;
12 | padding: 25px;
13 | margin: 30px 0px;
14 | background-color: rgb(230, 230, 250);
15 | border-radius: 25px;
16 | width: 80%;
17 | box-shadow: rgba(50, 50, 93, 0.25) 0px 50px 100px -20px,
18 | rgba(0, 0, 0, 0.3) 0px 30px 60px -30px,
19 | rgba(10, 37, 64, 0.35) 0px -2px 6px 0px inset;
20 | }
21 |
22 | .feedback__form__img {
23 | width: 40%;
24 | }
25 |
26 | .feedback__img {
27 | width: 90%;
28 | }
29 |
30 | .feedback__form__details {
31 | width: 60%;
32 | padding: 0px 10px;
33 | }
34 |
35 | .feedback__form__details * {
36 | margin: 5px;
37 | }
38 |
39 | .feedback__form__details input {
40 | background: 0;
41 | border: 0;
42 | outline: none;
43 | font-size: 18px;
44 | transition: padding 0.3s 0.2s ease;
45 | }
46 |
47 | .feedback__form__details input:focus {
48 | padding-bottom: 5px;
49 | }
50 |
51 | .feedback__form__details input:focus + .line:after {
52 | transform: scaleX(1);
53 | }
54 |
55 | .feedback__form__details .field {
56 | position: relative;
57 | }
58 |
59 | .feedback__form__details .line {
60 | width: 100%;
61 | height: 3px;
62 | position: absolute;
63 | bottom: -8px;
64 | background: #bdc3c7;
65 | }
66 |
67 | .feedback__form__details .line:after {
68 | content: ' ';
69 | position: absolute;
70 | float: right;
71 | width: 100%;
72 | height: 3px;
73 | transform: scalex(0);
74 | transition: transform 0.3s ease;
75 | background: rgb(75, 63, 107);
76 | }
77 |
78 | .feedback__form__details textarea {
79 | padding: 10px;
80 | border-radius: 10px;
81 | height: 300px;
82 | width: 100%;
83 | resize: none;
84 | overflow: auto;
85 | border: none;
86 | outline: none;
87 | }
88 |
89 | .error__message {
90 | color: red;
91 | font-weight: bold;
92 | margin: 0;
93 | padding: 0;
94 | }
95 |
96 | .feedback__form__details textarea:focus {
97 | border: 3px solid rgb(220, 189, 221);
98 | box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px;
99 | }
100 |
101 | .form__submit__btn {
102 | text-align: center;
103 | }
104 |
105 | .feedback__submit__btn {
106 | background-color: #ffffff;
107 | border-radius: 4px;
108 | box-shadow: rgba(45, 35, 66, 0.4) 0 2px 4px,
109 | rgba(45, 35, 66, 0.3) 0 7px 13px -3px, #d6d6e7 0 -3px 0 inset;
110 | color: #36395a;
111 | cursor: pointer;
112 | height: 48px;
113 | padding: 0px 16px;
114 | transition: box-shadow 0.15s, transform 0.15s;
115 | font-size: 18px;
116 | border: none;
117 | }
118 |
119 | .feedback__submit__btn:focus {
120 | box-shadow: #d6d6e7 0 0 0 1.5px inset, rgba(45, 35, 66, 0.4) 0 2px 4px,
121 | rgba(45, 35, 66, 0.3) 0 7px 13px -3px, #d6d6e7 0 -3px 0 inset;
122 | }
123 |
124 | .feedback__submit__btn:hover {
125 | box-shadow: rgba(45, 35, 66, 0.4) 0 4px 8px,
126 | rgba(45, 35, 66, 0.3) 0 7px 13px -3px, #d6d6e7 0 -3px 0 inset;
127 | transform: translateY(-2px);
128 | }
129 |
130 | .feedback__submit__btn:active {
131 | box-shadow: #d6d6e7 0 3px 7px inset;
132 | transform: translateY(2px);
133 | }
134 |
135 | @media screen and (max-width: 768px) {
136 | .feedback__form {
137 | flex-direction: column;
138 | }
139 | .feedback__form__img,
140 | .feedback__form__details {
141 | width: 100%;
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/frontend/src/components/FeedbackForm.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import './FeedbackForm.css';
3 | import Image from '../assets/images/feedback.png';
4 | import { toast } from 'react-toastify';
5 | import axios from 'axios';
6 |
7 | const FeedbackForm = () => {
8 | const [name, setName] = useState('');
9 | const [email, setEmail] = useState('');
10 | const [attachment, setAttachment] = useState(null);
11 | // TODO: Implement attachments later
12 | const [feedback, setFeedback] = useState('');
13 | const [nameError, setNameError] = useState('');
14 | const [emailError, setEmailError] = useState('');
15 | const [feedbackError, setFeedbackError] = useState('');
16 | const [fileInputKey, setFileInputKey] = useState(0);
17 |
18 | const handleAttachmentChange = (event) => {
19 | setAttachment(event.target.files[0]);
20 | };
21 |
22 | const resetState = () => {
23 | setName('');
24 | setEmail('');
25 | setFeedback('');
26 | setAttachment(null);
27 | setFileInputKey((prevKey) => prevKey + 1);
28 | };
29 |
30 | const isNameValid = (input) => {
31 | const namePattern = /^[a-zA-Z\s-]*$/;
32 | return namePattern.test(input);
33 | };
34 |
35 | const isEmailValid = (input) => {
36 | const emailPattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/;
37 | return emailPattern.test(input);
38 | };
39 |
40 | const handleSubmit = async () => {
41 | let isFormValid = true;
42 | setEmailError('');
43 | setNameError('');
44 | setFeedbackError('');
45 |
46 | if (!isNameValid(name) || !name) {
47 | setNameError('Please enter a valid name.');
48 | isFormValid = false;
49 | }
50 |
51 | if (!isEmailValid(email) || !email) {
52 | setEmailError('Please enter a valid email address.');
53 | isFormValid = false;
54 | }
55 |
56 | if (!feedback) {
57 | setFeedbackError('Please enter your feedback');
58 | isFormValid = false;
59 | }
60 |
61 | if (isFormValid) {
62 | // Submit form details to the API
63 | const apiEndpoint = `${import.meta.env.VITE_API_ENDPOINT}/auth/feedback`;
64 |
65 | try {
66 | const response = await axios.post(
67 | apiEndpoint,
68 | {
69 | message: feedback,
70 | rating: 5,
71 | },
72 | {
73 | headers: {
74 | Authorization: `Bearer ${localStorage.getItem('token')}`,
75 | },
76 | },
77 | );
78 | console.log(response.status);
79 |
80 | toast.success('Submitted');
81 | resetState();
82 | } catch (error) {
83 | if (error.response.status === 400) {
84 | const { error: errorMsg } = error.response.data;
85 | console.log(error, errorMsg);
86 |
87 | toast.error(errorMsg);
88 | } else if (
89 | error.response.status === 404 ||
90 | error.response.status === 401
91 | ) {
92 | toast.error('Please log in or sign up first!');
93 | } else {
94 | toast.error('Something went wrong. Please try again later.');
95 | }
96 | }
97 | }
98 | };
99 |
100 | return (
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
setName(e.target.value)}
113 | />
114 |
115 |
116 | {nameError &&
{nameError}
}
117 |
118 |
setEmail(e.target.value)}
123 | />
124 |
125 |
126 | {emailError &&
{emailError}
}
127 |
128 |
handleAttachmentChange(e)}
133 | />
134 |
135 |
136 |
137 | Feel free to report issues, give feedback, or contribute your
138 | suggestions.
139 |
140 |
153 |
154 |
155 | );
156 | };
157 |
158 | export default FeedbackForm;
159 |
--------------------------------------------------------------------------------
/frontend/src/components/Footer.css:
--------------------------------------------------------------------------------
1 | .footer {
2 | position: absolute;
3 | bottom: 0px;
4 | max-width: 100% !important;
5 | height: 13rem;
6 | display: flex !important;
7 | flex-wrap: wrap;
8 | justify-content: center;
9 | align-items: center;
10 | background-color: rgb(209, 210, 213);
11 | color: rgb(11, 1, 1);
12 | /* box-shadow: rgba(14, 95, 157, 0.3) 0px 1px 2px 0px, rgba(60, 64, 67, 0.15) 0px 1px 3px 1px; */
13 | box-shadow: -1px -0px 10px rgb(108, 117, 128);
14 | margin-top: 2rem;
15 | border-bottom: 50px solid rgb(55, 103, 165);
16 | }
17 |
18 | .mainfooter {
19 | width: 70%;
20 | display: flex;
21 | flex-wrap: wrap;
22 | justify-content: space-around;
23 | align-items: center;
24 | gap: 2rem;
25 | font-weight: 600;
26 | }
27 |
28 | .othertext {
29 | display: flex;
30 | gap: 15px;
31 | }
32 | .Heading {
33 | display: flex;
34 | position: relative;
35 | margin-bottom: 15px;
36 | margin-left: 0px;
37 | }
38 | .Icon {
39 | font-size: 35px !important;
40 | position: absolute;
41 | }
42 | .right {
43 | display: flex;
44 | margin-bottom: 44px;
45 | flex-direction: column;
46 | justify-content: center;
47 | margin-right: -20rem;
48 | }
49 |
50 | .Social {
51 | font-size: 50px !important;
52 | margin-left: 95px;
53 | }
54 |
55 | .left {
56 | margin-left: -20rem;
57 | }
58 | a {
59 | text-decoration: none;
60 | }
61 |
62 | span:hover {
63 | color: blue !important;
64 | font-weight: 500;
65 | text-decoration: underline;
66 | }
67 |
68 | @media only screen and (max-width: 600px) {
69 | .right {
70 | margin-right: 0px;
71 | }
72 | .left {
73 | margin-left: 0px;
74 | }
75 | .mainfooter {
76 | width: 100%;
77 |
78 | padding: 1rem;
79 | gap: 0rem;
80 | }
81 | .Social {
82 | margin-left: 40px;
83 | }
84 |
85 | .Heading {
86 | margin-left: 30px;
87 | }
88 | .footer {
89 | height: 17rem;
90 | }
91 | }
92 |
93 | /* Footer Container */
94 | /*.footer {
95 | position: absolute;
96 | bottom: 0px;
97 | max-width: 100% !important;
98 | height: auto;
99 | display: flex !important;
100 | flex-wrap: wrap;
101 | justify-content: center;
102 | align-items: center;
103 | background-color: rgb(209, 210, 213);
104 | color: rgb(11, 1, 1);
105 | box-shadow: -1px -0px 10px rgb(108, 117, 128);
106 | margin-top: 2rem;
107 | border-bottom: 50px solid rgb(55, 103, 165);
108 | padding: 20px;
109 | }
110 |
111 |
112 | /* .mainfooter {
113 | width: 100%;
114 | display: flex;
115 | flex-wrap: wrap;
116 | justify-content: space-around;
117 | align-items: center;
118 | gap: 2rem;
119 | font-weight: 600;
120 | } */
121 |
122 | /* Align the 'SnapURL' text with proper spacing */
123 | /*.Heading {
124 | display: flex;
125 | align-items: center;
126 | margin-bottom: 15px;
127 | gap: 10px;
128 | margin-left: 235px;
129 | }
130 |
131 |
132 | .Icon {
133 | font-size: 35px !important;
134 | position: absolute;
135 | margin-left: -10px;
136 | }
137 |
138 | /* Right and Left Columns */
139 | /*.right,
140 | .left {
141 | flex: 1;
142 | text-align: center;
143 | margin: 10px;
144 |
145 | }
146 |
147 | /* Social Media Icons */
148 | /* .Social {
149 | font-size: 20px !important;
150 | margin-right: -70px;
151 |
152 | }
153 | .right{
154 | margin-right: 20px;
155 | margin-top: 2px;
156 | }
157 |
158 | /* Text in Both Columns */
159 | /*.othertext {
160 | display: flex;
161 | flex-wrap: wrap;
162 | gap: 15px;
163 | justify-content: center;
164 | font-size: 14px;
165 | margin-bottom: 8px;
166 | }*/
167 |
168 | /* @media only screen and (max-width: 600px) {
169 | .footer {
170 | flex-direction: row;
171 | }
172 | .mainfooter {
173 | width: 90%;
174 | text-align: center;
175 | }
176 | .Heading {
177 | flex-direction: row;
178 | text-align:left;
179 | }
180 | .left,
181 | .right {
182 | margin: 0;
183 | }
184 | .othertext {
185 | flex-direction:row;
186 | text-align: center;
187 | }
188 | }*/
189 |
--------------------------------------------------------------------------------
/frontend/src/components/Footer.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './Footer.css';
3 | import { Container, Typography, IconButton } from '@mui/material';
4 | import { Facebook, Twitter, Instagram } from '@mui/icons-material';
5 | import InsertLinkIcon from '@mui/icons-material/InsertLink';
6 |
7 | function Footer() {
8 | return (
9 |
10 |
11 |
12 |
13 |
17 |
22 | SnapURL
23 |
24 |
25 |
26 | ©2021
27 |
28 | Company
29 |
30 | Terms
31 |
32 | Privacy
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | About
49 |
50 | FQA
51 |
52 | Contact
53 |
54 | Blog
55 |
56 |
57 |
58 |
59 | );
60 | }
61 |
62 | export default Footer;
63 |
--------------------------------------------------------------------------------
/frontend/src/components/ForgotPassword.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import {
3 | Container,
4 | Card,
5 | Typography,
6 | TextField,
7 | Button,
8 | Box,
9 | } from '@mui/material';
10 |
11 | const ForgotPassword = () => {
12 | const [email, setEmail] = useState('');
13 | const [message, setMessage] = useState('');
14 | const [isLoading, setIsLoading] = useState(false);
15 |
16 | const handleSendMail = async () => {
17 | try {
18 | if (!email) {
19 | setMessage('Please enter your email.');
20 | return;
21 | }
22 |
23 | // Make a POST request to your backend API to reset the password
24 | setIsLoading(true);
25 | const response = await fetch(
26 | `${import.meta.env.VITE_API_ENDPOINT}/auth/forgot-password`,
27 | {
28 | method: 'POST',
29 | headers: {
30 | 'Content-Type': 'application/json',
31 | },
32 | body: JSON.stringify({ email }),
33 | },
34 | );
35 |
36 | if (response.status === 200) {
37 | setMessage('Email sent successfully. Please check your inbox.');
38 | } else {
39 | // Handle error response from the backend
40 |
41 | const data = await response.json();
42 |
43 | setMessage(data.error || 'Email sent failed.');
44 | }
45 | } catch (error) {
46 | console.error('Error sending mail:', error);
47 | setMessage('mail send failed.');
48 | } finally {
49 | setIsLoading(false); // re-enable the button irrespective of success/error
50 | }
51 | };
52 |
53 | return (
54 |
64 |
65 |
66 |
67 |
68 | Forgot Password
69 |
70 |
92 |
93 | {message}
94 |
95 |
96 |
97 |
98 |
99 | );
100 | };
101 |
102 | export default ForgotPassword;
103 |
--------------------------------------------------------------------------------
/frontend/src/components/HistoryCard.jsx:
--------------------------------------------------------------------------------
1 | import Button from 'react-bootstrap/Button';
2 | import Card from 'react-bootstrap/Card';
3 | import React, { useState, useEffect } from 'react';
4 | import axios from 'axios';
5 | import { toast } from 'react-toastify';
6 | import { FaRegChartBar, FaExternalLinkAlt, FaTrashAlt } from 'react-icons/fa';
7 | import { Dropdown } from 'react-bootstrap';
8 | import PropTypes from 'prop-types';
9 |
10 | function HistoryCard({
11 | shortUrl,
12 | originalUrl,
13 | visitCount,
14 | category,
15 | categoryArray,
16 | }) {
17 | const [urlId, setUrlId] = useState('');
18 | const [showCard, setShowCard] = useState(true);
19 | const [visitCountState, setVisitCountState] = useState(visitCount);
20 | const [categoryState, setCategoryState] = useState(category);
21 |
22 | useEffect(() => {
23 | setUrlId(shortUrl);
24 | }, [shortUrl]);
25 |
26 | const deleteUrl = async () => {
27 | try {
28 | const result = await axios.delete(
29 | `${import.meta.env.VITE_API_ENDPOINT}/api/delete/${urlId}`,
30 | {
31 | headers: { Authorization: `Bearer ${localStorage.getItem('token')}` },
32 | },
33 | );
34 | // console.log(result);
35 | if (result.status === 200) {
36 | toast.success('URL Deleted Successfully');
37 | setShowCard(false);
38 | }
39 | } catch (error) {
40 | //
41 | console.error(error);
42 | toast.error(error.data.response.error);
43 | }
44 | };
45 |
46 | // extracting domain name from url
47 | const getDomainName = (url) => {
48 | const domainName = url.replace(/(^\w+:|^)\/\//, '').split('/')[0];
49 | return domainName;
50 | };
51 |
52 | const updateCategory = async (e) => {
53 | try {
54 | console.log(`update category api is called`);
55 | const result = await axios.put(
56 | `${import.meta.env.VITE_API_ENDPOINT}/api/filter/`,
57 | {
58 | shortUrl: shortUrl,
59 | category: e.target.innerText,
60 | },
61 | {
62 | headers: { Authorization: `Bearer ${localStorage.getItem('token')}` },
63 | },
64 | );
65 | // console.log(result);
66 | if (result.status === 200) {
67 | setCategoryState(e.target.innerText);
68 | toast.success('Category Updated Successfully');
69 | }
70 | } catch (error) {
71 | console.error(error);
72 | toast.error(error.data.response.error);
73 | }
74 | };
75 |
76 | if (showCard) {
77 | return (
78 |
79 |
80 | {getDomainName(originalUrl)}
81 |
82 |
83 |
89 |
90 |
91 |
92 |
93 |
94 | {`${
95 | import.meta.env.VITE_API_ENDPOINT
96 | }/u/${shortUrl}`}
97 | {originalUrl}
98 |
99 |
100 |
101 |
102 | {categoryState || 'Select Category'}
103 |
104 |
105 | {categoryArray.map((item, index) => {
106 | return (
107 |
108 | {item}
109 |
110 | );
111 | })}
112 |
113 |
114 |
115 |
{
118 | // open in new tab
119 | window.open(
120 | `${import.meta.env.VITE_API_ENDPOINT}/u/${shortUrl}`,
121 | '_blank',
122 | );
123 | // increase visit count
124 | setVisitCountState(visitCountState + 1);
125 | }}
126 | >
127 | Visit The Site
128 |
129 |
130 |
131 | {visitCountState}
132 |
133 |
134 |
135 | );
136 | } else {
137 | return null;
138 | }
139 | }
140 |
141 | HistoryCard.propTypes = {
142 | shortUrl: PropTypes.string.isRequired,
143 | originalUrl: PropTypes.string.isRequired,
144 | visitCount: PropTypes.number.isRequired,
145 | category: PropTypes.string,
146 | categoryArray: PropTypes.array,
147 | };
148 |
149 | export default HistoryCard;
150 |
--------------------------------------------------------------------------------
/frontend/src/components/Home.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { Container, Row, Col, Form, Button } from 'react-bootstrap';
3 | import { toast } from 'react-toastify';
4 | import axios from 'axios';
5 | import { FaLink } from 'react-icons/fa';
6 |
7 | function Home() {
8 | const [originalUrl, setOriginalUrl] = useState('');
9 | const [shortUrl, setShortUrl] = useState('');
10 | const [name, setName] = useState(localStorage.getItem('name') || 'Guest');
11 | const [isAuthenticated, setIsAuthenticated] = useState(false);
12 | const URL = `${import.meta.env.VITE_API_ENDPOINT}/api/url`;
13 |
14 | useEffect(() => {
15 | const token = localStorage.getItem('token');
16 | setName(localStorage.getItem('name') || 'Guest');
17 | if (!token) {
18 | setIsAuthenticated(false);
19 | toast.warning('Please Login to use the App');
20 | } else {
21 | setIsAuthenticated(true);
22 | }
23 | }, []);
24 |
25 | const handleSubmit = async (e) => {
26 | e.preventDefault();
27 | if (!validateForm()) return false;
28 | // show toast
29 | if (isAuthenticated) toast('URL will be shortened soon!');
30 |
31 | await fetchUrl();
32 | };
33 |
34 | function validateForm() {
35 | if (originalUrl === '') {
36 | toast.error('All fields are required');
37 | return false;
38 | }
39 | return true;
40 | }
41 |
42 | const fetchUrl = async () => {
43 | try {
44 | const response = await axios.post(
45 | URL,
46 | {
47 | url: originalUrl,
48 | },
49 | {
50 | headers: {
51 | Authorization: `Bearer ${localStorage.getItem('token')}`,
52 | },
53 | },
54 | );
55 | const { shortUrl } = response.data;
56 | setShortUrl(shortUrl);
57 | toast.success('URL shortened successfully!');
58 | } catch (error) {
59 | if (error.response) {
60 | const { status, data } = error.response;
61 | if (status === 400) {
62 | toast.error(data.error);
63 | } else if (status === 401) {
64 | toast.error('Heyy, you have to login first!');
65 | } else if (status === 500) {
66 | toast.error(data.error);
67 | } else {
68 | toast.error('Something went wrong');
69 | }
70 | } else {
71 | toast.error('Network error. Please try again.');
72 | }
73 | }
74 | };
75 | return (
76 | <>
77 |
78 |
79 | Welcome {name}
80 |
81 | A simple and easy to use tool to shorten your long links and make
82 | them easy to share.
83 |
84 |
85 |
86 |
87 |
88 |
89 |
91 | Enter URL
92 |
93 | setOriginalUrl(e.target.value)}
99 | />
100 |
101 |
102 | {shortUrl && (
103 |
104 | Shortened URL
105 |
106 |
107 | )}
108 |
109 | {/* make this button in center of the form input and make width 50%*/}
110 |
116 | Shorten
117 |
118 |
119 |
120 |
121 |
122 | >
123 | );
124 | }
125 |
126 | export default Home;
127 |
--------------------------------------------------------------------------------
/frontend/src/components/LandingPage/Hero.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Typography from '@mui/material/Typography';
3 | import Button from '@mui/material/Button';
4 | import Box from '@mui/material/Box';
5 | import Grid from '@mui/material/Grid';
6 | import Stack from '@mui/material/Stack';
7 | import HeroImage from './Images/working_img.svg';
8 |
9 | const Hero = () => {
10 | return (
11 |
12 | {/* mobile screen */}
13 |
14 |
15 |
23 |
24 |
25 |
26 |
38 | Shorten, Share, Succeed
39 |
40 |
49 | Welcome to SnapURL Simplify Your Links, Maximize Your
50 | Impact
51 |
52 |
53 |
63 | Get Started
64 |
65 |
66 |
67 |
68 | {/* desktop screen */}
69 |
80 |
81 |
82 |
93 | Shorten, Share, Succeed
94 |
95 |
102 | Welcome to SnapURL Simplify Your Links, Maximize Your
103 | Impact
104 |
105 |
106 |
117 | Get Started
118 |
119 |
120 |
121 |
122 |
130 |
131 |
132 |
133 | );
134 | };
135 |
136 | export default Hero;
137 |
--------------------------------------------------------------------------------
/frontend/src/components/LandingPage/HomePage.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Box, Container } from '@mui/material';
3 | import Hero from './Hero';
4 | import LinkParent from './form/LinkParent';
5 |
6 | const HomePage = () => {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | );
17 | };
18 |
19 | export default HomePage;
20 |
--------------------------------------------------------------------------------
/frontend/src/components/LandingPage/Images/ShortenDesktop.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/components/LandingPage/Images/ShortenMobile.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/components/LandingPage/Images/desktop.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/components/LandingPage/Images/facebook.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/components/LandingPage/Images/img.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/components/LandingPage/Images/instagram.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/components/LandingPage/Images/mobile.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/components/LandingPage/Images/pinterest.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/components/LandingPage/Images/twitter.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/components/LandingPage/LandingPage.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | height: 40vmin;
7 | pointer-events: none;
8 | }
9 |
10 | @media (prefers-reduced-motion: no-preference) {
11 | .App-logo {
12 | animation: App-logo-spin infinite 20s linear;
13 | }
14 | }
15 |
16 | .App-header {
17 | background-color: #282c34;
18 | min-height: 100vh;
19 | display: flex;
20 | flex-direction: column;
21 | align-items: center;
22 | justify-content: center;
23 | font-size: calc(10px + 2vmin);
24 | color: white;
25 | }
26 |
27 | .App-link {
28 | color: #61dafb;
29 | }
30 |
31 | @keyframes App-logo-spin {
32 | from {
33 | transform: rotate(0deg);
34 | }
35 | to {
36 | transform: rotate(360deg);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/frontend/src/components/LandingPage/LandingPage.jsx:
--------------------------------------------------------------------------------
1 | import './LandingPage.css';
2 | import { createTheme, ThemeProvider } from '@mui/material';
3 | import HomePage from './HomePage';
4 | import appTheme from './appTheme';
5 | import FooterSegment from './footer_segment/FooterSegment';
6 | import React from 'react';
7 |
8 | const theme = createTheme(appTheme);
9 |
10 | function LandingPage() {
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
18 | );
19 | }
20 |
21 | export default LandingPage;
22 |
--------------------------------------------------------------------------------
/frontend/src/components/LandingPage/appTheme.js:
--------------------------------------------------------------------------------
1 | // STUB: create appTheme
2 | // appTheme contains custom palette, typography, components config
3 |
4 | const appTheme = {
5 | palette: {
6 | primary: {
7 | main: 'hsl(180, 66%, 49%)', // cyan
8 | },
9 | secondary: {
10 | main: '#fff', // white
11 | contrastText: 'hsl(257, 27%, 26%)', // dark violet
12 | },
13 | violetBg: {
14 | main: 'hsl(257, 27%, 26%)', // dark violet
15 | contrastText: '#fff', // white
16 | },
17 | neutral: {
18 | lightGray: 'hsla(0, 0%, 35%, 0.7)',
19 | gray: 'hsl(0, 0%, 75%)',
20 | grayishViolet: 'hsl(257, 7%, 63%)',
21 | veryDarkBlue: 'hsl(255, 11%, 22%)',
22 | veryDarkViolet: 'hsl(260, 8%, 14%)',
23 | },
24 | },
25 | typography: {
26 | fontFamily: "'Poppins', sans-serif",
27 | h1: {
28 | fontWeight: 800,
29 | },
30 | button: {
31 | fontSize: '0.9rem',
32 | textTransform: 'capitalize',
33 | },
34 | body1: {
35 | // fontSize: "1.45rem",
36 | fontSize: window.innerWidth <= 768 ? '1rem' : '1.45rem',
37 | },
38 | },
39 | components: {
40 | // Name of the component ⚛️
41 | // MuiButtonBase: {
42 | // defaultProps: {
43 | // // The props to apply
44 | // disableRipple: true, // No more ripple, on the whole application 💣!
45 | // },
46 | // },
47 | MuiButton: {
48 | variants: [
49 | {
50 | props: { variant: 'cyanBg' },
51 | style: {
52 | backgroundColor: 'hsl(180, 66%, 49%)',
53 | color: '#fff',
54 | '&:hover': {
55 | backgroundColor: 'hsla(180, 66%, 49%, 0.65)',
56 | },
57 | '&:focus': {
58 | backgroundColor: 'hsl(257, 27%, 26%)',
59 | },
60 | },
61 | },
62 | {
63 | props: { variant: 'violetText' },
64 | style: {
65 | backgroundColor: '#fff',
66 | color: 'hsl(257, 27%, 26%)',
67 | },
68 | },
69 | {
70 | props: { variant: 'text' },
71 | style: {
72 | color: '#fff',
73 | '&:hover': {
74 | color: 'hsl(260, 8%, 14%)',
75 | fontWeight: 800,
76 | backgroundColor: 'transparent',
77 | },
78 | },
79 | },
80 | ],
81 | defaultProps: {
82 | disableElevation: true,
83 | disableFocusRipple: true,
84 | disableRipple: true,
85 | },
86 | },
87 | MuiPaper: {
88 | styleOverrides: {
89 | // Name of the slot
90 | root: {
91 | // Some CSS
92 | backgroundImage: 'none', // remove box-shadow in dark-mode
93 | // backgroundColor: '#121212'
94 | },
95 | },
96 | },
97 | MuiListItemText: {
98 | styleOverrides: {
99 | primary: {
100 | fontSize: '0.9rem',
101 | },
102 | },
103 | },
104 | },
105 | };
106 |
107 | export default appTheme;
108 |
--------------------------------------------------------------------------------
/frontend/src/components/LandingPage/footer_segment/BoostBox.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Box, Button, Typography } from '@mui/material';
3 | import mobileBackgroundBoost from '../Images/ShortenMobile.svg';
4 | import desktopBackgroundBoost from '../Images/desktop.svg';
5 |
6 | const boxStyle = {
7 | backgroundImage: {
8 | xs: `url(${mobileBackgroundBoost})`,
9 | sm: `url(${desktopBackgroundBoost})`,
10 | },
11 | backgroundSize: 'cover',
12 | backgroundRepeat: 'no-repeat',
13 | backgroundPosition: {
14 | xs: 'center ',
15 | sm: 'bottom',
16 | },
17 | bgcolor: 'violetBg.main',
18 | borderRadius: 0,
19 | maxWidth: '100%',
20 | px: 6,
21 | py: {
22 | xs: 6,
23 | md: 8,
24 | },
25 | textAlign: 'center',
26 | };
27 |
28 | const textStyle = {
29 | color: 'secondary.main',
30 | fontWeight: 900,
31 | };
32 |
33 | const buttonStyle = {
34 | borderRadius: 8,
35 | px: 1.3,
36 | py: { xs: 1, md: 2 },
37 | fontSize: '1.15rem',
38 | fontWeight: 600,
39 | mt: { xs: 1.5, md: 2 },
40 | width: 'clamp(10em, 20%, 11em )',
41 | };
42 |
43 | const BoostBox = () => {
44 | return (
45 |
46 |
47 | Boost your links today
48 |
49 |
50 | Get Started
51 |
52 |
53 | );
54 | };
55 |
56 | export default BoostBox;
57 |
--------------------------------------------------------------------------------
/frontend/src/components/LandingPage/footer_segment/Footer.jsx:
--------------------------------------------------------------------------------
1 | // STUB: contains footer nav links
2 | import React from 'react';
3 | import { Avatar, Box, IconButton, Stack } from '@mui/material';
4 | import FooterLinkList from './FooterLinkList';
5 | import {
6 | FaFacebookSquare as FacebookLogo,
7 | FaTwitter as TwitterLogo,
8 | FaInstagram as InstagramLogo,
9 | } from 'react-icons/fa';
10 |
11 | const textLinks = [
12 | {
13 | title: 'Features',
14 | bodyText: [
15 | {
16 | text: 'Link Shortening',
17 | path: '',
18 | },
19 | {
20 | text: 'Branded Links',
21 | path: '',
22 | },
23 | {
24 | text: 'Analytics',
25 | path: '',
26 | },
27 | ],
28 | },
29 | {
30 | title: 'Resources',
31 | bodyText: [
32 | {
33 | text: 'Blog',
34 | path: '',
35 | },
36 | {
37 | text: 'Developers',
38 | path: '',
39 | },
40 | {
41 | text: 'Support',
42 | path: '',
43 | },
44 | ],
45 | },
46 | {
47 | title: 'Company',
48 | bodyText: [
49 | {
50 | text: 'About',
51 | path: '',
52 | },
53 | {
54 | text: 'Our Team',
55 | path: '',
56 | },
57 | {
58 | text: 'Careers',
59 | path: '',
60 | },
61 | {
62 | text: 'Contact',
63 | path: '',
64 | },
65 | {
66 | text: 'Feedback',
67 | path: '/feedback',
68 | },
69 | ],
70 | },
71 | ];
72 |
73 | const iconLinks = [FacebookLogo, TwitterLogo, InstagramLogo];
74 |
75 | const containerStyles = {
76 | backgroundColor: 'neutral.veryDarkViolet',
77 | flexDirection: { xs: 'column', md: 'row' },
78 | justifyContent: 'space-around',
79 | py: 6,
80 | px: 5,
81 | textAlign: { xs: 'center', md: 'left' },
82 | };
83 |
84 | const textListStyles = {
85 | display: { md: 'flex' },
86 | };
87 |
88 | const iconListStyles = {
89 | flexDirection: 'row',
90 | justifyContent: 'center',
91 | alignItems: 'flex-start',
92 | py: { xs: 3, md: '5px' },
93 | };
94 |
95 | const iconButtonStyles = {
96 | p: 0,
97 | };
98 |
99 | const avatarStyles = {
100 | backgroundColor: 'revert',
101 | mx: 0.8,
102 | transform: 'scale(1.1)',
103 | '&:hover': {
104 | fill: 'hsl(180, 66%, 49%)',
105 | },
106 | };
107 |
108 | const Footer = () => {
109 | return (
110 |
111 |
112 | {/* */}
113 | SnapURL
114 |
115 |
116 |
117 | {textLinks.map((item, index) => {
118 | return ;
119 | })}
120 |
121 |
122 |
123 | {iconLinks.map((item, index) => {
124 | return (
125 |
126 |
132 |
133 | );
134 | })}
135 |
136 |
137 | );
138 | };
139 |
140 | export default Footer;
141 |
--------------------------------------------------------------------------------
/frontend/src/components/LandingPage/footer_segment/FooterLink.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Button } from '@mui/material';
3 | import propTypes from 'prop-types';
4 | import { useNavigate } from 'react-router-dom';
5 |
6 | const FooterLink = ({ bodyText, path }) => {
7 | const navigate = useNavigate();
8 |
9 | return (
10 | {
22 | navigate(path);
23 | }}
24 | >
25 | {bodyText}
26 |
27 | );
28 | };
29 |
30 | export default FooterLink;
31 |
32 | FooterLink.defaultProps = {
33 | bodyText: 'FooterLink',
34 | path: '',
35 | };
36 |
37 | FooterLink.propTypes = {
38 | bodyText: propTypes.string,
39 | path: propTypes.string,
40 | };
41 |
--------------------------------------------------------------------------------
/frontend/src/components/LandingPage/footer_segment/FooterLinkList.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Stack, Typography } from '@mui/material';
3 | import FooterLink from './FooterLink';
4 | import propTypes from 'prop-types';
5 |
6 | const FooterLinkList = ({ content }) => {
7 | return (
8 |
15 |
24 | {content.title}
25 |
26 | {content.bodyText.map((item) => (
27 |
28 | ))}
29 |
30 | );
31 | };
32 |
33 | export default FooterLinkList;
34 |
35 | FooterLinkList.defaultProps = {
36 | content: {
37 | title: 'Features',
38 | bodyText: [
39 | {
40 | text: 'Link Shortening',
41 | path: '',
42 | },
43 | {
44 | text: 'Branded Links',
45 | path: '',
46 | },
47 | {
48 | text: 'Analytics',
49 | path: '',
50 | },
51 | ],
52 | },
53 | };
54 |
55 | FooterLinkList.propTypes = {
56 | content: propTypes.object,
57 | };
58 |
--------------------------------------------------------------------------------
/frontend/src/components/LandingPage/footer_segment/FooterSegment.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import BoostBox from './BoostBox';
3 | import Footer from './Footer';
4 |
5 | const FooterSegment = () => {
6 | return (
7 |
8 |
9 |
10 |
11 | );
12 | };
13 |
14 | export default FooterSegment;
15 |
--------------------------------------------------------------------------------
/frontend/src/components/LandingPage/form/BackHalfForm.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Form, FormikProvider } from 'formik';
3 | import { Button, TextField, Typography } from '@mui/material';
4 |
5 | const BackHalfForm = (props) => {
6 | const { errors, touched, isSubmitting, handleSubmit, getFieldProps } =
7 | props.formik;
8 |
9 | return (
10 |
11 |
70 |
71 | );
72 | };
73 |
74 | export default BackHalfForm;
75 |
--------------------------------------------------------------------------------
/frontend/src/components/LandingPage/form/LinkForm.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Form, FormikProvider, useFormik } from 'formik';
3 | import { Button, Stack, TextField } from '@mui/material';
4 | import { LinkSchema } from './Schema';
5 | import mobileBackgroundShorten from '../Images/ShortenMobile.svg';
6 | import desktopBackgroundShorten from '../Images/ShortenDesktop.svg';
7 | import axios from 'axios';
8 | import PropTypes from 'prop-types';
9 |
10 | const LinkForm = ({ onFormValueChange, onSnackbarSuccess }) => {
11 | const formik = useFormik({
12 | initialValues: {
13 | link: '',
14 | },
15 | validationSchema: LinkSchema,
16 | onSubmit: async (values, actions) => {
17 | const { link } = values;
18 |
19 | // console.log("Original URL:", link);
20 |
21 | // call axios post request
22 | const ApiEndpoint = `${import.meta.env.VITE_API_ENDPOINT}/api/url`;
23 |
24 | try {
25 | const response = await axios.post(
26 | ApiEndpoint,
27 | {
28 | url: link,
29 | },
30 | {
31 | headers: {
32 | Authorization: `Bearer ${localStorage.getItem('token')}`,
33 | },
34 | },
35 | );
36 |
37 | const { shortUrl, customBackHalf } = response.data;
38 | // console.log(`Short URL: ${shortUrl} and Original URL: ${link}`);
39 | onFormValueChange({ originalUrl: link, shortUrl, customBackHalf });
40 |
41 | onSnackbarSuccess({
42 | children: 'Your shortlink is ready',
43 | severity: 'success',
44 | });
45 | actions.resetForm();
46 | // console.log("Short URL:", shortUrl);
47 | } catch (error) {
48 | // console.log(error);
49 | if (error.response.status === 401) {
50 | onSnackbarSuccess({
51 | children: 'Please login to create a shortlink',
52 | severity: 'error',
53 | });
54 | return;
55 | }
56 | onSnackbarSuccess({
57 | children: 'Something went wrong. Please try again later.',
58 | severity: 'error',
59 | });
60 | }
61 | },
62 | });
63 |
64 | const { errors, touched, isSubmitting, handleSubmit, getFieldProps } = formik;
65 |
66 | const style = {
67 | // border: "3px solid",
68 | backgroundImage: {
69 | xs: `url(${mobileBackgroundShorten})`,
70 | sm: `url(${desktopBackgroundShorten})`,
71 | },
72 | backgroundSize: {
73 | xs: '70% auto',
74 | sm: 'cover',
75 | },
76 | backgroundRepeat: 'no-repeat',
77 | backgroundPosition: {
78 | xs: 'right top',
79 | sm: 'bottom',
80 | },
81 | bgcolor: 'violetBg.main',
82 | borderRadius: 1,
83 | flexDirection: 'row',
84 | flexWrap: 'wrap',
85 | justifyContent: 'space-between',
86 | alignItems: 'center',
87 | maxWidth: '100%',
88 | // px: 6,
89 | // py: 4,
90 | px: [2, 6],
91 | py: [1, 4],
92 | };
93 |
94 | return (
95 |
96 |
129 |
130 | );
131 | };
132 |
133 | export default LinkForm;
134 |
135 | LinkForm.propTypes = {
136 | onFormValueChange: PropTypes.func.isRequired,
137 | onSnackbarSuccess: PropTypes.func.isRequired,
138 | };
139 |
--------------------------------------------------------------------------------
/frontend/src/components/LandingPage/form/LinkList.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import SingleOutput from './SingleOutput';
3 |
4 | const LinkList = ({ responseList, onSnackbarSuccess, onFormValueChange }) => {
5 | return (
6 |
7 | {responseList &&
8 | responseList.map((item) => {
9 | const { shortUrl } = item;
10 |
11 | return (
12 |
18 | );
19 | })}
20 |
21 | );
22 | };
23 |
24 | export default LinkList;
25 |
--------------------------------------------------------------------------------
/frontend/src/components/LandingPage/form/LinkParent.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import Snackbar from '@mui/material/Snackbar';
3 | import Alert from '@mui/material/Alert';
4 | import LinkForm from './LinkForm';
5 | import LinkList from './LinkList';
6 |
7 | const LinkParent = () => {
8 | const [responseList, setResponseList] = useState([]);
9 | const [snackbar, setSnackbar] = useState(null);
10 | // const [shortUrls, setShortUrls] = useState([]);
11 |
12 | React.useEffect(() => {
13 | // console.log("responseList from parent *", responseList);
14 | }, [responseList]);
15 |
16 | const handleCloseSnackbar = () => setSnackbar(null);
17 |
18 | // STUB: grab formValue from LinkForm onSubmit
19 | const handleFormValue = (param) => {
20 | // console.log("formValue from parent", param);
21 | // setResponseList([...responseList, param]);
22 | setResponseList((prev) => {
23 | // remove old url data if exists
24 | prev = prev.filter((data) => data.shortUrl !== param.shortUrl);
25 | // now update
26 | return [...prev, param];
27 | });
28 | // console.log("responseList from parent", responseList);
29 | };
30 |
31 | // STUB: grab snackbarSuccess msg from LinkForm onSubmit
32 | const handleSnackbarSuccess = (param) => {
33 | setSnackbar(param);
34 | };
35 |
36 | return (
37 |
38 |
42 |
47 |
48 | {!!snackbar && (
49 |
55 |
56 |
57 | )}
58 |
59 | );
60 | };
61 |
62 | export default LinkParent;
63 |
--------------------------------------------------------------------------------
/frontend/src/components/LandingPage/form/Schema.jsx:
--------------------------------------------------------------------------------
1 | import * as Yup from 'yup';
2 |
3 | // STUB: define form validation
4 | const LinkSchema = Yup.object().shape({
5 | link: Yup.string()
6 | .required('Please add a link')
7 | .matches(/^https?:\/\/[^\s/$.?#].[^\s]*$/, 'Must be a valid URL!'),
8 | });
9 |
10 | const backHalfSchema = Yup.object().shape({
11 | customBackHalf: Yup.string()
12 | .required('Please add a link')
13 | .min(3, 'Length should be 3-10 characters')
14 | .max(10, 'Length should be 3-10 characters'),
15 | });
16 |
17 | export { LinkSchema, backHalfSchema };
18 |
--------------------------------------------------------------------------------
/frontend/src/components/Linkinbio.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ProfileForm from './ProfileForm';
3 | import '../App.css';
4 |
5 | function Linkinbio() {
6 | return (
7 |
8 | {/* Render ProfileForm component */}
9 |
10 |
11 | );
12 | }
13 |
14 | export default Linkinbio;
15 |
--------------------------------------------------------------------------------
/frontend/src/components/Logout.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useContext } from 'react';
2 | import UserContext from '../context/UserContext';
3 | import { useNavigate } from 'react-router-dom';
4 |
5 | function Logout() {
6 | const navigate = useNavigate();
7 | const context = useContext(UserContext);
8 | // delete the token from local storage
9 | useEffect(() => {
10 | localStorage.removeItem('token');
11 | localStorage.removeItem('name');
12 |
13 | context.setUser(null);
14 | navigate('/');
15 | }, [context, navigate]);
16 |
17 | return
;
18 | }
19 |
20 | export default Logout;
21 |
--------------------------------------------------------------------------------
/frontend/src/components/Pagination.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import HistoryCard from './HistoryCard';
3 |
4 | const Pagination = ({ history, cardsPerPage, categoryArray }) => {
5 | const [currentPage, setCurrentPage] = useState(1);
6 | const totalPages = Math.ceil(history.length / cardsPerPage);
7 |
8 | const handleClick = (e, page) => {
9 | e.preventDefault();
10 | setCurrentPage(page);
11 | };
12 |
13 | const renderCards = () => {
14 | const indexOfLastCard = currentPage * cardsPerPage;
15 | const indexOfFirstCard = indexOfLastCard - cardsPerPage;
16 | const currentCards = history.slice(indexOfFirstCard, indexOfLastCard);
17 |
18 | return (
19 |
20 | {currentCards.map((data, index) => (
21 |
22 |
30 |
31 | ))}
32 |
33 | );
34 | };
35 |
36 | const renderPageNumbers = () => {
37 | const pageNumbers = [];
38 |
39 | for (let i = 1; i <= totalPages; i++) {
40 |
41 |
42 | {pageNumbers.push(
43 |
48 | handleClick(e, i)} className="page-link">
49 | {i}
50 |
51 | ,
52 | )}
53 |
54 | ;
55 | }
56 |
57 | return pageNumbers;
58 | };
59 |
60 | return (
61 |
62 |
63 | {renderCards()}
64 |
65 |
66 | {renderPageNumbers()}
67 |
68 |
69 | );
70 | };
71 |
72 | export default Pagination;
73 |
--------------------------------------------------------------------------------
/frontend/src/components/ProfilePage.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import {
3 | FaInstagram,
4 | FaLinkedin,
5 | FaGithub,
6 | FaFacebook,
7 | FaUser,
8 | FaShare,
9 | } from 'react-icons/fa';
10 | import './linkinbio.css';
11 | function ProfilePage() {
12 | const [name, setName] = useState('');
13 | const [profilePicture, setProfilePicture] = useState('');
14 | const [links, setLinks] = useState([
15 | { label: 'Instagram', url: '' },
16 | { label: 'LinkedIn', url: '' },
17 | { label: 'GitHub', url: '' },
18 | { label: 'Facebook', url: '' },
19 | ]);
20 |
21 | useEffect(() => {
22 | // Retrieve the user's details from localStorage or a backend server
23 | // For this example, we'll use localStorage
24 | setName(localStorage.getItem('name'));
25 | setProfilePicture(localStorage.getItem('profilePicture'));
26 |
27 | // Retrieve the links array from localStorage
28 | const storedLinks = JSON.parse(localStorage.getItem('links')) || [];
29 |
30 | // Update the links based on the retrieved data
31 | const updatedLinks = links.map((link) => {
32 | const storedLink = storedLinks.find(
33 | (stored) => stored.label === link.label,
34 | );
35 | if (storedLink) {
36 | return storedLink;
37 | }
38 | return link;
39 | });
40 |
41 | setLinks(updatedLinks);
42 | }, [links]); // Include 'links' as a dependency
43 |
44 | const instagramLink = links.find((link) => link.label === 'Instagram').url;
45 | const linkedinLink = links.find((link) => link.label === 'LinkedIn').url;
46 | const githubLink = links.find((link) => link.label === 'GitHub').url;
47 | const facebookLink = links.find((link) => link.label === 'Facebook').url;
48 |
49 | return (
50 |
51 |
52 |
Your Profile
53 |
54 |
55 | {profilePicture ? (
56 |
57 | ) : (
58 |
59 | )}
60 |
61 |
{name}
62 |
63 |
64 |
Social Media Links
65 | {instagramLink || linkedinLink || githubLink || facebookLink ? (
66 |
67 | {instagramLink && (
68 |
73 |
74 |
75 | )}
76 | {linkedinLink && (
77 |
82 |
83 |
84 | )}
85 | {githubLink && (
86 |
87 |
88 |
89 | )}
90 | {facebookLink && (
91 |
96 |
97 |
98 | )}
99 |
100 | ) : (
101 |
No social media links
102 | )}
103 |
104 |
105 |
106 | Share
107 |
108 |
109 |
110 | );
111 | }
112 |
113 | export default ProfilePage;
114 |
--------------------------------------------------------------------------------
/frontend/src/components/ResetPassword.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import {
3 | Container,
4 | Card,
5 | Typography,
6 | TextField,
7 | Button,
8 | Box,
9 | } from '@mui/material';
10 | import { useLocation, useNavigate } from 'react-router-dom';
11 |
12 | function ResetPassword() {
13 | const location = useLocation();
14 | const queryParams = new URLSearchParams(location.search);
15 | const token = queryParams.get('token');
16 |
17 | const [password, setPassword] = useState('');
18 | const [confirmPassword, setConfirmPassword] = useState('');
19 | const [message, setMessage] = useState('');
20 | const [isLoading, setIsLoading] = useState(false);
21 | const navigate = useNavigate();
22 |
23 | useEffect(() => {
24 | if (!token) {
25 | setMessage('Invalid or missing token.');
26 | setIsLoading(true);
27 | }
28 | }, [token]);
29 |
30 | const handleResetPassword = async () => {
31 | try {
32 | if (password !== confirmPassword) {
33 | setMessage('Passwords do not match.');
34 | return;
35 | }
36 |
37 | setIsLoading(true);
38 |
39 | // Make a POST request to your backend API to reset the password
40 | const response = await fetch(
41 | `${import.meta.env.VITE_API_ENDPOINT}/auth/reset-password`,
42 | {
43 | method: 'POST',
44 | headers: {
45 | 'Content-Type': 'application/json',
46 | },
47 | body: JSON.stringify({ token, password }),
48 | },
49 | );
50 |
51 | if (response.status === 200) {
52 | setMessage('Password reset successful.');
53 | navigate('/login');
54 | } else {
55 | // Handle error response from the backend
56 | const data = await response.json();
57 | setMessage(data.error || 'Password reset failed.');
58 | }
59 | } catch (error) {
60 | console.error('Error resetting password:', error);
61 | setMessage('Password reset failed.');
62 | } finally {
63 | setIsLoading(false); // re-enable the btn
64 | }
65 | };
66 |
67 | return (
68 |
78 |
79 |
80 |
81 |
82 | Reset Password
83 |
84 |
116 |
117 | {message}
118 |
119 |
120 |
121 |
122 |
123 | );
124 | }
125 |
126 | export default ResetPassword;
127 |
--------------------------------------------------------------------------------
/frontend/src/components/Sharepage.css:
--------------------------------------------------------------------------------
1 | /* Container styles */
2 | .share-pages-container {
3 | background-color: #f7f7f7;
4 | padding: 20px;
5 | border-radius: 10px;
6 | text-align: center;
7 | }
8 |
9 | /* Input styles */
10 | .url-input {
11 | width: 100%;
12 | padding: 10px;
13 | margin: 10px 0;
14 | border: 1px solid #ccc;
15 | border-radius: 5px;
16 | }
17 |
18 | /* Shorten button styles */
19 | .shorten-button {
20 | background-color: #007bff;
21 | color: white;
22 | border: none;
23 | border-radius: 5px;
24 | padding: 10px 20px;
25 | cursor: pointer;
26 | }
27 |
28 | /* Shortened URL container styles */
29 | .short-url-container {
30 | background-color: #fff;
31 | border: 1px solid #ddd;
32 | border-radius: 5px;
33 | padding: 20px;
34 | margin-top: 20px;
35 | }
36 |
37 | /* Shortened URL text styles */
38 | .short-url-text {
39 | font-weight: bold;
40 | color: #333;
41 | margin-bottom: 10px;
42 | }
43 |
44 | /* Shortened URL styles */
45 | .short-url {
46 | color: #007bff;
47 | word-wrap: break-word;
48 | }
49 |
50 | /* Platform select styles */
51 | .platform-select {
52 | width: 100%;
53 | padding: 10px;
54 | margin: 10px 0;
55 | border: 1px solid #ccc;
56 | border-radius: 5px;
57 | }
58 |
59 | /* Share button styles */
60 | .share-button {
61 | background-color: #007bff;
62 | color: white;
63 | border: none;
64 | border-radius: 5px;
65 | padding: 10px 20px;
66 | cursor: pointer;
67 | }
68 |
69 | /* Share.css */
70 |
71 | /* Container styles */
72 | .share-pages-container {
73 | background-color: #f7f7f7;
74 | padding: 20px;
75 | border-radius: 10px;
76 | text-align: center;
77 | }
78 |
79 | /* Input styles */
80 | .url-input {
81 | width: 100%;
82 | padding: 10px;
83 | margin: 10px 0;
84 | border: 1px solid #ccc;
85 | border-radius: 5px;
86 | }
87 |
88 | /* Shorten button styles */
89 | .shorten-button {
90 | background-color: #4b3f6b;
91 | color: white;
92 | border: none;
93 | border-radius: 5px;
94 | padding: 10px 20px;
95 | cursor: pointer;
96 | }
97 |
98 | /* Shortened URL container styles */
99 | .short-url-container {
100 | background-color: #fff;
101 | border: 1px solid #ddd;
102 | border-radius: 5px;
103 | padding: 20px;
104 | margin-top: 20px;
105 | }
106 |
107 | /* Shortened URL text styles */
108 | .short-url-text {
109 | font-weight: bold;
110 | color: #333;
111 | margin-bottom: 10px;
112 | }
113 |
114 | /* Shortened URL styles */
115 | .short-url {
116 | color: #00a6ff;
117 | word-wrap: break-word;
118 | }
119 |
120 | /* Copy Link button styles */
121 | .copy-link-button {
122 | background-color: #007bff;
123 | color: white;
124 | border: none;
125 | border-radius: 5px;
126 | padding: 10px 20px;
127 | cursor: pointer;
128 | margin-top: 10px;
129 | }
130 |
131 | /* Platform select styles */
132 | .platform-select {
133 | width: 100%;
134 | padding: 10px;
135 | margin: 10px 0;
136 | border: 1px solid #ccc;
137 | border-radius: 5px;
138 | }
139 |
140 | /* Share button styles */
141 | .share-button {
142 | background-color: #4b3f6b;
143 | color: white;
144 | border: none;
145 | border-radius: 5px;
146 | padding: 10px 20px;
147 | cursor: pointer;
148 | }
149 |
--------------------------------------------------------------------------------
/frontend/src/components/Sharepage.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import shortid from 'shortid';
3 | import { CopyToClipboard } from 'react-copy-to-clipboard'; // Import the CopyToClipboard component
4 | import './Sharepage.css'; // Import the CSS file
5 |
6 | const Sharepages = () => {
7 | const [longUrl, setLongUrl] = useState('');
8 | const [shortUrl, setShortUrl] = useState('');
9 | const [copied, setCopied] = useState(false); // State to track if the link is copied
10 | const [platform, setPlatform] = useState('');
11 |
12 | const handleShorten = () => {
13 | const shortId = shortid.generate();
14 | const shortenedUrl = `https://yourshorteningservice.com/${shortId}`;
15 | setShortUrl(shortenedUrl);
16 | setCopied(false); // Reset the copied state
17 | };
18 |
19 | const handleShare = () => {
20 | if (platform === 'twitter') {
21 | window.open(
22 | `https://twitter.com/intent/tweet?text=${shortUrl}`,
23 | '_blank',
24 | );
25 | } else if (platform === 'facebook') {
26 | window.open(
27 | `https://www.facebook.com/sharer/sharer.php?u=${shortUrl}`,
28 | '_blank',
29 | );
30 | } else if (platform === 'linkedin') {
31 | // Share on LinkedIn
32 | window.open(
33 | `https://www.linkedin.com/sharing/share-offsite/?url=${shortUrl}`,
34 | '_blank',
35 | );
36 | } else if (platform === 'whatsapp') {
37 | window.open(`https://api.whatsapp.com/send?text=${shortUrl}`, '_blank');
38 | } else {
39 | alert('Unsupported platform.');
40 | }
41 | };
42 |
43 | return (
44 |
45 |
setLongUrl(e.target.value)}
51 | />
52 |
53 | Shorten
54 |
55 |
56 | {shortUrl && (
57 |
58 |
Shortened URL:
59 |
{shortUrl}
60 |
61 | {/* Copy to Clipboard button */}
62 |
setCopied(true)}>
63 |
64 | {copied ? 'Copied!' : 'Copy Link'}
65 |
66 |
67 |
68 |
setPlatform(e.target.value)}
72 | >
73 | Select a platform to share
74 | Twitter
75 | Facebook
76 | LinkedIn
77 | WhatsApp
78 |
79 |
80 | Share
81 |
82 |
83 | )}
84 |
85 | );
86 | };
87 |
88 | export default Sharepages;
89 |
--------------------------------------------------------------------------------
/frontend/src/context/UserContext.js:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react';
2 |
3 | const UserContext = createContext();
4 |
5 | export default UserContext;
6 |
--------------------------------------------------------------------------------
/frontend/src/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import App from './App';
4 |
5 | const root = ReactDOM.createRoot(document.getElementById('root'));
6 | root.render(
7 | //
8 | ,
9 | //
10 | );
11 |
--------------------------------------------------------------------------------
/frontend/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react-swc'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | build: {
8 | outDir: 'build',
9 | },
10 | })
--------------------------------------------------------------------------------
/netlify.toml:
--------------------------------------------------------------------------------
1 | [build]
2 | base = "frontend" # Specify the directory where your frontend code is located.
3 | publish = "build" # Define where your built frontend files will be located.
4 | command = "npm run build" # Adjust this command based on your frontend build process.
5 |
6 | [context.production]
7 | command = "npm run build"
8 |
9 | [context.deploy-preview]
10 | command = "npm run build"
11 |
12 | [ignore]
13 | gitignore = true # Ignore changes to the backend directory to prevent builds when backend code changes.
14 | files = [".all-contributorsrc", "LICENSE", "README.md", "backend", "chrome-extension"] # Ignore changes to these files.
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "url-shortener-app",
3 | "version": "1.0.0",
4 | "description": "A URL Shortener Web Application",
5 | "main": "index.js",
6 | "scripts": {
7 | "install-frontend": "cd frontend && npm install",
8 | "install-backend": "cd backend && npm install",
9 | "install": "npm run install-frontend && npm run install-backend",
10 | "backend": "cd backend && npm start",
11 | "frontend": "cd frontend && npm start",
12 | "start": "npm-run-all --parallel backend frontend",
13 | "build-frontend": "cd frontend && npm run build ",
14 | "test": "echo \"Error: no test specified\" && exit 1"
15 | },
16 | "repository": {
17 | "type": "git",
18 | "url": "git+https://github.com/DhananjayThomble/URL-Shortener-App.git"
19 | },
20 | "author": "",
21 | "license": "ISC",
22 | "bugs": {
23 | "url": "https://github.com/DhananjayThomble/URL-Shortener-App/issues"
24 | },
25 | "homepage": "https://github.com/DhananjayThomble/URL-Shortener-App#readme",
26 | "devDependencies": {
27 | "npm-run-all": "^4.1.5"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------