feature-page (#11,#4)
Refactor URL Shortening Feature and UI Enhancements * Adds new feature page, user interface and user interface enhancement Co-authored-by: anthony <anthony@Project42 - SCM <noreply@scm.project42.io>> Co-committed-by: anthony <anthony@Project42 - SCM <noreply@scm.project42.io>>
This commit is contained in:
parent
11e9ee6a32
commit
7f813e2c34
17 changed files with 577 additions and 290 deletions
45
frontend/package-lock.json
generated
45
frontend/package-lock.json
generated
|
@ -16,9 +16,10 @@
|
||||||
"@types/react": "^18.3.12",
|
"@types/react": "^18.3.12",
|
||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "^18.3.1",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
|
"framer-motion": "^11.15.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-icons": "^5.3.0",
|
"react-icons": "^5.4.0",
|
||||||
"react-router-dom": "^7.0.1",
|
"react-router-dom": "^7.0.1",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
"react-toastify": "^11.0.1",
|
"react-toastify": "^11.0.1",
|
||||||
|
@ -7850,6 +7851,32 @@
|
||||||
"url": "https://github.com/sponsors/rawify"
|
"url": "https://github.com/sponsors/rawify"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/framer-motion": {
|
||||||
|
"version": "11.15.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.15.0.tgz",
|
||||||
|
"integrity": "sha512-MLk8IvZntxOMg7lDBLw2qgTHHv664bYoYmnFTmE0Gm/FW67aOJk0WM3ctMcG+Xhcv+vh5uyyXwxvxhSeJzSe+w==",
|
||||||
|
"dependencies": {
|
||||||
|
"motion-dom": "^11.14.3",
|
||||||
|
"motion-utils": "^11.14.3",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@emotion/is-prop-valid": "*",
|
||||||
|
"react": "^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^18.0.0 || ^19.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@emotion/is-prop-valid": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fresh": {
|
"node_modules/fresh": {
|
||||||
"version": "0.5.2",
|
"version": "0.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||||
|
@ -10668,6 +10695,16 @@
|
||||||
"mkdirp": "bin/cmd.js"
|
"mkdirp": "bin/cmd.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/motion-dom": {
|
||||||
|
"version": "11.14.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.14.3.tgz",
|
||||||
|
"integrity": "sha512-lW+D2wBy5vxLJi6aCP0xyxTxlTfiu+b+zcpVbGVFUxotwThqhdpPRSmX8xztAgtZMPMeU0WGVn/k1w4I+TbPqA=="
|
||||||
|
},
|
||||||
|
"node_modules/motion-utils": {
|
||||||
|
"version": "11.14.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.14.3.tgz",
|
||||||
|
"integrity": "sha512-Xg+8xnqIJTpr0L/cidfTTBFkvRw26ZtGGuIhA94J9PQ2p4mEa06Xx7QVYZH0BP+EpMSaDlu+q0I0mmvwADPsaQ=="
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
@ -12928,9 +12965,9 @@
|
||||||
"integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg=="
|
"integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg=="
|
||||||
},
|
},
|
||||||
"node_modules/react-icons": {
|
"node_modules/react-icons": {
|
||||||
"version": "5.3.0",
|
"version": "5.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.4.0.tgz",
|
||||||
"integrity": "sha512-DnUk8aFbTyQPSkCfF8dbX6kQjXA9DktMeJqfjrg6cK9vwQVMxmcA3BfP4QoiztVmEHtwlTgLFsPuH2NskKT6eg==",
|
"integrity": "sha512-7eltJxgVt7X64oHh6wSWNwwbKTCtMfK35hcjvJS0yxEAhPM8oUKdS3+kqaW1vicIltw+kR2unHaa12S9pPALoQ==",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "*"
|
"react": "*"
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,9 +11,10 @@
|
||||||
"@types/react": "^18.3.12",
|
"@types/react": "^18.3.12",
|
||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "^18.3.1",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
|
"framer-motion": "^11.15.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-icons": "^5.3.0",
|
"react-icons": "^5.4.0",
|
||||||
"react-router-dom": "^7.0.1",
|
"react-router-dom": "^7.0.1",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
"react-toastify": "^11.0.1",
|
"react-toastify": "^11.0.1",
|
||||||
|
|
|
@ -1,25 +1,13 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
|
import { BrowserRouter as Router } from "react-router-dom";
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
|
import AppContent from "./AppContent";
|
||||||
// Pages & Components
|
|
||||||
import Navbar from "./components/navbar/Navbar";
|
|
||||||
import Footer from "./components/footer/Footer";
|
|
||||||
import Home from "./pages/home/Home";
|
|
||||||
import About from "./pages/about/About";
|
|
||||||
import Contact from "./pages/contact/Contact";
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<div className="App">
|
<div className="App">
|
||||||
<Router>
|
<Router>
|
||||||
<Navbar />
|
<AppContent />
|
||||||
<Routes>
|
|
||||||
<Route path="/" element={<Home />} />
|
|
||||||
<Route path="/about" element={<About />} />
|
|
||||||
<Route path="/contact" element={<Contact />} />
|
|
||||||
</Routes>
|
|
||||||
<Footer />
|
|
||||||
</Router>
|
</Router>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
26
frontend/src/AppContent.tsx
Normal file
26
frontend/src/AppContent.tsx
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import { Routes, Route, useLocation } from "react-router-dom";
|
||||||
|
import "./App.css";
|
||||||
|
import { AnimatePresence } from "framer-motion";
|
||||||
|
|
||||||
|
// Pages & Components
|
||||||
|
import Navbar from "./components/navbar/Navbar";
|
||||||
|
import Footer from "./components/footer/Footer";
|
||||||
|
import Home from "./pages/home/Home";
|
||||||
|
import Features from "./pages/features/Features";
|
||||||
|
import Contact from "./pages/contact/Contact";
|
||||||
|
|
||||||
|
export default function AppContent() {
|
||||||
|
const location = useLocation();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Navbar />
|
||||||
|
<AnimatePresence mode="wait" />
|
||||||
|
<Routes location={location} key={location.pathname}>
|
||||||
|
<Route path="/" element={<Home />} />
|
||||||
|
<Route path="/features" element={<Features />} />
|
||||||
|
<Route path="/contact" element={<Contact />} />
|
||||||
|
</Routes>
|
||||||
|
<Footer />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
155
frontend/src/components/URLShortener/URLShortener.css
Normal file
155
frontend/src/components/URLShortener/URLShortener.css
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
.URLShortener-component {
|
||||||
|
height: 300px;
|
||||||
|
width: 800px;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: 0px 10px 15px rgba(0, 0, 0, 0.3), 0px 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 2fr 1fr;
|
||||||
|
transition: box-shadow 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.URLShortener-component:hover {
|
||||||
|
box-shadow: 0px 20px 25px rgba(0, 0, 0, 0.3), 0px 8px 10px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-side {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
padding-bottom: 20%;
|
||||||
|
padding-left: 5%;
|
||||||
|
padding-right: 5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-side h1 {
|
||||||
|
color: var(--color-tertiary);
|
||||||
|
border-bottom: 1px solid var(--color-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-input {
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-input input {
|
||||||
|
width: 250px;
|
||||||
|
height: 40px;
|
||||||
|
outline: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.4);
|
||||||
|
padding-left: 5px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-input button {
|
||||||
|
border-radius: 5px;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
background-color: var(--color-tertiary);
|
||||||
|
border: none;
|
||||||
|
min-width: 80px;
|
||||||
|
color: #fff;
|
||||||
|
transition: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-input button:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-side {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
border-left: 2px solid rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 0 5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-side-title h1 {
|
||||||
|
color: var(--color-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-side-content p {
|
||||||
|
font-family: "Roboto", sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #808080;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 730px) {
|
||||||
|
.URLShortener-component {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
height: auto;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-side {
|
||||||
|
padding-bottom: 5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-side {
|
||||||
|
border-left: none;
|
||||||
|
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 475px) {
|
||||||
|
.url-input input {
|
||||||
|
height: 25px;
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-side h1 {
|
||||||
|
font-size: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-side-title h1 {
|
||||||
|
font-size: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-side-content p {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-input button {
|
||||||
|
min-width: 60px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 475px) {
|
||||||
|
.url-input {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-input button {
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-input input {
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-toast {
|
||||||
|
font-family: "Roboto", sans-serif;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-input.shortened {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-20px);
|
||||||
|
animation: fadeIn 0.3s forwards;
|
||||||
|
}
|
112
frontend/src/components/URLShortener/URLShortener.tsx
Normal file
112
frontend/src/components/URLShortener/URLShortener.tsx
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import "./URLShortener.css";
|
||||||
|
import axios from "axios";
|
||||||
|
import { ToastContainer, toast } from "react-toastify";
|
||||||
|
import "react-toastify/dist/ReactToastify.css";
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
const [url, setUrl] = useState<string>("");
|
||||||
|
const [shortenedUrl, setShortenedUrl] = useState<string>("");
|
||||||
|
const [showInput, setShowInput] = useState<boolean>(false);
|
||||||
|
|
||||||
|
async function ShortenIt(e: React.FormEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
toast.error("Please provide a URL", {
|
||||||
|
position: "top-right",
|
||||||
|
autoClose: 5000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: true,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
progress: undefined,
|
||||||
|
theme: "dark",
|
||||||
|
className: "custom-toast",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const timestamp = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
|
// Send the POST request to the backend
|
||||||
|
await axios
|
||||||
|
.post("http://127.0.0.1:8000/shortenit", {
|
||||||
|
url: url,
|
||||||
|
timestamp: timestamp,
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (response) {
|
||||||
|
const code: string = response.data.url;
|
||||||
|
const fullShortenedUrl: string = `http://127.0.0.1:8000/${code}`;
|
||||||
|
setShortenedUrl(fullShortenedUrl);
|
||||||
|
setShowInput(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setShowInput(false);
|
||||||
|
}, 10000);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyURL() {
|
||||||
|
try {
|
||||||
|
if (shortenedUrl) {
|
||||||
|
await navigator.clipboard.writeText(shortenedUrl);
|
||||||
|
toast.success("Copied to Clipboard", {
|
||||||
|
position: "top-right",
|
||||||
|
autoClose: 5000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: true,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
progress: undefined,
|
||||||
|
theme: "dark",
|
||||||
|
className: "custom-toast",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="URLShortener-component">
|
||||||
|
<div className="left-side">
|
||||||
|
<h1>Paste the URL</h1>
|
||||||
|
<form className="url-input" onSubmit={ShortenIt}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="www.example.com"
|
||||||
|
aria-label="URL input field"
|
||||||
|
value={url}
|
||||||
|
onChange={(e) => setUrl(e.target.value)}
|
||||||
|
/>
|
||||||
|
<button type="submit">Shorten It</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{shortenedUrl && showInput && (
|
||||||
|
<div className="url-input shortened">
|
||||||
|
<input type="text" readOnly value={shortenedUrl} />
|
||||||
|
<button onClick={copyURL}>Copy</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="right-side">
|
||||||
|
<div className="right-side-title">
|
||||||
|
<h1>Shorten It</h1>
|
||||||
|
</div>
|
||||||
|
<div className="right-side-content">
|
||||||
|
<p>
|
||||||
|
Shortenit is a free and open-source URL shortening service designed
|
||||||
|
for simplicity and efficiency.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ToastContainer />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
8
frontend/src/components/featuresCard/CardProps.ts
Normal file
8
frontend/src/components/featuresCard/CardProps.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export interface CardProps {
|
||||||
|
icon: React.ReactNode;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
link : string;
|
||||||
|
}
|
64
frontend/src/components/featuresCard/FeaturesCard.css
Normal file
64
frontend/src/components/featuresCard/FeaturesCard.css
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
.Card {
|
||||||
|
position: relative;
|
||||||
|
height: 270px;
|
||||||
|
width: 250px;
|
||||||
|
border-radius: 10px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: repeat(2, 1fr);
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0px 10px 15px rgba(0, 0, 0, 0.15), 0px 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
font-family: "Roboto", sans-serif;
|
||||||
|
padding: 15px;
|
||||||
|
transition: .3s, transform .6s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Card:hover {
|
||||||
|
box-shadow: 0px 10px 15px rgba(0, 0, 0, 0.4), 0px 4px 6px rgba(0, 0, 0, 0.2);
|
||||||
|
transform: translateY(-25px);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.top-part {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-icon {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 30px;
|
||||||
|
color: var(--color-primary);
|
||||||
|
height: 60px;
|
||||||
|
width: 60px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: var(--color-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-part {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-description {
|
||||||
|
color: #666;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-button button {
|
||||||
|
padding: 8px 30px;
|
||||||
|
border-radius: 5px;
|
||||||
|
background-color: #1d4ed8;
|
||||||
|
border: 1px solid #f5f5f5;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: 0.3s;
|
||||||
|
}
|
||||||
|
.feature-button button a {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-button button:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
38
frontend/src/components/featuresCard/FeaturesCard.tsx
Normal file
38
frontend/src/components/featuresCard/FeaturesCard.tsx
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import React from "react";
|
||||||
|
import "./FeaturesCard.css";
|
||||||
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
|
import { CardProps } from "./CardProps";
|
||||||
|
|
||||||
|
const FeaturesCard: React.FC<CardProps> = ({
|
||||||
|
icon,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
link,
|
||||||
|
}) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
return (
|
||||||
|
<div className="Card" onClick={() => navigate(`${link}`)}>
|
||||||
|
<div className="top-part">
|
||||||
|
<div className="feature-icon">{icon}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bottom-part">
|
||||||
|
<div className="feature-title">
|
||||||
|
<h3>{title}</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="feature-description">
|
||||||
|
<p>{description}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="feature-button">
|
||||||
|
<button>
|
||||||
|
<Link to={link}>Read More</Link>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FeaturesCard;
|
|
@ -4,6 +4,9 @@
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo a {
|
.logo a {
|
||||||
|
|
|
@ -23,7 +23,7 @@ function Navbar() {
|
||||||
<Link to="/">Home</Link>
|
<Link to="/">Home</Link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<Link to="/about">About</Link>
|
<Link to="/features">Features</Link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<Link to="/contact">Contact</Link>
|
<Link to="/contact">Contact</Link>
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import "./About.css";
|
|
||||||
|
|
||||||
function About() {
|
|
||||||
return <h1>ABOUT PAGE</h1>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default About;
|
|
70
frontend/src/pages/features/Features.css
Normal file
70
frontend/src/pages/features/Features.css
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
.features-header {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
font-family: "Roboto", sans-serif;
|
||||||
|
background-color: var(--color-primary);
|
||||||
|
margin-bottom: 75px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-header h2 {
|
||||||
|
font-size: 25px;
|
||||||
|
color: var(--color-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-header h1 {
|
||||||
|
font-size: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-body {
|
||||||
|
min-height: 100vh;
|
||||||
|
margin: 0 10%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
grid-row-gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1466px) {
|
||||||
|
.features-body {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1076px) {
|
||||||
|
.features-body {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 708px) {
|
||||||
|
.features-body {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(1, 1fr);
|
||||||
|
grid-row-gap: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 390px) {
|
||||||
|
.features-body {
|
||||||
|
display: flex;
|
||||||
|
gap: 1em;
|
||||||
|
overflow-x: scroll;
|
||||||
|
scroll-snap-type: x mandatory;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-header {
|
||||||
|
margin-bottom: -50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Feature-Card {
|
||||||
|
flex-shrink: 0;
|
||||||
|
aspect-ratio: 3/2;
|
||||||
|
scroll-snap-align: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-header h1 {
|
||||||
|
font-size: 25px;
|
||||||
|
}
|
||||||
|
}
|
41
frontend/src/pages/features/Features.tsx
Normal file
41
frontend/src/pages/features/Features.tsx
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import React from "react";
|
||||||
|
import "./Features.css";
|
||||||
|
import FeaturesCard from "../../components/featuresCard/FeaturesCard";
|
||||||
|
import { FaLink } from "react-icons/fa";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
|
function Features() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<motion.section
|
||||||
|
className="features-header"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ duration: 0.7, ease: "easeInOut" }}
|
||||||
|
>
|
||||||
|
<h2>Features</h2>
|
||||||
|
<h1>Our Services & Features</h1>
|
||||||
|
</motion.section>
|
||||||
|
|
||||||
|
<motion.section
|
||||||
|
className="features-body"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ duration: 1.5, ease: "easeInOut" }}
|
||||||
|
>
|
||||||
|
<div className="Feature-Card">
|
||||||
|
<FeaturesCard
|
||||||
|
icon={<FaLink />}
|
||||||
|
title="URL Shortener"
|
||||||
|
description="Transform long URLs into short, shareable links in a blink of an eye."
|
||||||
|
link="/"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</motion.section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Features;
|
|
@ -6,161 +6,3 @@
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-container {
|
|
||||||
height: 300px;
|
|
||||||
width: 800px;
|
|
||||||
border-radius: 5px;
|
|
||||||
box-shadow: 0px 10px 15px rgba(0, 0, 0, 0.3), 0px 4px 6px rgba(0, 0, 0, 0.1);
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 2fr 1fr;
|
|
||||||
transition: box-shadow 0.3s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-container:hover {
|
|
||||||
box-shadow: 0px 20px 25px rgba(0, 0, 0, 0.3), 0px 8px 10px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.left-side {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-start;
|
|
||||||
gap: 10px;
|
|
||||||
padding-bottom: 20%;
|
|
||||||
padding-left: 5%;
|
|
||||||
padding-right: 5%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.left-side h1 {
|
|
||||||
color: var(--color-tertiary);
|
|
||||||
border-bottom: 1px solid var(--color-tertiary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.url-input {
|
|
||||||
display: flex;
|
|
||||||
gap: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.url-input input {
|
|
||||||
/* position: relative; */
|
|
||||||
width: 250px;
|
|
||||||
height: 40px;
|
|
||||||
outline: none;
|
|
||||||
border-radius: 5px;
|
|
||||||
border-top-right-radius: 0;
|
|
||||||
border-bottom-right-radius: 0;
|
|
||||||
border: 1px solid rgba(0, 0, 0, 0.4);
|
|
||||||
padding-left: 5px;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.url-input button {
|
|
||||||
border-radius: 5px;
|
|
||||||
border-top-left-radius: 0;
|
|
||||||
border-bottom-left-radius: 0;
|
|
||||||
background-color: var(--color-tertiary);
|
|
||||||
border: none;
|
|
||||||
min-width: 80px;
|
|
||||||
color: #fff;
|
|
||||||
transition: 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.url-input button:hover {
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.right-side {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
text-align: center;
|
|
||||||
border-left: 2px solid rgba(0, 0, 0, 0.1);
|
|
||||||
padding: 0 5%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.right-side-title h1 {
|
|
||||||
color: var(--color-tertiary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.right-side-content p {
|
|
||||||
font-family: "Roboto", sans-serif;
|
|
||||||
font-size: 14px;
|
|
||||||
/* color: var(--text-color); */
|
|
||||||
color: #808080;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 730px) {
|
|
||||||
.home-container {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
height: auto;
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.left-side {
|
|
||||||
padding-bottom: 5%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.right-side {
|
|
||||||
border-left: none;
|
|
||||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 475px) {
|
|
||||||
.url-input input {
|
|
||||||
height: 25px;
|
|
||||||
width: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.left-side h1 {
|
|
||||||
font-size: 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.right-side-title h1 {
|
|
||||||
font-size: 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.right-side-content p {
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.url-input button {
|
|
||||||
min-width: 60px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 475px) {
|
|
||||||
.url-input {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.url-input button {
|
|
||||||
height: 30px;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.url-input input {
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-toast {
|
|
||||||
font-family: "Roboto", "sans-serif";
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeIn {
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.url-input.shortened {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-20px);
|
|
||||||
animation: fadeIn 0.3s forwards;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,110 +1,20 @@
|
||||||
import React, { FormEvent, useState, useEffect } from "react";
|
import React from "react";
|
||||||
import "./Home.css";
|
import "./Home.css";
|
||||||
import axios from "axios";
|
import { motion } from "framer-motion";
|
||||||
import { ToastContainer, toast } from "react-toastify";
|
|
||||||
import "react-toastify/dist/ReactToastify.css";
|
import URLShortener from "../../components/URLShortener/URLShortener";
|
||||||
|
|
||||||
function Home() {
|
function Home() {
|
||||||
const [url, setUrl] = useState<string>("");
|
|
||||||
const [shortenedUrl, setShortenedUrl] = useState<string>("");
|
|
||||||
|
|
||||||
async function ShortenIt(e: React.FormEvent) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (!url) {
|
|
||||||
toast.error("Please provide a URL", {
|
|
||||||
position: "top-right",
|
|
||||||
autoClose: 5000,
|
|
||||||
hideProgressBar: false,
|
|
||||||
closeOnClick: true,
|
|
||||||
pauseOnHover: true,
|
|
||||||
draggable: true,
|
|
||||||
progress: undefined,
|
|
||||||
theme: "dark",
|
|
||||||
className: "custom-toast",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const timestamp = Math.floor(Date.now() / 1000);
|
|
||||||
|
|
||||||
// Send the POST request to the backend
|
|
||||||
await axios
|
|
||||||
.post("http://127.0.0.1:8000/shortenit", {
|
|
||||||
url: url,
|
|
||||||
timestamp: timestamp,
|
|
||||||
})
|
|
||||||
.then((response) => {
|
|
||||||
if (response) {
|
|
||||||
const code: string = response.data.url;
|
|
||||||
const fullShortenedUrl: string = `http://127.0.0.1:8000/${code}`;
|
|
||||||
setShortenedUrl(fullShortenedUrl);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function copyURL() {
|
|
||||||
try {
|
|
||||||
if (shortenedUrl) {
|
|
||||||
await navigator.clipboard.writeText(shortenedUrl);
|
|
||||||
toast.success("Copied to Clipboard", {
|
|
||||||
position: "top-right",
|
|
||||||
autoClose: 5000,
|
|
||||||
hideProgressBar: false,
|
|
||||||
closeOnClick: true,
|
|
||||||
pauseOnHover: true,
|
|
||||||
draggable: true,
|
|
||||||
progress: undefined,
|
|
||||||
theme: "dark",
|
|
||||||
className: "custom-toast",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="home-page">
|
<motion.div
|
||||||
<div className="home-container">
|
className="home-page"
|
||||||
<div className="left-side">
|
initial={{ opacity: 0 }}
|
||||||
<h1>Paste the URL</h1>
|
animate={{ opacity: 1 }}
|
||||||
<form className="url-input" onSubmit={ShortenIt}>
|
exit={{ opacity: 0 }}
|
||||||
<input
|
transition={{ duration: 1.5, ease: "easeInOut" }}
|
||||||
type="text"
|
>
|
||||||
placeholder="www.example.com"
|
<URLShortener />
|
||||||
aria-label="URL input field"
|
</motion.div>
|
||||||
value={url}
|
|
||||||
onChange={(e) => setUrl(e.target.value)}
|
|
||||||
/>
|
|
||||||
<button type="submit">Shorten It</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{shortenedUrl && (
|
|
||||||
<div className="url-input shortened">
|
|
||||||
<input type="text" readOnly value={shortenedUrl} />
|
|
||||||
<button onClick={copyURL}>Copy</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="right-side">
|
|
||||||
<div className="right-side-title">
|
|
||||||
<h1>Shorten It</h1>
|
|
||||||
</div>
|
|
||||||
<div className="right-side-content">
|
|
||||||
<p>
|
|
||||||
Shortenit is a free and open-source URL shortening service
|
|
||||||
designed for simplicity and efficiency.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ToastContainer />
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue