From 3932b48a643619d00f58767248cf855248bb3857 Mon Sep 17 00:00:00 2001 From: WaveringAna Date: Sat, 25 Jan 2025 23:01:12 -0500 Subject: [PATCH] I LOVE WOMEN --- .gitignore | 1 - .preludeignore | 2 + Cargo.lock | 50 +++++------ Cargo.toml | 4 +- Dockerfile | 45 ++++++++++ frontend/src/App.tsx | 55 ++++++------ frontend/src/components/AuthForms.tsx | 2 +- frontend/src/components/LinkForm.tsx | 93 ++++++++++--------- frontend/src/components/LinkList.tsx | 125 +++++++++++++++----------- src/main.rs | 6 +- 10 files changed, 233 insertions(+), 150 deletions(-) create mode 100644 .preludeignore create mode 100644 Dockerfile diff --git a/.gitignore b/.gitignore index da1a81b..d5621cf 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,3 @@ **/node_modules node_modules .env -.sqlx \ No newline at end of file diff --git a/.preludeignore b/.preludeignore new file mode 100644 index 0000000..08db334 --- /dev/null +++ b/.preludeignore @@ -0,0 +1,2 @@ +.sqlx +.env \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 05f8463..eb85298 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,31 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "SimpleLink" -version = "0.1.0" -dependencies = [ - "actix-cors", - "actix-web", - "anyhow", - "argon2", - "base62", - "chrono", - "clap", - "dotenv", - "jsonwebtoken", - "lazy_static", - "regex", - "serde", - "serde_json", - "sqlx", - "thiserror 1.0.69", - "tokio", - "tracing", - "tracing-subscriber", - "uuid", -] - [[package]] name = "actix-codec" version = "0.5.2" @@ -2039,6 +2014,31 @@ dependencies = [ "time", ] +[[package]] +name = "simplelink" +version = "0.1.0" +dependencies = [ + "actix-cors", + "actix-web", + "anyhow", + "argon2", + "base62", + "chrono", + "clap", + "dotenv", + "jsonwebtoken", + "lazy_static", + "regex", + "serde", + "serde_json", + "sqlx", + "thiserror 1.0.69", + "tokio", + "tracing", + "tracing-subscriber", + "uuid", +] + [[package]] name = "slab" version = "0.4.9" diff --git a/Cargo.toml b/Cargo.toml index f4fc4e9..a8c9acc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,10 @@ [package] -name = "SimpleLink" +name = "simplelink" version = "0.1.0" edition = "2021" [lib] -name = "simple_link" +name = "simplelink" path = "src/lib.rs" [dependencies] diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..75f422a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,45 @@ +# Build stage +FROM rust:latest as builder + +# Install PostgreSQL client libraries and SSL dependencies +RUN apt-get update && \ + apt-get install -y pkg-config libssl-dev libpq-dev && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /usr/src/app + +# Copy manifests first (better layer caching) +COPY Cargo.toml Cargo.lock ./ + +# Copy source code and SQLx prepared queries +COPY src/ src/ +COPY migrations/ migrations/ +COPY .sqlx/ .sqlx/ + +# Build your application +RUN cargo build --release + +# Runtime stage +FROM debian:bookworm-slim + +# Install runtime dependencies +RUN apt-get update && \ + apt-get install -y libpq5 ca-certificates openssl libssl3 && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Copy the binary from builder +COPY --from=builder /usr/src/app/target/release/simplelink /app/simplelink +# Copy migrations folder for SQLx +COPY --from=builder /usr/src/app/migrations /app/migrations + +# Expose the port (this is just documentation) +EXPOSE 8080 + +# Set default network configuration +ENV SERVER_HOST=0.0.0.0 +ENV SERVER_PORT=8080 + +# Run the binary +CMD ["./simplelink"] \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b212377..2de0985 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,51 +1,56 @@ import { ThemeProvider } from "@/components/theme-provider" -import { Button } from './components/ui/button' import { LinkForm } from './components/LinkForm' import { LinkList } from './components/LinkList' import { AuthForms } from './components/AuthForms' import { AuthProvider, useAuth } from './context/AuthContext' -import { useState } from 'react' +import { Button } from "@/components/ui/button" import { Toaster } from './components/ui/toaster' +import { ModeToggle } from './components/mode-toggle' +import { useState } from 'react' function AppContent() { const { user, logout } = useAuth() const [refreshCounter, setRefreshCounter] = useState(0) const handleLinkCreated = () => { - // Increment refresh counter to trigger list refresh setRefreshCounter(prev => prev + 1) } return ( -
-
-
-
-

SimpleLink

+
+
+
+

SimpleLink

