├── .eslintrc.json ├── .gitignore ├── LICENSE ├── README.md ├── app ├── [shortUrl] │ ├── main.tsx │ └── page.tsx ├── about │ ├── main.tsx │ └── page.tsx ├── analytics │ └── [id] │ │ ├── main.tsx │ │ └── page.tsx ├── auth │ ├── forgot-password │ │ ├── main.tsx │ │ └── page.tsx │ ├── signin │ │ ├── main.tsx │ │ └── page.tsx │ └── signup │ │ ├── main.tsx │ │ └── page.tsx ├── contact │ ├── main.tsx │ └── page.tsx ├── dashboard │ ├── main.tsx │ └── page.tsx ├── favicon.ico ├── forbidden │ ├── main.tsx │ └── page.tsx ├── globals.css ├── layout.tsx ├── not-found.tsx ├── page.tsx └── profile │ ├── main.tsx │ └── page.tsx ├── components.json ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public ├── next.svg ├── robots.txt └── vercel.svg ├── src ├── ServerComponents │ ├── dashboard │ │ ├── InputUrls.tsx │ │ ├── ModalDelete.tsx │ │ ├── ModalUpdate.tsx │ │ └── ShortUrlsList.tsx │ └── profile │ │ └── ProfileUi.tsx ├── components │ ├── DropDownProfile.tsx │ ├── Footer.tsx │ ├── Navbar.tsx │ ├── Options.tsx │ ├── Sidebar.tsx │ ├── SignUpForm.tsx │ ├── Trakteer.tsx │ ├── icons │ │ ├── AddNoteIcon.tsx │ │ ├── AnalyticIcon.tsx │ │ ├── CopyDocumentIcon.tsx │ │ ├── DashboardIcon.tsx │ │ ├── DeleteDocumentIcon.tsx │ │ ├── EditDocumentIcon.tsx │ │ ├── GoogleIcon.tsx │ │ ├── ProfileUser.tsx │ │ ├── TrakteerIcon.tsx │ │ └── UserIcon.tsx │ ├── images │ │ ├── 404.webp │ │ ├── Forbidden.webp │ │ ├── Illustration.webp │ │ └── Logo.webp │ └── ui │ │ └── sidebar.tsx ├── config │ └── FirebaseConfig.ts ├── data │ └── UrlsType.ts ├── lib │ └── utils.ts └── sections │ └── Hero.tsx ├── tailwind.config.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.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 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Muhammad Irvan Shandika_21.11.4285 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![Logo](https://res.cloudinary.com/dszhlpm81/image/upload/v1725119989/assets/UFPMOvuUtfuMxUpG2yy%2BnF743hBOeLEzqjNEDiHixcQ%3D/Logo_mph06v.webp) 3 | 4 | 5 | # iShort URLs 6 | 7 | iShort URLs is a website platform that provides url shorteners for free and without any restrictions. iShort URLs is a small project to accompany college holidays and while looking for titles for thesis. iShort URLs can also help users to shorten urls without having to send long urls. 8 | ## Screenshots 9 | 10 | ### Main page view 11 | ![App Screenshot](https://res.cloudinary.com/dszhlpm81/image/upload/v1725119702/assets/UFPMOvuUtfuMxUpG2yy%2BnF743hBOeLEzqjNEDiHixcQ%3D/8b819794-928a-4add-976d-780b0685ad6c.png) 12 | 13 | ## FAQ 14 | 15 | #### Is iShort URLs free? 16 | 17 | Of course it's free and provides all the features without any limits. 18 | 19 | #### Is my data safe? 20 | 21 | Of course, because it uses the latest technology with the latest security performance. 22 | 23 | 24 | ## Tech Stack 25 | 26 | **Client:** Nextjs, Typescript, TailwindCSS, Next UI, ShadCN 27 | 28 | **Server:** Node, Firebase 29 | 30 | 31 | ## License 32 | 33 | [MIT](https://choosealicense.com/licenses/mit/) 34 | 35 | 36 | ## Support 37 | 38 | For support, email support@ishort.my.id. 39 | 40 | -------------------------------------------------------------------------------- /app/[shortUrl]/main.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useEffect, useState } from "react"; 3 | import { useRouter } from "next/navigation"; 4 | import { collection, query, where, getDocs, updateDoc, doc } from "firebase/firestore"; 5 | import { db } from "@/src/config/FirebaseConfig"; 6 | import Image from "next/image"; 7 | import FingerprintJS from "@fingerprintjs/fingerprintjs"; 8 | import Link from "next/link"; 9 | import { Button } from "@nextui-org/react"; 10 | 11 | interface RedirectPageProps { 12 | params: { shortUrl: string }; 13 | } 14 | 15 | export default function RedirectPage({ params }: RedirectPageProps) { 16 | const [countdown, setCountdown] = useState(5); 17 | const [longUrl, setLongUrl] = useState(null); 18 | const [error, setError] = useState(null); 19 | const router = useRouter(); 20 | 21 | useEffect(() => { 22 | const fetchLongUrl = async () => { 23 | try { 24 | const q = query(collection(db, "shorturls"), where("shortUrl", "==", params.shortUrl)); 25 | const querySnapshot = await getDocs(q); 26 | 27 | if (querySnapshot.empty) { 28 | setError("Short URL not found in the database"); 29 | return; 30 | } 31 | 32 | const docData = querySnapshot.docs[0].data(); 33 | setLongUrl(docData.longUrl); 34 | 35 | if (process.env.NODE_ENV === "production") { 36 | trackVisitor(querySnapshot.docs[0].id, docData.visitors || []); 37 | } 38 | } catch (err) { 39 | setError("Failed to fetch the long URL from the database"); 40 | } 41 | }; 42 | 43 | const trackVisitor = async (docId: string, existingVisitors: { visitorId: string; accessDate: string }[]) => { 44 | const fp = await FingerprintJS.load(); 45 | const result = await fp.get(); 46 | const visitorId = result.visitorId; 47 | 48 | const isVisitorExist = existingVisitors.some((visitor) => visitor.visitorId === visitorId); 49 | 50 | if (!isVisitorExist) { 51 | const currentDate = new Date().toISOString(); 52 | 53 | const newVisitor = { 54 | visitorId, 55 | accessDate: currentDate, 56 | }; 57 | 58 | existingVisitors.push(newVisitor); 59 | 60 | await updateDoc(doc(db, "shorturls", docId), { 61 | visitors: existingVisitors, 62 | visitorCount: existingVisitors.length, 63 | }); 64 | } 65 | }; 66 | 67 | fetchLongUrl(); 68 | }, [params.shortUrl]); 69 | 70 | useEffect(() => { 71 | if (longUrl) { 72 | const timer = setInterval(() => { 73 | setCountdown((prev) => (prev > 0 ? prev - 1 : 0)); 74 | }, 1000); 75 | 76 | const redirectTimer = setTimeout(() => { 77 | router.push(longUrl); 78 | }, 5000); 79 | 80 | return () => { 81 | clearInterval(timer); 82 | clearTimeout(redirectTimer); 83 | }; 84 | } 85 | }, [longUrl, router]); 86 | 87 | if (error) { 88 | return ( 89 |
90 | Error 97 |

Error

98 |

{error}

99 | 100 | Go back to the homepage 101 | 102 |
103 | ); 104 | } 105 | 106 | return ( 107 |
108 |

Redirecting...

109 |

You will be redirected to:

110 |

{longUrl}

111 |

in {countdown} seconds...

112 | 115 |
116 | ); 117 | } 118 | -------------------------------------------------------------------------------- /app/[shortUrl]/page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import type { Metadata } from "next"; 3 | import NavigasiBar from "@/src/components/Navbar"; 4 | import Footer from "@/src/components/Footer"; 5 | import RedirectPage from "./main"; 6 | 7 | export const metadata: Metadata = { 8 | title: "Redirecting... | iShort URLs", 9 | }; 10 | 11 | export default function Redirect({ params }: { params: { shortUrl: string } }) { 12 | return ( 13 | <> 14 | 15 | 16 |