Add edit link functionality
This commit is contained in:
parent
613799dc78
commit
b514d490d2
7 changed files with 359 additions and 38 deletions
|
@ -58,6 +58,12 @@ export const getAllLinks = async () => {
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const editLink = async (id: number, data: Partial<CreateLinkRequest>) => {
|
||||||
|
const response = await api.patch<Link>(`/links/${id}`, data);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export const deleteLink = async (id: number) => {
|
export const deleteLink = async (id: number) => {
|
||||||
await api.delete(`/links/${id}`);
|
await api.delete(`/links/${id}`);
|
||||||
};
|
};
|
||||||
|
|
139
frontend/src/components/EditModal.tsx
Normal file
139
frontend/src/components/EditModal.tsx
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
// src/components/EditModal.tsx
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import * as z from 'zod';
|
||||||
|
import { Link } from '../types/api';
|
||||||
|
import { editLink } from '../api/client';
|
||||||
|
import { useToast } from '@/hooks/use-toast';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogFooter,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from '@/components/ui/form';
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
url: z
|
||||||
|
.string()
|
||||||
|
.min(1, 'URL is required')
|
||||||
|
.url('Must be a valid URL')
|
||||||
|
.refine((val) => val.startsWith('http://') || val.startsWith('https://'), {
|
||||||
|
message: 'URL must start with http:// or https://',
|
||||||
|
}),
|
||||||
|
custom_code: z
|
||||||
|
.string()
|
||||||
|
.regex(/^[a-zA-Z0-9_-]{1,32}$/, {
|
||||||
|
message:
|
||||||
|
'Custom code must be 1-32 characters and contain only letters, numbers, underscores, and hyphens',
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
interface EditModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
link: Link;
|
||||||
|
onSuccess: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EditModal({ isOpen, onClose, link, onSuccess }: EditModalProps) {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
url: link.original_url,
|
||||||
|
custom_code: link.short_code,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = async (values: z.infer<typeof formSchema>) => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
await editLink(link.id, values);
|
||||||
|
toast({
|
||||||
|
description: 'Link updated successfully',
|
||||||
|
});
|
||||||
|
onSuccess();
|
||||||
|
onClose();
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const error = err as { response?: { data?: { error?: string } } };
|
||||||
|
toast({
|
||||||
|
variant: 'destructive',
|
||||||
|
title: 'Error',
|
||||||
|
description: error.response?.data?.error || 'Failed to update link',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Edit Link</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="url"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Destination URL</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="https://example.com" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="custom_code"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Short Code</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="custom-code" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={onClose}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={loading}>
|
||||||
|
{loading ? 'Saving...' : 'Save Changes'}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import { useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import { Link } from '../types/api'
|
import { Link } from '../types/api'
|
||||||
import { getAllLinks, deleteLink } from '../api/client'
|
import { getAllLinks, deleteLink } from '../api/client'
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
|
@ -12,7 +12,7 @@ import {
|
||||||
} from "@/components/ui/table"
|
} from "@/components/ui/table"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { useToast } from "@/hooks/use-toast"
|
import { useToast } from "@/hooks/use-toast"
|
||||||
import { Copy, Trash2, BarChart2 } from "lucide-react"
|
import { Copy, Trash2, BarChart2, Pencil } from "lucide-react"
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
@ -23,6 +23,7 @@ import {
|
||||||
} from "@/components/ui/dialog"
|
} from "@/components/ui/dialog"
|
||||||
|
|
||||||
import { StatisticsModal } from "./StatisticsModal"
|
import { StatisticsModal } from "./StatisticsModal"
|
||||||
|
import { EditModal } from './EditModal'
|
||||||
|
|
||||||
interface LinkListProps {
|
interface LinkListProps {
|
||||||
refresh?: number;
|
refresh?: number;
|
||||||
|
@ -39,27 +40,32 @@ export function LinkList({ refresh = 0 }: LinkListProps) {
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
linkId: null,
|
linkId: null,
|
||||||
});
|
});
|
||||||
|
const [editModal, setEditModal] = useState<{ isOpen: boolean; link: Link | null }>({
|
||||||
|
isOpen: false,
|
||||||
|
link: null,
|
||||||
|
});
|
||||||
const { toast } = useToast()
|
const { toast } = useToast()
|
||||||
|
|
||||||
const fetchLinks = async () => {
|
const fetchLinks = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
const data = await getAllLinks()
|
const data = await getAllLinks()
|
||||||
setLinks(data)
|
setLinks(data)
|
||||||
} catch (err) {
|
} catch (err: unknown) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
|
||||||
toast({
|
toast({
|
||||||
title: "Error",
|
title: "Error",
|
||||||
description: "Failed to load links",
|
description: `Failed to load links: ${errorMessage}`,
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}, [toast, setLinks, setLoading])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchLinks()
|
fetchLinks()
|
||||||
}, [refresh]) // Re-fetch when refresh counter changes
|
}, [fetchLinks, refresh]) // Re-fetch when refresh counter changes
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
if (!deleteModal.linkId) return
|
if (!deleteModal.linkId) return
|
||||||
|
@ -71,10 +77,11 @@ export function LinkList({ refresh = 0 }: LinkListProps) {
|
||||||
toast({
|
toast({
|
||||||
description: "Link deleted successfully",
|
description: "Link deleted successfully",
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err: unknown) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
|
||||||
toast({
|
toast({
|
||||||
title: "Error",
|
title: "Error",
|
||||||
description: "Failed to delete link",
|
description: `Failed to delete link: ${errorMessage}`,
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -85,13 +92,13 @@ export function LinkList({ refresh = 0 }: LinkListProps) {
|
||||||
const baseUrl = window.location.origin
|
const baseUrl = window.location.origin
|
||||||
navigator.clipboard.writeText(`${baseUrl}/${shortCode}`)
|
navigator.clipboard.writeText(`${baseUrl}/${shortCode}`)
|
||||||
toast({
|
toast({
|
||||||
description: (
|
description: (
|
||||||
<>
|
<>
|
||||||
Link copied to clipboard
|
Link copied to clipboard
|
||||||
<br />
|
<br />
|
||||||
You can add ?source=TextHere to the end of the link to track the source of clicks
|
You can add ?source=TextHere to the end of the link to track the source of clicks
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,6 +134,7 @@ export function LinkList({ refresh = 0 }: LinkListProps) {
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="rounded-md border">
|
<div className="rounded-md border">
|
||||||
|
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
@ -134,7 +142,7 @@ export function LinkList({ refresh = 0 }: LinkListProps) {
|
||||||
<TableHead className="hidden md:table-cell">Original URL</TableHead>
|
<TableHead className="hidden md:table-cell">Original URL</TableHead>
|
||||||
<TableHead>Clicks</TableHead>
|
<TableHead>Clicks</TableHead>
|
||||||
<TableHead className="hidden md:table-cell">Created</TableHead>
|
<TableHead className="hidden md:table-cell">Created</TableHead>
|
||||||
<TableHead>Actions</TableHead>
|
<TableHead className="w-[1%] whitespace-nowrap pr-4">Actions</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
|
@ -148,8 +156,8 @@ export function LinkList({ refresh = 0 }: LinkListProps) {
|
||||||
<TableCell className="hidden md:table-cell">
|
<TableCell className="hidden md:table-cell">
|
||||||
{new Date(link.created_at).toLocaleDateString()}
|
{new Date(link.created_at).toLocaleDateString()}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell className="p-2 pr-4">
|
||||||
<div className="flex gap-2">
|
<div className="flex items-center gap-1">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
|
@ -168,6 +176,15 @@ export function LinkList({ refresh = 0 }: LinkListProps) {
|
||||||
<BarChart2 className="h-4 w-4" />
|
<BarChart2 className="h-4 w-4" />
|
||||||
<span className="sr-only">View statistics</span>
|
<span className="sr-only">View statistics</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8"
|
||||||
|
onClick={() => setEditModal({ isOpen: true, link })}
|
||||||
|
>
|
||||||
|
<Pencil className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Edit Link</span>
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
|
@ -191,6 +208,14 @@ export function LinkList({ refresh = 0 }: LinkListProps) {
|
||||||
onClose={() => setStatsModal({ isOpen: false, linkId: null })}
|
onClose={() => setStatsModal({ isOpen: false, linkId: null })}
|
||||||
linkId={statsModal.linkId!}
|
linkId={statsModal.linkId!}
|
||||||
/>
|
/>
|
||||||
|
{editModal.link && (
|
||||||
|
<EditModal
|
||||||
|
isOpen={editModal.isOpen}
|
||||||
|
onClose={() => setEditModal({ isOpen: false, link: null })}
|
||||||
|
link={editModal.link}
|
||||||
|
onSuccess={fetchLinks}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
|
@ -31,7 +31,7 @@ const CustomTooltip = ({
|
||||||
label,
|
label,
|
||||||
}: {
|
}: {
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
payload?: any[];
|
payload?: { value: number; payload: EnhancedClickStats }[];
|
||||||
label?: string;
|
label?: string;
|
||||||
}) => {
|
}) => {
|
||||||
if (active && payload && payload.length > 0) {
|
if (active && payload && payload.length > 0) {
|
||||||
|
@ -81,12 +81,12 @@ export function StatisticsModal({ isOpen, onClose, linkId }: StatisticsModalProp
|
||||||
|
|
||||||
setClicksOverTime(enhancedClicksData);
|
setClicksOverTime(enhancedClicksData);
|
||||||
setSourcesData(sourcesData);
|
setSourcesData(sourcesData);
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
console.error("Failed to fetch statistics:", error);
|
console.error("Failed to fetch statistics:", error);
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
title: "Error",
|
title: "Error",
|
||||||
description: error.response?.data || "Failed to load statistics",
|
description: error instanceof Error ? error.message : "Failed to load statistics",
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
|
@ -3,19 +3,32 @@ import react from '@vitejs/plugin-react'
|
||||||
import tailwindcss from '@tailwindcss/vite'
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
|
||||||
export default defineConfig(() => ({
|
export default defineConfig(({ command }) => {
|
||||||
plugins: [react(), tailwindcss()],
|
if (command === 'serve') { //command == 'dev'
|
||||||
/*server: {
|
return {
|
||||||
proxy: {
|
server: {
|
||||||
'/api': {
|
proxy: {
|
||||||
target: process.env.VITE_API_URL || 'http://localhost:8080',
|
'/api': {
|
||||||
changeOrigin: true,
|
target: process.env.VITE_API_URL || 'http://localhost:8080',
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
plugins: [react(), tailwindcss()],
|
||||||
},*/
|
resolve: {
|
||||||
resolve: {
|
alias: {
|
||||||
alias: {
|
"@": path.resolve(__dirname, "./src"),
|
||||||
"@": path.resolve(__dirname, "./src"),
|
},
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
}))
|
} else { //command === 'build'
|
||||||
|
return {
|
||||||
|
plugins: [react(), tailwindcss()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@": path.resolve(__dirname, "./src"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
139
src/handlers.rs
139
src/handlers.rs
|
@ -457,12 +457,149 @@ pub async fn login(
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn edit_link(
|
||||||
|
state: web::Data<AppState>,
|
||||||
|
user: AuthenticatedUser,
|
||||||
|
path: web::Path<i32>,
|
||||||
|
payload: web::Json<CreateLink>,
|
||||||
|
) -> Result<impl Responder, AppError> {
|
||||||
|
let link_id: i32 = path.into_inner();
|
||||||
|
|
||||||
|
// Validate the new URL if provided
|
||||||
|
validate_url(&payload.url)?;
|
||||||
|
|
||||||
|
// Validate custom code if provided
|
||||||
|
if let Some(ref custom_code) = payload.custom_code {
|
||||||
|
validate_custom_code(custom_code)?;
|
||||||
|
|
||||||
|
// Check if the custom code is already taken by another link
|
||||||
|
let existing_link = match &state.db {
|
||||||
|
DatabasePool::Postgres(pool) => {
|
||||||
|
sqlx::query_as::<_, Link>("SELECT * FROM links WHERE short_code = $1 AND id != $2")
|
||||||
|
.bind(custom_code)
|
||||||
|
.bind(link_id)
|
||||||
|
.fetch_optional(pool)
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
DatabasePool::Sqlite(pool) => {
|
||||||
|
sqlx::query_as::<_, Link>("SELECT * FROM links WHERE short_code = ?1 AND id != ?2")
|
||||||
|
.bind(custom_code)
|
||||||
|
.bind(link_id)
|
||||||
|
.fetch_optional(pool)
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if existing_link.is_some() {
|
||||||
|
return Err(AppError::InvalidInput(
|
||||||
|
"Custom code already taken".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the link
|
||||||
|
let updated_link = match &state.db {
|
||||||
|
DatabasePool::Postgres(pool) => {
|
||||||
|
let mut tx = pool.begin().await?;
|
||||||
|
|
||||||
|
// First verify the link belongs to the user
|
||||||
|
let link =
|
||||||
|
sqlx::query_as::<_, Link>("SELECT * FROM links WHERE id = $1 AND user_id = $2")
|
||||||
|
.bind(link_id)
|
||||||
|
.bind(user.user_id)
|
||||||
|
.fetch_optional(&mut *tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if link.is_none() {
|
||||||
|
return Err(AppError::NotFound);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the link
|
||||||
|
let updated = sqlx::query_as::<_, Link>(
|
||||||
|
r#"
|
||||||
|
UPDATE links
|
||||||
|
SET
|
||||||
|
original_url = $1,
|
||||||
|
short_code = COALESCE($2, short_code)
|
||||||
|
WHERE id = $3 AND user_id = $4
|
||||||
|
RETURNING *
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(&payload.url)
|
||||||
|
.bind(&payload.custom_code)
|
||||||
|
.bind(link_id)
|
||||||
|
.bind(user.user_id)
|
||||||
|
.fetch_one(&mut *tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// If source is provided, add a click record
|
||||||
|
if let Some(ref source) = payload.source {
|
||||||
|
sqlx::query("INSERT INTO clicks (link_id, source) VALUES ($1, $2)")
|
||||||
|
.bind(link_id)
|
||||||
|
.bind(source)
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.commit().await?;
|
||||||
|
updated
|
||||||
|
}
|
||||||
|
DatabasePool::Sqlite(pool) => {
|
||||||
|
let mut tx = pool.begin().await?;
|
||||||
|
|
||||||
|
// First verify the link belongs to the user
|
||||||
|
let link =
|
||||||
|
sqlx::query_as::<_, Link>("SELECT * FROM links WHERE id = ?1 AND user_id = ?2")
|
||||||
|
.bind(link_id)
|
||||||
|
.bind(user.user_id)
|
||||||
|
.fetch_optional(&mut *tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if link.is_none() {
|
||||||
|
return Err(AppError::NotFound);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the link
|
||||||
|
let updated = sqlx::query_as::<_, Link>(
|
||||||
|
r#"
|
||||||
|
UPDATE links
|
||||||
|
SET
|
||||||
|
original_url = ?1,
|
||||||
|
short_code = COALESCE(?2, short_code)
|
||||||
|
WHERE id = ?3 AND user_id = ?4
|
||||||
|
RETURNING *
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(&payload.url)
|
||||||
|
.bind(&payload.custom_code)
|
||||||
|
.bind(link_id)
|
||||||
|
.bind(user.user_id)
|
||||||
|
.fetch_one(&mut *tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// If source is provided, add a click record
|
||||||
|
if let Some(ref source) = payload.source {
|
||||||
|
sqlx::query("INSERT INTO clicks (link_id, source) VALUES (?1, ?2)")
|
||||||
|
.bind(link_id)
|
||||||
|
.bind(source)
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.commit().await?;
|
||||||
|
updated
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(updated_link))
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn delete_link(
|
pub async fn delete_link(
|
||||||
state: web::Data<AppState>,
|
state: web::Data<AppState>,
|
||||||
user: AuthenticatedUser,
|
user: AuthenticatedUser,
|
||||||
path: web::Path<i32>,
|
path: web::Path<i32>,
|
||||||
) -> Result<impl Responder, AppError> {
|
) -> Result<impl Responder, AppError> {
|
||||||
let link_id = path.into_inner();
|
let link_id: i32 = path.into_inner();
|
||||||
|
|
||||||
match &state.db {
|
match &state.db {
|
||||||
DatabasePool::Postgres(pool) => {
|
DatabasePool::Postgres(pool) => {
|
||||||
|
|
|
@ -70,6 +70,7 @@ async fn main() -> Result<()> {
|
||||||
"/links/{id}/sources",
|
"/links/{id}/sources",
|
||||||
web::get().to(handlers::get_link_sources),
|
web::get().to(handlers::get_link_sources),
|
||||||
)
|
)
|
||||||
|
.route("/links/{id}", web::patch().to(handlers::edit_link))
|
||||||
.route("/auth/register", web::post().to(handlers::register))
|
.route("/auth/register", web::post().to(handlers::register))
|
||||||
.route("/auth/login", web::post().to(handlers::login))
|
.route("/auth/login", web::post().to(handlers::login))
|
||||||
.route(
|
.route(
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue