Skip to content
Open

Auth #11

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ model Ticket {
updatedAt DateTime @updatedAt
updates Update[]
comments Comment[]
favorites Favorite[]
}


Expand All @@ -45,6 +46,13 @@ model Comment {
ticket Ticket @relation(fields: [ticket_id], references: [ticket_id])
}

model Favorite {
favorite_id Int @id @default(autoincrement())
userId String
ticketId Int
ticket Ticket @relation(fields: [ticketId], references: [ticket_id], onDelete: Cascade)
@@unique([userId, ticketId]) // Ensure each user can only favorite a ticket once
}

// model PublicMessages {
// message_id Int @id @default(autoincrement())
Expand Down
39 changes: 27 additions & 12 deletions src/app/_components/createupdate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import { api } from "~/trpc/react";


interface CreateUpdateProps {
ticketId: number,
updates: any,
setUpdates: any

ticketId: number;



}


Expand All @@ -19,6 +21,8 @@ const CreateUpdate: React.FC<CreateUpdateProps> = ({ ticketId, updates, setUpdat
const [status, setStatus] = useState<string>('');
const createUpdateMutation = api.update.create.useMutation();

const updateTicketStatusMutation = api.ticket.updateStatus.useMutation();

if (!user?.publicMetadata?.isAdmin) {
return null;
}
Expand All @@ -35,15 +39,26 @@ const CreateUpdate: React.FC<CreateUpdateProps> = ({ ticketId, updates, setUpdat
if (!description || !status) return;

try {
const newupdate = await createUpdateMutation.mutateAsync({
ticket_id: ticketId,
content: description,
status,
});
setUpdates([...updates, newupdate]);
setDescription('');
setStatus('');
// Optionally, trigger a refetch of updates or update the state

await createUpdateMutation.mutateAsync({
ticket_id: ticketId,
content: description,
status,
});



await updateTicketStatusMutation.mutateAsync({
ticket_id: ticketId,
newStatus: status
})

setDescription('');
setStatus('');

// Optionally, trigger a refetch of updates or update the state


} catch (error) {
console.error('Error creating update:', error);
}
Expand Down
115 changes: 100 additions & 15 deletions src/app/_components/ticketlist.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"use client";

import { Ticket, Comment } from '@prisma/client';
import { useQueryClient } from "@tanstack/react-query";
import Link from 'next/link';
import { useEffect, useState } from 'react';
import { api } from '~/trpc/react'; // Import trpc hook
Expand All @@ -26,19 +27,69 @@ interface IUpdate {
}


interface TicketWithFavorite extends Ticket {
isFavorited: boolean;
}


const TicketList = () => {
const {user} = useUser(); // Use Clerk's hook to get user data
const { user, isLoaded, isSignedIn } = useUser();



// const [tickets, setTickets] = useState<Ticket[]>([]);
const [tickets, setTickets] = useState<TicketWithFavorite[]>([]);
const [selectedTicket, setSelectedTicket] = useState<TicketWithFavorite | null>(null);
// const { data, isLoading, isError, error } = api.ticket.getTickets.useQuery();
const queryClient = useQueryClient();

const [tickets, setTickets] = useState<Ticket[]>([]);
const { data, isLoading, isError, error } = api.ticket.getLatest.useQuery();


const [selectedTicket, setSelectedTicket] = useState<Ticket | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const [comments, setComments] = useState<IComment[] | null>([]);
const [updates, setUpdates] = useState<IUpdate[] | null>([]);
const [newComment, setNewComment] = useState('');


// const [data, setData] = useState(null);
// const [data, setData] = useState<TicketWithFavorite[] | null>(null);
// const [isLoading, setIsLoading] = useState(false);
// const [error, setError] = useState<string | null>(null);

// const { data, isLoading, isError, error } = api.ticket.getLatestWithFavorites.useQuery({userId: user?.id.toString()});

const { data, isLoading, isError, error } = api.ticket.getLatestWithFavorites.useQuery(
{ userId: user?.id ?? "" },
{
enabled: isLoaded && !!user?.id, // Only run when `user.id` is defined and `useUser` is fully loaded
}
);

// const { data, isLoading, isError, error } = api.ticket.getLatestWithFavorites.useQuery(
// { userId: user?.id || "" },
// { enabled: !!user } // Only run query if user is available
// );

// const fetchData = async () => {
// setIsLoading(true);
// setError(null);

// try {
// // Create a caller instance to fetch data imperatively
// const caller = api.ticket.getLatestWithFavorites.createCaller();
// const response = await caller({ userId: user?.id || "" });
// setData(response || []); // Update data state with the fetched response or an empty array
// } catch (err) {
// setError("Failed to fetch data");
// console.error("Error fetching data:", err);
// } finally {
// setIsLoading(false);
// }
// };



const toggleFavoriteMutation = api.ticket.toggleFavorite.useMutation();

const handleCommentChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setNewComment(e.target.value);
};
Expand All @@ -58,14 +109,16 @@ const TicketList = () => {
// fullname: z.string().min(1),
// imageUrl: z.string().min(1)

console.log({
ticket_id: selectedTicket.ticket_id,
content: newComment,
user_id: user!.id,
fullname: (user?.firstName ?? '') + " " + (user?.lastName ?? ''),
imageUrl: user!.imageUrl
})
const newcommentfromdb = await createCommentMutation.mutateAsync({

// console.log({
// ticket_id: selectedTicket.ticket_id,
// content: newComment,
// user_id: user!.id,
// fullname: (user?.firstName ?? '') + " " + (user?.lastName ?? ''),
// imageUrl: user!.imageUrl
// })
await createCommentMutation.mutateAsync({

ticket_id: selectedTicket.ticket_id,
content: newComment,
user_id: user!.id,
Expand Down Expand Up @@ -98,8 +151,18 @@ const TicketList = () => {
}
}, [data]);

const toggleFavorite = async (ticketId: number) => {
if (!user) return;
await toggleFavoriteMutation.mutateAsync({
ticketId,
userId: user.id,
});

const openModal = (ticket: Ticket) => {

};


const openModal = (ticket: TicketWithFavorite) => {
setSelectedTicket(ticket);
// @ts-expect-error: just forget about it
setComments(ticket.comments as IComment[] | null);
Expand Down Expand Up @@ -133,6 +196,28 @@ const TicketList = () => {
className="bg-white p-6 rounded-lg shadow-lg hover:shadow-xl transition-shadow duration-300"
onClick={() => openModal(ticket)}
>
<div className="flex justify-between items-center">
<h3 className="text-xl font-semibold text-gray-800 mb-2">{ticket.title}</h3>
<button
onClick={async (e) => {
e.stopPropagation(); // Prevents modal from opening when toggling favorite

try {
await toggleFavorite(ticket.ticket_id);
} catch (error) {
console.error("Error toggling favorite:", error);
}
// toggleFavorite(ticket.ticket_id);
}}
className='text-2xl'
>
{ticket.isFavorited ? (
<span className="text-yellow-500">★</span> // Filled star for favorited
) : (
<span className="text-gray-300">☆</span> // Outline star for not favorited
)}
</button>
</div>
<h3 className="text-xl font-semibold text-gray-800 mb-2">{ticket.title}</h3>
<p className="text-gray-600 text-sm mb-4 line-clamp-3">{ticket.content}</p>
<div className="text-sm text-gray-500">
Expand Down Expand Up @@ -175,7 +260,7 @@ const TicketList = () => {
<div>
<CreateUpdate ticketId={selectedTicket.ticket_id} setUpdates={setUpdates} updates={updates}/>
<div className="flex flex-col items-center">
{updates?.sort((a,b) => new Date(b.createdAt).getTime()- new Date(a.createdAt).getTime()).map((update: IUpdate) => (
{updates?.sort((a,b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()).map((update: IUpdate) => (
<div key={update.update_id} className="flex flex-col items-center">
<Update key={update.update_id} description={update.content} date={update.createdAt.toString()} status={update.status} />
<svg key={1000 - update.update_id} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-6 mt-2">
Expand Down
18 changes: 16 additions & 2 deletions src/app/create-ticket/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,29 @@ import { auth } from '@clerk/nextjs/server';
import { useUser } from '@clerk/nextjs'; // Import the Clerk hook
import { useState } from 'react';
import { api } from '~/trpc/react'; // Import the trpc hook
import { useRouter } from 'next/navigation';
import { useQueryClient } from '@tanstack/react-query';

const CreateTicketPage = () => {
const { user, isLoaded, isSignedIn } = useUser(); // Clerk hook to get user data
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
const router = useRouter(); // Initialize the router
const queryClient = useQueryClient(); // Initialize query client


// Get the mutate function for creating a ticket
const createTicketMutation = api.ticket.create.useMutation();
const createTicketMutation = api.ticket.create.useMutation({
onSuccess: () => {
// queryClient.invalidateQueries(api.ticket.getTickets.getQueryKey());
router.push('/');
},
onError: (error) => {
// Optionally handle errors here
console.error('Error creating ticket:', error);
alert('Failed to create ticket');
},
});

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
Expand All @@ -32,7 +47,6 @@ const CreateTicketPage = () => {
fullname: (user?.firstName ?? '') + " " + (user?.lastName ?? ''),
imageUrl: user?.imageUrl
});
alert('Ticket created successfully!');
} catch (error) {
console.error('Error creating ticket:', error);
alert('Failed to create ticket');
Expand Down
81 changes: 81 additions & 0 deletions src/server/api/routers/ticket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,24 @@ export const ticketRouter = createTRPCRouter({
},
});
}),
updateStatus: publicProcedure
.input(
z.object({
ticket_id: z.number().int().positive(),
newStatus: z.string().min(1),
})
)
.mutation(async ({ ctx, input }) => {
const { ticket_id, newStatus } = input;

// Update the ticket's status
const updatedTicket = await ctx.db.ticket.update({
where: { ticket_id },
data: { status: newStatus },
});

return updatedTicket;
}),

getLatest: publicProcedure.query(async ({ ctx }) => {
const tickets = await ctx.db.ticket.findMany({
Expand All @@ -41,6 +59,69 @@ export const ticketRouter = createTRPCRouter({
});
return ticket ?? null;
}),
getLatestWithFavorites: publicProcedure
.input(z.object({ userId: z.string().nonempty("User ID is required") }))
.query(async ({ ctx, input }) => {
const { userId } = input;

// Fetch the latest tickets with their favorite status for the user
const tickets = await ctx.db.ticket.findMany({
orderBy: { createdAt: "desc" },
include: {
favorites: {
where: { userId }, // Check if the user has favorited this ticket
select: { favorite_id: true }, // Only select the favorite ID to determine if it's favorited
},
comments: true,
updates: true
},
});

// Map each ticket to include an `isFavorited` boolean based on the favorites array
return tickets.map(ticket => ({
...ticket,
isFavorited: ticket.favorites.length > 0, // If there are any favorites, the user has favorited this ticket
}));
}),
toggleFavorite: publicProcedure
.input(
z.object({
ticketId: z.number().int(),
userId: z.string().nonempty("User ID is required"),
})
)
.mutation(async ({ ctx, input }) => {
const { ticketId, userId } = input;

// Check if a favorite already exists for this user and ticket
const existingFavorite = await ctx.db.favorite.findUnique({
where: {
userId_ticketId: {
userId,
ticketId,
},
},
});

if (existingFavorite) {
// If it exists, delete the favorite (unfavorite the ticket)
await ctx.db.favorite.delete({
where: {
favorite_id: existingFavorite.favorite_id,
},
});
return { isFavorited: false }; // Return `isFavorited` status as false
} else {
// If it doesn't exist, create a new favorite
await ctx.db.favorite.create({
data: {
userId,
ticketId,
},
});
return { isFavorited: true }; // Return `isFavorited` status as true
}
}),
// getById: publicProcedure
// .input(z.object({ ticketId: z.string() })) // Validate ticketId parameter
// .query(async ({ ctx, input }) => {
Expand Down