+
{user ? ( -
-

Welcome, {user.email}

- -
+ ) : ( -
-

A link shortening and tracking service

-
+ A link shortening and tracking service + )} + +
+
+
+ +
+
+
+ {user ? ( + <> + + + + ) : ( + )}
- - {user ? ( - <> - - - - ) : ( - - )}
-
+
) } diff --git a/frontend/src/components/AuthForms.tsx b/frontend/src/components/AuthForms.tsx index c3df9dc..7b9032a 100644 --- a/frontend/src/components/AuthForms.tsx +++ b/frontend/src/components/AuthForms.tsx @@ -56,7 +56,7 @@ export function AuthForms() { return ( - setActiveTab(value)}> + setActiveTab(value as 'login' | 'register')}> Login Register diff --git a/frontend/src/components/LinkForm.tsx b/frontend/src/components/LinkForm.tsx index 40df960..262ef1b 100644 --- a/frontend/src/components/LinkForm.tsx +++ b/frontend/src/components/LinkForm.tsx @@ -2,18 +2,19 @@ import { useState } from 'react' import { useForm } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import * as z from 'zod' -import { CreateLinkRequest, Link } from '../types/api' +import { CreateLinkRequest } from '../types/api' import { createShortLink } from '../api/client' import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { LinkIcon } from "lucide-react" import { Form, FormControl, FormField, - FormItem, FormLabel, FormMessage, } from "@/components/ui/form" -import { Input } from "@/components/ui/input" import { useToast } from "@/hooks/use-toast" const formSchema = z.object({ @@ -29,7 +30,7 @@ const formSchema = z.object({ }) interface LinkFormProps { - onSuccess: (link: Link) => void + onSuccess: () => void; } export function LinkForm({ onSuccess }: LinkFormProps) { @@ -47,9 +48,9 @@ export function LinkForm({ onSuccess }: LinkFormProps) { const onSubmit = async (values: z.infer) => { try { setLoading(true) - const link = await createShortLink(values as CreateLinkRequest) + await createShortLink(values as CreateLinkRequest) form.reset() - onSuccess(link) + onSuccess() // Call the onSuccess callback to trigger refresh toast({ description: "Short link created successfully", }) @@ -65,45 +66,51 @@ export function LinkForm({ onSuccess }: LinkFormProps) { } return ( -
-
- - ( - - URL - - - - - - )} - /> + + + Create Short Link + Enter a URL to generate a shortened link + + + + + ( +
+ URL + +
+ + +
+
+ +
+ )} + /> - ( - - Custom Code (optional) - - - - - - )} - /> + ( +
+ Custom Code (optional) + + + + +
+ )} + /> -
- -
- - -
+ + + +
) -} - +} \ No newline at end of file diff --git a/frontend/src/components/LinkList.tsx b/frontend/src/components/LinkList.tsx index be8353e..b23d200 100644 --- a/frontend/src/components/LinkList.tsx +++ b/frontend/src/components/LinkList.tsx @@ -1,6 +1,7 @@ import { useEffect, useState } from 'react' import { Link } from '../types/api' import { getAllLinks, deleteLink } from '../api/client' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Table, TableBody, @@ -9,6 +10,9 @@ import { TableHeader, TableRow, } from "@/components/ui/table" +import { Button } from "@/components/ui/button" +import { useToast } from "@/hooks/use-toast" +import { Copy, Trash2 } from "lucide-react" import { Dialog, DialogContent, @@ -17,14 +21,12 @@ import { DialogDescription, DialogFooter, } from "@/components/ui/dialog" -import { Button } from "@/components/ui/button" -import { useToast } from "@/hooks/use-toast" interface LinkListProps { - refresh: number + refresh?: number; } -export function LinkList({ refresh }: LinkListProps) { +export function LinkList({ refresh = 0 }: LinkListProps) { const [links, setLinks] = useState([]) const [loading, setLoading] = useState(true) const [deleteModal, setDeleteModal] = useState<{ isOpen: boolean; linkId: number | null }>({ @@ -51,7 +53,7 @@ export function LinkList({ refresh }: LinkListProps) { useEffect(() => { fetchLinks() - }, [refresh]) + }, [refresh]) // Re-fetch when refresh counter changes const handleDelete = async () => { if (!deleteModal.linkId) return @@ -61,8 +63,7 @@ export function LinkList({ refresh }: LinkListProps) { await fetchLinks() setDeleteModal({ isOpen: false, linkId: null }) toast({ - title: "Link deleted", - description: "The link has been successfully deleted.", + description: "Link deleted successfully", }) } catch (err) { toast({ @@ -73,12 +74,19 @@ export function LinkList({ refresh }: LinkListProps) { } } + const handleCopy = (shortCode: string) => { + navigator.clipboard.writeText(`http://localhost:8080/${shortCode}`) + toast({ + description: "Link copied to clipboard", + }) + } + if (loading && !links.length) { return
Loading...
} return ( -
+ <> setDeleteModal({ isOpen: open, linkId: null })}> @@ -98,48 +106,63 @@ export function LinkList({ refresh }: LinkListProps) { - - - - Short Code - Original URL - Clicks - Created - Actions - - - - {links.map((link) => ( - - {link.short_code} - {link.original_url} - {link.clicks} - {new Date(link.created_at).toLocaleDateString()} - -
- - -
-
-
- ))} -
-
-
+ + + Your Links + Manage and track your shortened links + + +
+ + + + Short Code + Original URL + Clicks + Created + Actions + + + + {links.map((link) => ( + + {link.short_code} + + {link.original_url} + + {link.clicks} + + {new Date(link.created_at).toLocaleDateString()} + + +
+ + +
+
+
+ ))} +
+
+
+
+
+ ) } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 01388b7..024c340 100644 --- a/src/main.rs +++ b/src/main.rs @@ -28,7 +28,9 @@ async fn main() -> Result<()> { let state = AppState { db: pool }; - info!("Starting server at http://127.0.0.1:8080"); + let host = std::env::var("SERVER_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()); + let port = std::env::var("SERVER_PORT").unwrap_or_else(|_| "8080".to_string()); + info!("Starting server at http://{}:{}", host, port); // Start HTTP server HttpServer::new(move || { @@ -54,7 +56,7 @@ async fn main() -> Result<()> { }) .workers(2) .backlog(10_000) - .bind("127.0.0.1:8080")? + .bind(format!("{}:{}", host, port))? .run() .await?;