Compare commits

..

No commits in common. "main" and "team" have entirely different histories.
main ... team

19 changed files with 74 additions and 846 deletions

1
.env
View File

@ -1 +0,0 @@
NEXT_PUBLIC_API_BASE_URL=http://localhost:3001/api/

View File

@ -1 +0,0 @@
NEXT_PUBLIC_API_BASE_URL=http://localhost:3000/api/

View File

@ -1,104 +0,0 @@
import { Metadata } from 'next';
import Link from 'next/link';
import React from 'react';
export const metadata: Metadata = {
title: 'Blog',
};
const blogs = [
{
id: 1,
image: '/assets/images/blog/image-1.jpg',
title: 'Excessive sugar is harmful',
description: 'Sugar consumption can have serious effects on your health if taken in excess. Learn how to reduce it.',
author: 'Alma Clark',
profile: '/assets/images/profile-1.jpeg',
date: '06 May',
slug: '/blog/1',
},
{
id: 2,
image: '/assets/images/blog/image-1.jpg',
title: 'Creative Photography',
description: 'Photography is not just about capturing pictures, but emotions and stories through your lens.',
author: 'Alma Clark',
profile: '/assets/images/profile-2.jpeg',
date: '06 May',
slug: '/blog/2',
},
{
id: 3,
image: '/assets/images/blog/image-1.jpg',
title: 'Plan your next trip',
description: 'Traveling helps you explore new cultures, food, and make memories that last a lifetime.',
author: 'Alma Clark',
profile: '/assets/images/profile-3.jpeg',
date: '06 May',
slug: '/blog/3',
},
{
id: 4,
image: '/assets/images/blog/image-1.jpg',
title: 'My latest Vlog',
description: 'Check out my latest vlog where I share behind-the-scenes of my daily life and adventures.',
author: 'Alma Clark',
profile: '/assets/images/profile-4.jpeg',
date: '06 May',
slug: '/blog/4',
},
];
const Blog = () => {
return (
<div className="mt-10">
<h3 className="mb-6 text-xl font-bold md:text-3xl">Blogs</h3>
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 xl:grid-cols-4">
{/* ✅ First box: Create New Blog */}
<Link href="/create-blog" className="flex items-center justify-center space-y-5 rounded-md border border-dashed border-blue-500 bg-blue-50 p-5 text-center shadow hover:bg-blue-100 transition dark:border-blue-800 dark:bg-blue-900 dark:hover:bg-blue-800">
<div>
<div className="mb-3 text-5xl text-blue-600 dark:text-white">+</div>
<h5 className="text-lg font-semibold text-blue-800 dark:text-white">Create New Blog</h5>
</div>
</Link>
{blogs.map((blog) => (
<div
key={blog.id}
className="space-y-4 rounded-md border border-white-light bg-white p-5 shadow-[0px_0px_2px_0px_rgba(145,158,171,0.20),0px_12px_24px_-4px_rgba(145,158,171,0.12)] dark:border-[#1B2E4B] dark:bg-black"
>
<div className="max-h-56 overflow-hidden rounded-md">
<img src={blog.image} alt={blog.title} className="w-full object-cover" />
</div>
{/* ✅ Description first, then Title */}
<h5 className="text-lg font-semibold dark:text-white">{blog.title}</h5>
<p className="text-sm text-gray-600 dark:text-gray-400">{blog.description}</p>
{/*
<div className="flex items-center">
<div className="me-4 overflow-hidden rounded-full bg-white-dark">
<img src={blog.profile} className="h-11 w-11 object-cover" alt={blog.author} />
</div>
<div className="flex-1">
<h4 className="mb-1.5 font-semibold dark:text-white">{blog.author}</h4>
<p className="text-xs text-gray-500">{blog.date}</p>
</div>
</div> */}
{/* ✅ Read More button */}
<div>
<Link
href={blog.slug}
className="inline-block mt-3 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
>
Read More
</Link>
</div>
</div>
))}
</div>
</div>
);
};
export default Blog;

View File

@ -1,217 +0,0 @@
"use client";
import IconTrashLines from "@/components/icon/icon-trash-lines";
import dynamic from "next/dynamic";
import React, { useMemo, useState } from "react";
import "react-quill/dist/quill.snow.css";
const ReactQuill = dynamic(() => import("react-quill"), {
ssr: false,
});
const formats = [
"header",
"bold",
"italic",
"underline",
"strike",
"blockquote",
"code-block",
"list",
"bullet",
"align",
"link",
"image",
"video",
];
const PostForm = () => {
const [formData, setFormData] = useState({
title: "",
slug: "",
coverImage: null as File | null,
description: "",
});
const modules = useMemo(
() => ({
toolbar: {
container: [
[{ header: [1, 2, 3, false] }],
["bold", "italic", "underline", "strike"],
[{ list: "ordered" }, { list: "bullet" }],
["blockquote", "code-block"],
[{ align: [] }],
["link", "image", "video"],
["clean"],
],
handlers: {
image: function (this: any) {
const input = document.createElement("input");
input.setAttribute("type", "file");
input.setAttribute("accept", "image/*");
input.click();
input.onchange = async () => {
const file = input.files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = () => {
const quill = this.quill;
const range = quill.getSelection(true);
quill.insertEmbed(
range.index,
"image",
reader.result
);
};
reader.readAsDataURL(file);
}
};
},
},
},
}),
[]
);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData((prev) => ({
...prev,
[name]: value,
}));
};
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0] || null;
setFormData((prev) => ({
...prev,
coverImage: file,
}));
};
const handleRemoveImage = () => {
setFormData((prev) => ({
...prev,
coverImage: null,
}));
};
const handleDescriptionChange = (value: string) => {
setFormData((prev) => ({
...prev,
description: value,
}));
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const data = new FormData();
data.append("title", formData.title);
data.append("slug", formData.slug);
if (formData.coverImage) {
data.append("coverImage", formData.coverImage);
}
data.append("description", formData.description);
console.log("FormData prepared:", {
title: formData.title,
slug: formData.slug,
coverImage: formData.coverImage?.name,
description: formData.description,
});
};
return (
<form
onSubmit={handleSubmit}
className="space-y-5 max-w-4xl mx-auto p-6 bg-white rounded shadow-md"
>
<h2 className="text-xl font-bold mb-4">Create Blog</h2>
<div>
<label className="block font-medium mb-1">Blog Title</label>
<input
type="text"
name="title"
value={formData.title}
onChange={handleChange}
placeholder="Enter blog title"
className="w-full border rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500 outline-none"
/>
</div>
<div>
<label className="block font-medium mb-1">Slug</label>
<input
type="text"
name="slug"
value={formData.slug}
onChange={handleChange}
placeholder="Enter slug"
className="w-full border rounded-md px-3 py-2 focus:ring-2 focus:ring-green-500 outline-none"
/>
</div>
<div>
<label className="block font-medium mb-1">Cover Image</label>
<input
type="file"
accept="image/*"
onChange={handleImageChange}
className="w-full border rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500 outline-none"
/>
</div>
{formData.coverImage && (
<div className="mt-3">
<p className="text-sm font-medium">Preview:</p>
<div className="relative inline-block mt-2">
<img
src={URL.createObjectURL(formData.coverImage)}
alt="Selected"
className="w-48 h-32 object-cover rounded border"
/>
<button
type="button"
onClick={handleRemoveImage}
className="absolute top-1 right-1 bg-red-600 text-white text-xs px-2 py-1 rounded hover:bg-red-700"
>
<IconTrashLines className="shrink-0" />
</button>
</div>
</div>
)}
<div className="mb-5">
<ReactQuill
value={formData.description}
onChange={handleDescriptionChange}
modules={modules}
formats={formats}
placeholder="Write your description..."
className="bg-white text-black rounded-md"
/>
</div>
<button
type="submit"
className="bg-blue-600 text-white px-6 py-2 mt-5 rounded-md hover:bg-blue-700 transition"
>
Submit
</button>
</form>
);
};
export default PostForm;

View File

@ -1,6 +0,0 @@
import { redirect } from 'next/navigation';
export default function AuthLoginRedirect() {
// Redirect legacy /auth/login URL to the actual login page at /login
redirect('/login');
}

View File

@ -1,13 +1,8 @@
'use client';
import IconLockDots from '@/components/icon/icon-lock-dots';
import IconMail from '@/components/icon/icon-mail';
import IconEye from '@/components/icon/icon-eye';
import { useRouter } from 'next/navigation';
import React, { useState } from 'react';
import axios from 'axios';
import Cookies from 'universal-cookie';
const cookies = new Cookies();
const LoginForm = () => {
const router = useRouter();
@ -18,9 +13,7 @@ const LoginForm = () => {
password: '',
});
// ✅ State for loading and errors
const [loading, setLoading] = useState(false);
const [showPassword, setShowPassword] = useState(false);
// ✅ State for errors
const [errors, setErrors] = useState<{ email?: string; password?: string }>({});
// Generic handler for inputs
@ -50,7 +43,7 @@ const LoginForm = () => {
return newErrors;
};
const submitForm = async (e: React.FormEvent) => {
const submitForm = (e: React.FormEvent) => {
e.preventDefault();
const validationErrors = validate();
@ -59,25 +52,8 @@ const LoginForm = () => {
return;
}
setLoading(true);
try {
const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3000/api/';
const response = await axios.post(`${baseUrl}auth/login`, formData);
if (response.data.success && response.data.data?.token) {
cookies.set('token', response.data.data.token, { path: '/' });
cookies.set('user', JSON.stringify(response.data.data.user), { path: '/' });
router.push('/');
}
} catch (error: any) {
console.error('Login error', error);
setErrors({
email: error.response?.data?.message || 'Invalid email or password',
password: '',
});
} finally {
setLoading(false);
}
console.log('Form Data:', formData);
router.push('/');
};
return (
@ -109,22 +85,15 @@ const LoginForm = () => {
<input
id="Password"
name="password"
type={showPassword ? "text" : "password"}
type="password"
placeholder="Enter Password"
className="form-input ps-10 pe-10 placeholder:text-white-dark"
className="form-input ps-10 placeholder:text-white-dark"
value={formData.password}
onChange={handleChange}
/>
<span className="absolute start-4 top-1/2 -translate-y-1/2">
<IconLockDots fill={true} />
</span>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className={`absolute end-4 top-1/2 -translate-y-1/2 hover:text-black dark:hover:text-white ${showPassword ? 'text-black dark:text-white' : ''}`}
>
<IconEye />
</button>
</div>
{errors.password && (
<p className="text-red-500 text-sm mt-1">{errors.password}</p>
@ -134,9 +103,8 @@ const LoginForm = () => {
<button
type="submit"
className="btn btn-gradient !mt-6 w-full border-0 uppercase shadow-[0_10px_20px_-10px_rgba(67,97,238,0.44)]"
disabled={loading}
>
{loading ? 'Signing in...' : 'Sign in'}
Sign in
</button>
</form>
);

View File

@ -1,11 +1,9 @@
'use client';
'use client';
import React, { useState, ChangeEvent, FormEvent } from 'react';
import IconTrashLines from '../icon/icon-trash-lines';
import axios from 'axios';
import Cookies from 'universal-cookie';
import { useRouter } from 'next/navigation';
import { showMessage } from '@/utils/CommonFunction.utils';
import { buildApiUrl } from '@/utils/BaseUrl.utils';
interface FormValues {
year: string;
@ -87,17 +85,9 @@ const CreateEventForm: React.FC = () => {
}
try {
const cookies = new Cookies();
const token = cookies.get('token');
if (!token) {
showMessage('Access denied. Please sign in first.');
router.push('/login');
return;
}
const ImageUpload = await axios.post(buildApiUrl('upload/single'), data, {
const ImageUpload = await axios.post(`https://api.tamilculturewaterloo.org/api/upload/single`, data, {
headers: {
"Content-Type": "multipart/form-data", // important for file upload
Authorization: `Bearer ${token}`,
},
})
console.log("ImageUpload", ImageUpload)
@ -109,11 +99,7 @@ const CreateEventForm: React.FC = () => {
eventimageurl: ImageUpload?.data?.data?.fullUrl
}
const res = await axios.post(buildApiUrl('events'), createData, {
headers: {
Authorization: `Bearer ${cookies.get('token')}`,
},
})
const res = await axios.post(`https://api.tamilculturewaterloo.org/api/events`, createData)
console.log("res", res)
showMessage("Event Created Successfully", "success")
router?.push(`/`)

View File

@ -3,7 +3,6 @@ import React, { useState, ChangeEvent, FormEvent } from 'react';
import IconTrashLines from '../icon/icon-trash-lines';
import axios from 'axios';
import { useRouter } from 'next/navigation';
import { buildApiUrl } from '@/utils/BaseUrl.utils';
interface FormErrors {
[key: string]: string;
@ -17,18 +16,10 @@ const CreateEventGalleryForm: React.FC<EditEventFormProps> = ({ eventId }) => {
const [selectedImages, setSelectedImages] = useState<File[]>([]);
const [previewUrls, setPreviewUrls] = useState<string[]>([]);
const [loading, setLoading] = useState(false);
const [errors, setErrors] = useState<FormErrors>({});
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
const files = Array.from(e.target.files || []);
if (selectedImages.length + files.length > 50) {
setErrors({ images: 'You can only upload up to 50 images at a time' });
alert('You can only upload up to 50 images at a time');
return;
}
const validImages: File[] = [];
const previews: string[] = [];
@ -41,7 +32,6 @@ const CreateEventGalleryForm: React.FC<EditEventFormProps> = ({ eventId }) => {
setSelectedImages(prev => [...prev, ...validImages]);
setPreviewUrls(prev => [...prev, ...previews]);
setErrors(prev => ({ ...prev, images: '' })); // clear error if valid
};
const handleImageDelete = (index: number) => {
@ -54,8 +44,6 @@ const CreateEventGalleryForm: React.FC<EditEventFormProps> = ({ eventId }) => {
if (selectedImages.length === 0) {
newErrors.images = 'Please upload at least one image';
} else if (selectedImages.length > 50) {
newErrors.images = 'Only 50 images can be uploaded at a time';
}
setErrors(newErrors);
@ -66,7 +54,6 @@ const CreateEventGalleryForm: React.FC<EditEventFormProps> = ({ eventId }) => {
e.preventDefault();
if (!validateForm()) return;
setLoading(true);
try {
const formData = new FormData();
@ -74,32 +61,27 @@ const CreateEventGalleryForm: React.FC<EditEventFormProps> = ({ eventId }) => {
formData.append(`files`, file);
});
const uploadRes = await axios.post(buildApiUrl('upload/multiple'), formData)
const uploadRes = await axios.post(`https://api.tamilculturewaterloo.org/api/upload/multiple`, formData)
console.log("uploadres", uploadRes)
const uploadedUrls = uploadRes?.data?.data?.map((image: any) => image?.fullUrl) || [];
const uploadedUrls = uploadRes?.data?.data?.map((image: any) => image?.fullUrl) || [];
// Step 2: Prepare body for bulk save
// Step 2: Prepare correct body for bulk save
const body = {
eventid: Number(eventId),
imageurl: uploadedUrls
eventid: Number(eventId), // ensure number
imageurl: uploadedUrls // API may expect 'imageurls' not 'imageurl'
};
console.log("Sending body:", body);
// Step 3: Call bulk API
await axios.post(
buildApiUrl('event-images/bulk'),
`https://api.tamilculturewaterloo.org/api/event-images/bulk`,
body
);
router.push(`/event-gallery?eventid=${eventId}`);
} catch (error: any) {
} catch (error) {
console.error('Upload failed:', error);
const message = error.response?.data?.message || 'Failed to upload images. Please try again.';
setErrors({ submit: message });
alert(message);
} finally {
setLoading(false);
}
};
@ -147,10 +129,9 @@ const CreateEventGalleryForm: React.FC<EditEventFormProps> = ({ eventId }) => {
<div className="mt-6">
<button
type="submit"
disabled={loading}
className={`bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700 ${loading ? 'opacity-50 cursor-not-allowed' : ''}`}
className="bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700"
>
{loading ? 'Uploading...' : 'Submit'}
Submit
</button>
</div>
</form>

View File

@ -3,7 +3,6 @@ import React, { useState, useEffect, ChangeEvent, FormEvent } from 'react';
import IconTrashLines from '../icon/icon-trash-lines';
import axios from 'axios';
import { useRouter } from 'next/navigation';
import { buildApiUrl } from '@/utils/BaseUrl.utils';
interface FormValues {
year: string;
@ -44,7 +43,7 @@ const EditEventForm: React.FC<EditEventFormProps> = ({ eventId }) => {
const getEvent = async () => {
try {
const res = await axios.get(buildApiUrl(`events/${eventId}`));
const res = await axios.get(`https://api.tamilculturewaterloo.org/api/events/${eventId}`);
const data = res?.data?.data;
setFormData({
@ -125,7 +124,7 @@ const EditEventForm: React.FC<EditEventFormProps> = ({ eventId }) => {
imgFormData.append('file', formData.eventimageurl);
const uploadRes = await axios.post(
buildApiUrl('upload/single'),
'https://api.tamilculturewaterloo.org/api/upload/single',
imgFormData,
{ headers: { 'Content-Type': 'multipart/form-data' } }
);
@ -141,7 +140,7 @@ const EditEventForm: React.FC<EditEventFormProps> = ({ eventId }) => {
eventimageurl: imageUrl,
};
const res = await axios.put(buildApiUrl(`events/${eventId}`), updatedData, );
const res = await axios.put(`https://api.tamilculturewaterloo.org/api/events/${eventId}`, updatedData, );
console.log('Event updated:', res.data);
router.push('/');

View File

@ -1,5 +1,5 @@
'use client';
import React, { useEffect, useRef, useState } from 'react';
import React, { useEffect, useState } from 'react';
import Lightbox from 'yet-another-react-lightbox';
import Captions from 'yet-another-react-lightbox/plugins/captions';
import Zoom from 'yet-another-react-lightbox/plugins/zoom';
@ -8,22 +8,7 @@ import 'yet-another-react-lightbox/plugins/captions.css';
import axios from 'axios';
import { useRouter } from 'next/navigation';
import IconTrashLines from '../icon/icon-trash-lines';
import IconMenu from '../icon/icon-menu';
import Swal from 'sweetalert2';
import { buildApiUrl } from '@/utils/BaseUrl.utils';
import { ReactSortable } from 'react-sortablejs';
import Sortable, { Swap } from 'sortablejs';
if (typeof window !== 'undefined') {
Sortable.mount(new Swap());
}
interface GalleryImage {
id: number;
src: string;
title: string;
description: string;
}
interface EditEventFormProps {
eventId: string | null;
@ -34,12 +19,7 @@ const ListOfEventsGallery: React.FC<EditEventFormProps> = ({ eventId }) => {
const [isOpen, setIsOpen] = useState(false);
const [photoIndex, setPhotoIndex] = useState(0);
const [eventImages, setEventImages] = useState<GalleryImage[]>([]);
const latestImagesRef = useRef<GalleryImage[]>([]);
useEffect(() => {
latestImagesRef.current = eventImages;
}, [eventImages]);
const [eventImages, setEventImages] = useState<any[]>([]);
useEffect(() => {
if (eventId) {
@ -49,58 +29,23 @@ const ListOfEventsGallery: React.FC<EditEventFormProps> = ({ eventId }) => {
const getEventGallery = async () => {
try {
const res = await axios.get(buildApiUrl(`event-images/event/${eventId}`));
const formatted: GalleryImage[] = res.data?.data?.map((img: any) => ({
id: img.id,
const res = await axios.get(`https://api.tamilculturewaterloo.org/api/event-images/event/${eventId}`);
const formatted = res.data?.data?.map((img: any) => ({
id: img.id, // ensure ID is preserved for deletion
src: img.imageurl,
title: img?.title || '',
description: img?.description || '',
})) || [];
setEventImages(formatted);
} catch (error) {
console.log('error', error);
}
};
const handleReorder = (newList: GalleryImage[]) => {
setEventImages(newList);
};
const saveNewOrder = async (list: GalleryImage[]) => {
try {
const images = list.map((item, index) => ({
id: item.id,
sort_order: index,
}));
await axios.put(buildApiUrl('event-images/reorder'), { images });
Swal.fire({
title: 'Saved!',
text: 'Image order saved successfully.',
icon: 'success',
toast: true,
position: 'top-end',
showConfirmButton: false,
timer: 2000,
});
} catch (error: any) {
console.error('Failed to save order:', error);
const errMsg = error.response?.data?.message || error.message || 'Unknown error';
Swal.fire({
title: 'Error!',
text: `Failed to save the new order: ${errMsg}`,
icon: 'error',
toast: true,
position: 'top-end',
showConfirmButton: false,
timer: 5000,
});
setEventImages(formatted || []);
} catch (error) {
console.log("error", error);
}
};
const showAlert = async (e: React.MouseEvent, item: GalleryImage) => {
e.stopPropagation();
const showAlert = async (e: React.MouseEvent, item: any) => {
e.stopPropagation(); // ✅ Prevents triggering the parent onClick (lightbox)
if (isOpen) setIsOpen(false);
if (isOpen) setIsOpen(false); // optional: close lightbox if it's open
Swal.fire({
icon: 'warning',
@ -113,14 +58,14 @@ const ListOfEventsGallery: React.FC<EditEventFormProps> = ({ eventId }) => {
}).then(async (result) => {
if (result.isConfirmed) {
try {
await axios.delete(buildApiUrl(`event-images/${item.id}`));
await axios.delete(`https://api.tamilculturewaterloo.org/api/event-images/${item.id}`);
Swal.fire({
title: 'Deleted!',
text: 'Your file has been deleted.',
icon: 'success',
customClass: { popup: 'sweet-alerts' },
});
getEventGallery();
getEventGallery(); // refresh after delete
} catch (error) {
Swal.fire({
title: 'Error!',
@ -135,75 +80,45 @@ const ListOfEventsGallery: React.FC<EditEventFormProps> = ({ eventId }) => {
return (
<div className="panel">
{/* Header */}
<div className="flex justify-between items-center mb-4">
<h5 className="text-lg font-semibold dark:text-white-light">Gallery</h5>
<div className='flex justify-between items-center'>
<h5 className="mb-5 text-lg font-semibold dark:text-white-light">Gallery</h5>
<button
type="button"
onClick={() => router.push(`/create-event-gallery?eventid=${eventId}`)}
className="bg-blue-600 text-white px-4 py-2 rounded text-sm font-medium hover:bg-blue-700 transition-colors"
className="bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700"
>
Upload Images
</button>
</div>
{/* Gallery Grid */}
{eventImages.length === 0 ? (
<div className="flex flex-col items-center justify-center text-gray-400 py-20">
<svg className="w-16 h-16 mb-4 opacity-30" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<p className="text-lg font-medium">No images yet</p>
<p className="text-sm mt-1">Click "Upload Images" to add some!</p>
</div>
) : (
<>
<p className="text-xs text-gray-400 italic mb-4">Drag the icon to reorder images</p>
<ReactSortable
list={eventImages}
setList={handleReorder}
onEnd={() => saveNewOrder(latestImagesRef.current)}
animation={200}
handle=".drag-handle"
swap={true}
swapClass="swap-highlight"
className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 items-start"
<div className="mt-10 grid grid-cols-1 gap-3 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
{eventImages.map((item, index) => (
<div
key={item.id}
className="cursor-pointer relative"
onClick={() => {
setPhotoIndex(index);
setIsOpen(true);
}}
>
{eventImages.map((item, index) => (
<div
key={item.id}
className="cursor-pointer relative rounded-md border border-gray-200 dark:border-gray-800 shadow-sm overflow-hidden group"
onClick={() => {
setPhotoIndex(index);
setIsOpen(true);
}}
{/* Delete button (top-right corner) */}
<div className="absolute top-5 right-5 flex gap-2 z-10">
<button
onClick={(e) => showAlert(e, item)}
className="bg-red-600 text-white text-xs px-2 py-1 rounded"
>
{/* Drag Handle */}
<div className="drag-handle absolute top-2 left-2 z-20 cursor-move bg-white/80 dark:bg-black/80 rounded p-1 opacity-0 group-hover:opacity-100 hover:bg-white dark:hover:bg-black transition-all shadow-sm">
<IconMenu className="w-4 h-4 text-gray-600 dark:text-gray-300" />
</div>
<IconTrashLines />
</button>
</div>
{/* Delete Button */}
<div className="absolute top-2 right-2 flex gap-2 z-10 opacity-0 group-hover:opacity-100 transition-opacity">
<button
onClick={(e) => showAlert(e, item)}
className="bg-red-600/80 hover:bg-red-600 text-white text-xs p-1.5 rounded-full shadow-sm transition-colors"
>
<IconTrashLines className="w-4 h-4" />
</button>
</div>
<img
src={item.src}
alt={`gallery-${index}`}
onDragStart={(e) => e.preventDefault()}
className="w-full h-auto"
/>
</div>
))}
</ReactSortable>
</>
)}
<img
src={item.src}
alt={`gallery-${index}`}
className="h-full w-full rounded-md object-cover"
/>
</div>
))}
</div>
<Lightbox
styles={{ container: { backgroundColor: 'rgba(0,0,0,0.6)' } }}

View File

@ -1,6 +1,5 @@
'use client';
'use client';
import axios from 'axios';
import Cookies from 'universal-cookie';
import { Metadata } from 'next';
import Link from 'next/link';
import React, { useEffect, useState } from 'react';
@ -8,7 +7,6 @@ import IconTrashLines from '../icon/icon-trash-lines';
import IconPencil from '../icon/icon-pencil';
import Swal from 'sweetalert2';
import { useRouter } from 'next/navigation';
import { buildApiUrl } from '@/utils/BaseUrl.utils';
export const metadata: Metadata = {
title: 'Knowledge Base',
@ -25,16 +23,7 @@ const ListOfEvents = () => {
const getEvents = async () => {
try {
const cookies = new Cookies();
const token = cookies.get('token');
if (!token) {
// No token: redirect to login
router.push('/login');
return;
}
const eventRes: any = await axios.get(buildApiUrl('events'), {
headers: { Authorization: `Bearer ${token}` },
});
const eventRes: any = await axios?.get(`https://api.tamilculturewaterloo.org/api/events`)
console.log("eventRes", eventRes)
setEvents(eventRes?.data?.data)
} catch (error) {
@ -70,13 +59,9 @@ const ListOfEvents = () => {
padding: '2em',
customClass: { popup: 'sweet-alerts' },
}).then(async (result) => {
if (result.isConfirmed) {
if (result.isConfirmed) {
try {
const cookies = new Cookies();
const token = cookies.get('token');
await axios.delete(buildApiUrl(`events/${event.id}`), {
headers: { Authorization: `Bearer ${token}` },
});
await axios.delete(`https://api.tamilculturewaterloo.org/api/events/${event.id}`);
Swal.fire({
title: 'Deleted!',
text: 'Your file has been deleted.',

View File

@ -22,7 +22,6 @@ import IconUser from '@/components/icon/icon-user';
import IconMail from '@/components/icon/icon-mail';
import IconLockDots from '@/components/icon/icon-lock-dots';
import IconLogout from '@/components/icon/icon-logout';
import Cookies from 'universal-cookie';
import IconMenuDashboard from '@/components/icon/menu/icon-menu-dashboard';
import IconCaretDown from '@/components/icon/icon-caret-down';
import IconMenuApps from '@/components/icon/menu/icon-menu-apps';
@ -81,29 +80,6 @@ const Header = () => {
router.refresh();
};
const handleSignOut = () => {
try {
const cookies = new Cookies();
// remove common auth cookies if present
cookies.remove('token', { path: '/' });
cookies.remove('auth', { path: '/' });
cookies.remove('i18nextLng', { path: '/' });
} catch (e) {
// ignore
}
// remove known localStorage keys (avoid removing theme prefs)
try {
localStorage.removeItem('authToken');
localStorage.removeItem('user');
} catch (e) {
// ignore
}
// Redirect to the login page. app/(auth)/login maps to `/login` in the URL.
router.replace('/login');
};
function createMarkup(messages: any) {
return { __html: messages };
}
@ -249,10 +225,10 @@ const Header = () => {
</Link>
</li>
<li className="border-t border-white-light dark:border-white-light/10">
<button type="button" onClick={handleSignOut} className="!py-3 text-danger w-full text-left">
<Link href="/auth/boxed-signin" className="!py-3 text-danger">
<IconLogout className="h-4.5 w-4.5 shrink-0 rotate-90 ltr:mr-2 rtl:ml-2" />
Sign Out
</button>
</Link>
</li>
</ul>
</Dropdown>

View File

@ -249,36 +249,12 @@ const Sidebar = () => {
</ul>
</li>
<h2 className="-mx-4 mb-1 flex items-center bg-white-light/30 px-7 py-3 font-extrabold uppercase dark:bg-dark dark:bg-opacity-[0.08]">
{/* <h2 className="-mx-4 mb-1 flex items-center bg-white-light/30 px-7 py-3 font-extrabold uppercase dark:bg-dark dark:bg-opacity-[0.08]">
<IconMinus className="hidden h-5 w-4 flex-none" />
<span>{t('blog')}</span>
<span>{t('user_interface')}</span>
</h2>
<li className="menu nav-item">
<button type="button" className={`${currentMenu === 'blog' ? 'active' : ''} nav-link group w-full`} onClick={() => toggleMenu('blog')}>
<div className="flex items-center">
<IconMenuInvoice className="shrink-0 group-hover:!text-primary" />
<span className="text-black ltr:pl-3 rtl:pr-3 dark:text-[#506690] dark:group-hover:text-white-dark">{t('Blog')}</span>
</div>
<div className={currentMenu !== 'blog' ? '-rotate-90 rtl:rotate-90' : ''}>
<IconCaretDown />
</div>
</button>
<AnimateHeight duration={300} height={currentMenu === 'blog' ? 'auto' : 0}>
<ul className="sub-menu text-gray-500">
<li>
<Link href="/blog">{t('list')}</Link>
</li>
<li>
<Link href="/create-blog">{t('add')}</Link>
</li>
</ul>
</AnimateHeight>
</li>
{/* <li className="menu nav-item">
<button type="button" className={`${currentMenu === 'component' ? 'active' : ''} nav-link group w-full`} onClick={() => toggleMenu('component')}>
<div className="flex items-center">
<IconMenuComponents className="shrink-0 group-hover:!text-primary" />
@ -573,7 +549,7 @@ const Sidebar = () => {
</ul>
</AnimateHeight>
</li> */}
{/*
{/*
<h2 className="-mx-4 mb-1 flex items-center bg-white-light/30 px-7 py-3 font-extrabold uppercase dark:bg-dark dark:bg-opacity-[0.08]">
<IconMinus className="hidden h-5 w-4 flex-none" />
<span>{t('user_and_pages')}</span>

View File

@ -1,38 +0,0 @@
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(req: NextRequest) {
const { pathname } = req.nextUrl;
// Allow public and framework paths without auth
const allowlist = [
'/login', // login page
];
if (
pathname.startsWith('/_next') ||
pathname.startsWith('/static') ||
pathname.startsWith('/assets') ||
allowlist.some((p) => pathname === p || pathname.startsWith(p + '/')) ||
// allow public files (images, css, etc.)
/\.(jpg|jpeg|png|svg|ico|css|js|map)$/.test(pathname)
) {
return NextResponse.next();
}
// For all other routes, require a token cookie
const token = req.cookies.get('token')?.value;
if (!token) {
const url = req.nextUrl.clone();
url.pathname = '/login';
return NextResponse.redirect(url);
}
return NextResponse.next();
}
export const config = {
matcher: ['/:path*'],
};

169
package-lock.json generated
View File

@ -28,10 +28,7 @@
"react-i18next": "^15.0.2",
"react-perfect-scrollbar": "^1.5.8",
"react-popper": "^2.3.0",
"react-quill": "^2.0.0",
"react-redux": "^9.1.2",
"react-sortablejs": "^6.1.4",
"sortablejs": "^1.15.7",
"sweetalert2": "^11.22.2",
"typescript": "^5.3.3",
"universal-cookie": "^7.2.0",
@ -1066,15 +1063,6 @@
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz",
"integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA=="
},
"node_modules/@types/quill": {
"version": "1.3.10",
"resolved": "https://registry.npmjs.org/@types/quill/-/quill-1.3.10.tgz",
"integrity": "sha512-IhW3fPW+bkt9MLNlycw8u8fWb7oO7W5URC9MfZYHBlA24rex9rs23D5DETChu1zvgVdc5ka64ICjJOgQMr6Shw==",
"license": "MIT",
"dependencies": {
"parchment": "^1.1.2"
}
},
"node_modules/@types/react": {
"version": "18.3.10",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.10.tgz",
@ -1113,13 +1101,6 @@
"@babel/runtime": "^7.9.2"
}
},
"node_modules/@types/sortablejs": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.15.9.tgz",
"integrity": "sha512-7HP+rZGE2p886PKV9c9OJzLBI6BBJu1O7lJGYnPyG3fS4/duUCcngkNCjsLwIMV+WMqANe3tt4irrXHSIe68OQ==",
"license": "MIT",
"peer": true
},
"node_modules/@types/use-sync-external-store": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz",
@ -2055,26 +2036,11 @@
"node": ">= 6"
}
},
"node_modules/classnames": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz",
"integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==",
"license": "MIT"
},
"node_modules/client-only": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="
},
"node_modules/clone": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
"license": "MIT",
"engines": {
"node": ">=0.8"
}
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
@ -3168,29 +3134,11 @@
"node": ">=0.10.0"
}
},
"node_modules/eventemitter3": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz",
"integrity": "sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==",
"license": "MIT"
},
"node_modules/extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
"license": "MIT"
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
},
"node_modules/fast-diff": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz",
"integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==",
"license": "Apache-2.0"
},
"node_modules/fast-glob": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
@ -4867,12 +4815,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/parchment": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/parchment/-/parchment-1.1.4.tgz",
"integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==",
"license": "BSD-3-Clause"
},
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@ -5329,74 +5271,6 @@
}
]
},
"node_modules/quill": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/quill/-/quill-1.3.7.tgz",
"integrity": "sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==",
"license": "BSD-3-Clause",
"dependencies": {
"clone": "^2.1.1",
"deep-equal": "^1.0.1",
"eventemitter3": "^2.0.3",
"extend": "^3.0.2",
"parchment": "^1.1.4",
"quill-delta": "^3.6.2"
}
},
"node_modules/quill-delta": {
"version": "3.6.3",
"resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-3.6.3.tgz",
"integrity": "sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==",
"license": "MIT",
"dependencies": {
"deep-equal": "^1.0.1",
"extend": "^3.0.2",
"fast-diff": "1.1.2"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/quill-delta/node_modules/deep-equal": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.2.tgz",
"integrity": "sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==",
"license": "MIT",
"dependencies": {
"is-arguments": "^1.1.1",
"is-date-object": "^1.0.5",
"is-regex": "^1.1.4",
"object-is": "^1.1.5",
"object-keys": "^1.1.1",
"regexp.prototype.flags": "^1.5.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/quill/node_modules/deep-equal": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.2.tgz",
"integrity": "sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==",
"license": "MIT",
"dependencies": {
"is-arguments": "^1.1.1",
"is-date-object": "^1.0.5",
"is-regex": "^1.1.4",
"object-is": "^1.1.5",
"object-keys": "^1.1.1",
"regexp.prototype.flags": "^1.5.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/react": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
@ -5490,21 +5364,6 @@
"react-dom": "^16.8.0 || ^17 || ^18"
}
},
"node_modules/react-quill": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/react-quill/-/react-quill-2.0.0.tgz",
"integrity": "sha512-4qQtv1FtCfLgoD3PXAur5RyxuUbPXQGOHgTlFie3jtxp43mXDtzCKaOgQ3mLyZfi1PUlyjycfivKelFhy13QUg==",
"license": "MIT",
"dependencies": {
"@types/quill": "^1.3.10",
"lodash": "^4.17.4",
"quill": "^1.3.7"
},
"peerDependencies": {
"react": "^16 || ^17 || ^18",
"react-dom": "^16 || ^17 || ^18"
}
},
"node_modules/react-redux": {
"version": "9.1.2",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.1.2.tgz",
@ -5527,22 +5386,6 @@
}
}
},
"node_modules/react-sortablejs": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/react-sortablejs/-/react-sortablejs-6.1.4.tgz",
"integrity": "sha512-fc7cBosfhnbh53Mbm6a45W+F735jwZ1UFIYSrIqcO/gRIFoDyZeMtgKlpV4DdyQfbCzdh5LoALLTDRxhMpTyXQ==",
"license": "MIT",
"dependencies": {
"classnames": "2.3.1",
"tiny-invariant": "1.2.0"
},
"peerDependencies": {
"@types/sortablejs": "1",
"react": ">=16.9.0",
"react-dom": ">=16.9.0",
"sortablejs": "1"
}
},
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@ -5848,12 +5691,6 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/sortablejs": {
"version": "1.15.7",
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.7.tgz",
"integrity": "sha512-Kk8wLQPlS+yi1ZEf48a4+fzHa4yxjC30M/Sr2AnQu+f/MPwvvX9XjZ6OWejiz8crBsLwSq8GHqaxaET7u6ux0A==",
"license": "MIT"
},
"node_modules/source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
@ -6255,12 +6092,6 @@
"resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz",
"integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q=="
},
"node_modules/tiny-invariant": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.2.0.tgz",
"integrity": "sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg==",
"license": "MIT"
},
"node_modules/tiny-warning": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",

View File

@ -29,10 +29,7 @@
"react-i18next": "^15.0.2",
"react-perfect-scrollbar": "^1.5.8",
"react-popper": "^2.3.0",
"react-quill": "^2.0.0",
"react-redux": "^9.1.2",
"react-sortablejs": "^6.1.4",
"sortablejs": "^1.15.7",
"sweetalert2": "^11.22.2",
"typescript": "^5.3.3",
"universal-cookie": "^7.2.0",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

View File

@ -677,10 +677,3 @@ img.dark-img {
.dark img.dark-img {
@apply !block;
}
/* Gallery drag-and-drop swap highlight */
.swap-highlight {
outline: 2px solid #f59e0b;
outline-offset: 2px;
opacity: 0.8;
}

View File

@ -1,13 +1 @@
const DEFAULT_BASE_URL = 'https://api.tamilculturewaterloo.org/api/';
const configuredBaseUrl =
typeof process !== 'undefined' && process.env.NEXT_PUBLIC_API_BASE_URL
? process.env.NEXT_PUBLIC_API_BASE_URL
: DEFAULT_BASE_URL;
export const Baseurl = configuredBaseUrl.endsWith('/') ? configuredBaseUrl : `${configuredBaseUrl}/`;
export const buildApiUrl = (path) => {
const normalizedPath = String(path).replace(/^\/+/, '');
return `${Baseurl}${normalizedPath}`;
};
export const Baseurl = "https://api.tamilculturewaterloo.org/api/"