Skip to content
Open
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
6 changes: 6 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import {
TechStackListPage,
TechStackCreatePage,
TechStackEditPage,
TestimonialListPage,
TestimonialCreatePage,
TestimonialEditPage,
LoginPage,
// RegisterPage, // Keep this commented out if you aren't using it yet
} from "@/pages";
Expand Down Expand Up @@ -42,6 +45,9 @@ function App() {
<Route path="tech-stack" element={<TechStackListPage />} />
<Route path="tech-stack/create" element={<TechStackCreatePage />} />
<Route path="tech-stack/:id/edit" element={<TechStackEditPage />} />
<Route path="testimonials" element={<TestimonialListPage />} />
<Route path="testimonials/create" element={<TestimonialCreatePage />} />
<Route path="testimonials/:id/edit" element={<TestimonialEditPage />} />
</Route>
</Routes>
</Router>
Expand Down
16 changes: 16 additions & 0 deletions src/components/layout/AdminLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
User,
Briefcase,
Layers,
MessageSquareQuote,
} from "lucide-react";
import { signOut } from "@/lib/auth-client";
import { useAuth } from "@/contexts/AuthContext";
Expand Down Expand Up @@ -98,6 +99,21 @@ const AdminLayout = () => {
Tags
</NavLink>
</li>
<li>
<NavLink
to="/testimonials"
className={({ isActive }) =>
`flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
isActive
? "bg-purple-100 text-purple-700"
: "text-gray-600 hover:bg-gray-100 hover:text-gray-900"
}`
}
>
<MessageSquareQuote className="w-4 h-4" />
Testimonials
</NavLink>
</li>
<li>
<NavLink
to="/settings"
Expand Down
2 changes: 1 addition & 1 deletion src/contexts/AuthContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,4 @@ export function useAuth() {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
}
}
106 changes: 106 additions & 0 deletions src/hooks/useTestimonials.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { apiService } from "@/services/api";
import type {
CreateTestimonialRequest,
UpdateTestimonialRequest,
} from "@/services/api/types";

// Query keys
export const testimonialKeys = {
all: ["testimonials"] as const,
lists: () => [...testimonialKeys.all, "list"] as const,
details: () => [...testimonialKeys.all, "detail"] as const,
detail: (id: number) => [...testimonialKeys.details(), id] as const,
};

// Fetch all testimonials
export const useTestimonials = () => {
return useQuery({
queryKey: testimonialKeys.lists(),
queryFn: async () => {
const response = await apiService.getAllTestimonials();
if (!response.success) {
throw new Error(response.error || "Failed to fetch testimonials");
}
return response.data || [];
},
});
};

// Fetch single testimonial
export const useTestimonial = (id: number) => {
return useQuery({
queryKey: testimonialKeys.detail(id),
queryFn: async () => {
const response = await apiService.getTestimonialById(id);
if (!response.success) {
throw new Error(response.error || "Failed to fetch testimonial");
}
return response.data;
},
enabled: !!id && id > 0,
});
};

// Create testimonial mutation
export const useCreateTestimonial = () => {
const queryClient = useQueryClient();

return useMutation({
mutationFn: async (data: CreateTestimonialRequest) => {
const response = await apiService.createTestimonial(data);
if (!response.success) {
throw new Error(response.error || "Failed to create testimonial");
}
return response.data!;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: testimonialKeys.lists() });
},
});
};

// Update testimonial mutation
export const useUpdateTestimonial = () => {
const queryClient = useQueryClient();

return useMutation({
mutationFn: async ({
id,
data,
}: {
id: number;
data: UpdateTestimonialRequest;
}) => {
const response = await apiService.updateTestimonial(id, data);
if (!response.success) {
throw new Error(response.error || "Failed to update testimonial");
}
return response.data!;
},
onSuccess: (_data, variables) => {
queryClient.invalidateQueries({ queryKey: testimonialKeys.lists() });
queryClient.invalidateQueries({
queryKey: testimonialKeys.detail(variables.id),
});
},
});
};

// Delete testimonial mutation
export const useDeleteTestimonial = () => {
const queryClient = useQueryClient();

return useMutation({
mutationFn: async (id: number) => {
const response = await apiService.deleteTestimonial(id);
if (!response.success) {
throw new Error(response.error || "Failed to delete testimonial");
}
return response.data!;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: testimonialKeys.lists() });
},
});
};
23 changes: 23 additions & 0 deletions src/pages/BlogCreatePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,34 @@ const BlogCreatePage = () => {
alert("Please enter a blog title");
return false;
}
if (formData.title.length > 150) {
alert("Blog title cannot exceed 150 characters");
return false;
}

if (!formData.author.trim()) {
alert("Please enter the author name");
return false;
}
if (formData.author.length > 50) {
alert("Author name cannot exceed 50 characters");
return false;
}

if (formData.excerpt && formData.excerpt.length > 300) {
alert("Excerpt cannot exceed 300 characters");
return false;
}

if (!formData.time_read.trim()) {
alert("Please enter the reading time");
return false;
}
if (formData.time_read.length > 50) {
alert("Reading time cannot exceed 50 characters");
return false;
}

if (
!formData.content ||
!formData.content.content ||
Expand All @@ -107,14 +127,17 @@ const BlogCreatePage = () => {
alert("Please add content to your blog");
return false;
}

if (!formData.thumbnail) {
alert("Please upload a thumbnail image");
return false;
}

if (!formData.tag_id) {
alert("Please select a tag");
return false;
}

return true;
};

Expand Down
22 changes: 22 additions & 0 deletions src/pages/BlogEditPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -174,14 +174,34 @@ const BlogEditPage = () => {
alert("Please enter a blog title");
return false;
}
if (formData.title.length > 150) {
alert("Blog title cannot exceed 150 characters");
return false;
}

if (!formData.author.trim()) {
alert("Please enter the author name");
return false;
}
if (formData.author.length > 50) {
alert("Author name cannot exceed 50 characters");
return false;
}

if (formData.excerpt && formData.excerpt.length > 300) {
alert("Excerpt cannot exceed 300 characters");
return false;
}

if (!formData.time_read.trim()) {
alert("Please enter the reading time");
return false;
}
if (formData.time_read.length > 50) {
alert("Reading time cannot exceed 50 characters");
return false;
}

if (
!formData.content ||
!formData.content.content ||
Expand All @@ -190,10 +210,12 @@ const BlogEditPage = () => {
alert("Please add content to your blog");
return false;
}

if (!formData.tag_id) {
alert("Please select a tag");
return false;
}

return true;
};

Expand Down
2 changes: 1 addition & 1 deletion src/pages/LoginPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,4 @@ export default function LoginPage() {
</Card>
</div>
);
}
}
32 changes: 30 additions & 2 deletions src/pages/ProjectCreatePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,22 +110,50 @@ const ProjectCreatePage = () => {
alert("Please enter a project title");
return false;
}
if (formData.title.length > 100) {
alert("Project title cannot exceed 100 characters");
return false;
}

if (!formData.description.trim()) {
alert("Please enter a project description");
return false;
}

if (formData.description.length > 2000) {
alert("Description is too long (max 2000 characters)");
return false;
}

if (!formData.owner.trim()) {
alert("Please enter the project owner");
return false;
}
if (!formData.category.trim()) {
if (formData.owner.length > 50) {
alert("Owner name cannot exceed 50 characters");
return false;
}

if (formData.category.trim() === "") {
alert("Please select a category");
return false;
}
if (!formData.scope.trim()) {

if (formData.scope.trim() === "") {
alert("Please select a scope");
return false;
}

if (formData.url && formData.url.length > 255) {
alert("Project URL is too long (max 255 characters)");
return false;
}

if (formData.testimonial && formData.testimonial.length > 500) {
alert("Testimonial cannot exceed 500 characters");
return false;
}

return true;
};

Expand Down
27 changes: 27 additions & 0 deletions src/pages/ProjectEditPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,22 +151,49 @@ const ProjectEditPage = () => {
alert("Please enter a project title");
return false;
}
if (formData.title.length > 100) {
alert("Project title cannot exceed 100 characters");
return false;
}

if (!formData.description.trim()) {
alert("Please enter a project description");
return false;
}
if (formData.description.length > 2000) {
alert("Description is too long (max 2000 characters)");
return false;
}

if (!formData.owner.trim()) {
alert("Please enter the project owner");
return false;
}
if (formData.owner.length > 50) {
alert("Owner name cannot exceed 50 characters");
return false;
}

if (!formData.category.trim()) {
alert("Please select a category");
return false;
}

if (!formData.scope.trim()) {
alert("Please select a scope");
return false;
}

if (formData.url && formData.url.length > 255) {
alert("Project URL is too long (max 255 characters)");
return false;
}

if (formData.testimonial && formData.testimonial.length > 500) {
alert("Testimonial cannot exceed 500 characters");
return false;
}

return true;
};

Expand Down
18 changes: 18 additions & 0 deletions src/pages/TechStackCreatePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,24 @@ const TechStackCreatePage = () => {
alert("Please enter a tech stack name");
return false;
}
if (formData.tech_stack_name.length > 50) {
alert("Tech stack name cannot exceed 50 characters");
return false;
}

if (
formData.tech_stack_description &&
formData.tech_stack_description.length > 500
) {
alert("Description cannot exceed 500 characters");
return false;
}

if (!formData.icon_url) {
alert("Please upload an icon");
return false;
}

return true;
};

Expand Down
Loading