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-dom": "^18.3.1",
|
||||
"axios": "^1.7.9",
|
||||
"framer-motion": "^11.15.0",
|
||||
"react": "^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-scripts": "5.0.1",
|
||||
"react-toastify": "^11.0.1",
|
||||
|
@ -7850,6 +7851,32 @@
|
|||
"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": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||
|
@ -10668,6 +10695,16 @@
|
|||
"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": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
|
@ -12928,9 +12965,9 @@
|
|||
"integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg=="
|
||||
},
|
||||
"node_modules/react-icons": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.3.0.tgz",
|
||||
"integrity": "sha512-DnUk8aFbTyQPSkCfF8dbX6kQjXA9DktMeJqfjrg6cK9vwQVMxmcA3BfP4QoiztVmEHtwlTgLFsPuH2NskKT6eg==",
|
||||
"version": "5.4.0",
|
||||
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.4.0.tgz",
|
||||
"integrity": "sha512-7eltJxgVt7X64oHh6wSWNwwbKTCtMfK35hcjvJS0yxEAhPM8oUKdS3+kqaW1vicIltw+kR2unHaa12S9pPALoQ==",
|
||||
"peerDependencies": {
|
||||
"react": "*"
|
||||
}
|
||||
|
|
|
@ -11,9 +11,10 @@
|
|||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"axios": "^1.7.9",
|
||||
"framer-motion": "^11.15.0",
|
||||
"react": "^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-scripts": "5.0.1",
|
||||
"react-toastify": "^11.0.1",
|
||||
|
|
|
@ -1,25 +1,13 @@
|
|||
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";
|
||||
|
||||
// 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";
|
||||
import AppContent from "./AppContent";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="App">
|
||||
<Router>
|
||||
<Navbar />
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/about" element={<About />} />
|
||||
<Route path="/contact" element={<Contact />} />
|
||||
</Routes>
|
||||
<Footer />
|
||||
<AppContent />
|
||||
</Router>
|
||||
</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;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.logo a {
|
||||
|
|
|
@ -23,7 +23,7 @@ function Navbar() {
|
|||
<Link to="/">Home</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/about">About</Link>
|
||||
<Link to="/features">Features</Link>
|
||||
</li>
|
||||
<li>
|
||||
<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;
|
||||
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 axios from "axios";
|
||||
import { ToastContainer, toast } from "react-toastify";
|
||||
import "react-toastify/dist/ReactToastify.css";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import URLShortener from "../../components/URLShortener/URLShortener";
|
||||
|
||||
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 (
|
||||
<div className="home-page">
|
||||
<div className="home-container">
|
||||
<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 && (
|
||||
<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>
|
||||
<motion.div
|
||||
className="home-page"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 1.5, ease: "easeInOut" }}
|
||||
>
|
||||
<URLShortener />
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue