Compare commits

...

8 Commits
team ... main

Author SHA1 Message Date
Ashwanth3637
897dbfe20d login 2026-06-16 16:00:36 +05:30
root
38313f6340 Changes from server by Mohan 2026-06-09 08:29:05 +00:00
Ravindranbit
ca698ad1df feat: gallery drag-and-drop reorder, grid layout fix, and svg upload support
- Add drag-and-drop image reordering using react-sortablejs (swap mode)
- Add sort_order column to event_images table
- Add PUT /api/event-images/reorder bulk reorder endpoint
- Fix gallery grid layout (items-start to remove bottom gap)
- Fix swapClass whitespace error (single CSS class)
- Increase upload limit from 10 to 50 images
- Add 50-image validation with user-friendly error message
- Add svg file support in multer file filter
- Remove auto-arrange feature
- Add empty state UI for gallery with no images
- Add drag handle and delete button on hover
2026-04-28 22:00:41 +05:30
Ravindranbit
83b891d8d8 feat: increase upload limit to 50 and add svg support with error handling 2026-04-28 17:14:25 +05:30
Ravindranbit
4b9797e368 feat: integrate frontend login form with backend API
- Migrated dummy login form submission to use Axios and correctly hit /api/auth/login
- Stored session tokens in universal-cookie upon successful authentication
- Added visually integrated 'show password' toggle functionality with custom eye icon
2026-04-28 16:44:54 +05:30
Ravindranbit
9f25fe3228 Refactor API URLs to use centralized buildApiUrl utility and environment variables 2026-04-28 16:31:48 +05:30
Ravindranbit
947f8114d2 fix(auth): sign-out clears auth and redirects to /login 2026-04-28 15:32:09 +05:30
Alaguraj0361
4ab9fd5264 blog and create blog page structure updated 2025-08-30 18:29:29 +05:30
19 changed files with 846 additions and 74 deletions

1
.env Normal file
View File

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

1
.env.example Normal file
View File

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

View File

@ -0,0 +1,104 @@
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

@ -0,0 +1,217 @@
"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;

6
app/auth/login/page.tsx Normal file
View File

@ -0,0 +1,6 @@
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,8 +1,13 @@
'use client'; 'use client';
import IconLockDots from '@/components/icon/icon-lock-dots'; import IconLockDots from '@/components/icon/icon-lock-dots';
import IconMail from '@/components/icon/icon-mail'; import IconMail from '@/components/icon/icon-mail';
import IconEye from '@/components/icon/icon-eye';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import React, { useState } from 'react'; import React, { useState } from 'react';
import axios from 'axios';
import Cookies from 'universal-cookie';
const cookies = new Cookies();
const LoginForm = () => { const LoginForm = () => {
const router = useRouter(); const router = useRouter();
@ -13,7 +18,9 @@ const LoginForm = () => {
password: '', password: '',
}); });
// ✅ State for errors // ✅ State for loading and errors
const [loading, setLoading] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const [errors, setErrors] = useState<{ email?: string; password?: string }>({}); const [errors, setErrors] = useState<{ email?: string; password?: string }>({});
// Generic handler for inputs // Generic handler for inputs
@ -43,7 +50,7 @@ const LoginForm = () => {
return newErrors; return newErrors;
}; };
const submitForm = (e: React.FormEvent) => { const submitForm = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
const validationErrors = validate(); const validationErrors = validate();
@ -52,8 +59,25 @@ const LoginForm = () => {
return; return;
} }
console.log('Form Data:', formData); setLoading(true);
router.push('/'); 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);
}
}; };
return ( return (
@ -85,15 +109,22 @@ const LoginForm = () => {
<input <input
id="Password" id="Password"
name="password" name="password"
type="password" type={showPassword ? "text" : "password"}
placeholder="Enter Password" placeholder="Enter Password"
className="form-input ps-10 placeholder:text-white-dark" className="form-input ps-10 pe-10 placeholder:text-white-dark"
value={formData.password} value={formData.password}
onChange={handleChange} onChange={handleChange}
/> />
<span className="absolute start-4 top-1/2 -translate-y-1/2"> <span className="absolute start-4 top-1/2 -translate-y-1/2">
<IconLockDots fill={true} /> <IconLockDots fill={true} />
</span> </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> </div>
{errors.password && ( {errors.password && (
<p className="text-red-500 text-sm mt-1">{errors.password}</p> <p className="text-red-500 text-sm mt-1">{errors.password}</p>
@ -103,8 +134,9 @@ const LoginForm = () => {
<button <button
type="submit" type="submit"
className="btn btn-gradient !mt-6 w-full border-0 uppercase shadow-[0_10px_20px_-10px_rgba(67,97,238,0.44)]" className="btn btn-gradient !mt-6 w-full border-0 uppercase shadow-[0_10px_20px_-10px_rgba(67,97,238,0.44)]"
disabled={loading}
> >
Sign in {loading ? 'Signing in...' : 'Sign in'}
</button> </button>
</form> </form>
); );

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
'use client'; 'use client';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import Lightbox from 'yet-another-react-lightbox'; import Lightbox from 'yet-another-react-lightbox';
import Captions from 'yet-another-react-lightbox/plugins/captions'; import Captions from 'yet-another-react-lightbox/plugins/captions';
import Zoom from 'yet-another-react-lightbox/plugins/zoom'; import Zoom from 'yet-another-react-lightbox/plugins/zoom';
@ -8,7 +8,22 @@ import 'yet-another-react-lightbox/plugins/captions.css';
import axios from 'axios'; import axios from 'axios';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import IconTrashLines from '../icon/icon-trash-lines'; import IconTrashLines from '../icon/icon-trash-lines';
import IconMenu from '../icon/icon-menu';
import Swal from 'sweetalert2'; 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 { interface EditEventFormProps {
eventId: string | null; eventId: string | null;
@ -19,7 +34,12 @@ const ListOfEventsGallery: React.FC<EditEventFormProps> = ({ eventId }) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [photoIndex, setPhotoIndex] = useState(0); const [photoIndex, setPhotoIndex] = useState(0);
const [eventImages, setEventImages] = useState<any[]>([]); const [eventImages, setEventImages] = useState<GalleryImage[]>([]);
const latestImagesRef = useRef<GalleryImage[]>([]);
useEffect(() => {
latestImagesRef.current = eventImages;
}, [eventImages]);
useEffect(() => { useEffect(() => {
if (eventId) { if (eventId) {
@ -29,23 +49,58 @@ const ListOfEventsGallery: React.FC<EditEventFormProps> = ({ eventId }) => {
const getEventGallery = async () => { const getEventGallery = async () => {
try { try {
const res = await axios.get(`https://api.tamilculturewaterloo.org/api/event-images/event/${eventId}`); const res = await axios.get(buildApiUrl(`event-images/event/${eventId}`));
const formatted = res.data?.data?.map((img: any) => ({ const formatted: GalleryImage[] = res.data?.data?.map((img: any) => ({
id: img.id, // ensure ID is preserved for deletion id: img.id,
src: img.imageurl, src: img.imageurl,
title: img?.title || '', title: img?.title || '',
description: img?.description || '', description: img?.description || '',
})); })) || [];
setEventImages(formatted || []); setEventImages(formatted);
} catch (error) { } catch (error) {
console.log("error", error); console.log('error', error);
} }
}; };
const showAlert = async (e: React.MouseEvent, item: any) => { const handleReorder = (newList: GalleryImage[]) => {
e.stopPropagation(); // ✅ Prevents triggering the parent onClick (lightbox) setEventImages(newList);
};
if (isOpen) setIsOpen(false); // optional: close lightbox if it's open 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,
});
}
};
const showAlert = async (e: React.MouseEvent, item: GalleryImage) => {
e.stopPropagation();
if (isOpen) setIsOpen(false);
Swal.fire({ Swal.fire({
icon: 'warning', icon: 'warning',
@ -58,14 +113,14 @@ const ListOfEventsGallery: React.FC<EditEventFormProps> = ({ eventId }) => {
}).then(async (result) => { }).then(async (result) => {
if (result.isConfirmed) { if (result.isConfirmed) {
try { try {
await axios.delete(`https://api.tamilculturewaterloo.org/api/event-images/${item.id}`); await axios.delete(buildApiUrl(`event-images/${item.id}`));
Swal.fire({ Swal.fire({
title: 'Deleted!', title: 'Deleted!',
text: 'Your file has been deleted.', text: 'Your file has been deleted.',
icon: 'success', icon: 'success',
customClass: { popup: 'sweet-alerts' }, customClass: { popup: 'sweet-alerts' },
}); });
getEventGallery(); // refresh after delete getEventGallery();
} catch (error) { } catch (error) {
Swal.fire({ Swal.fire({
title: 'Error!', title: 'Error!',
@ -80,45 +135,75 @@ const ListOfEventsGallery: React.FC<EditEventFormProps> = ({ eventId }) => {
return ( return (
<div className="panel"> <div className="panel">
<div className='flex justify-between items-center'> {/* Header */}
<h5 className="mb-5 text-lg font-semibold dark:text-white-light">Gallery</h5> <div className="flex justify-between items-center mb-4">
<h5 className="text-lg font-semibold dark:text-white-light">Gallery</h5>
<button <button
type="button" type="button"
onClick={() => router.push(`/create-event-gallery?eventid=${eventId}`)} onClick={() => router.push(`/create-event-gallery?eventid=${eventId}`)}
className="bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700" className="bg-blue-600 text-white px-4 py-2 rounded text-sm font-medium hover:bg-blue-700 transition-colors"
> >
Upload Images Upload Images
</button> </button>
</div> </div>
<div className="mt-10 grid grid-cols-1 gap-3 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4"> {/* Gallery Grid */}
{eventImages.map((item, index) => ( {eventImages.length === 0 ? (
<div <div className="flex flex-col items-center justify-center text-gray-400 py-20">
key={item.id} <svg className="w-16 h-16 mb-4 opacity-30" fill="none" stroke="currentColor" viewBox="0 0 24 24">
className="cursor-pointer relative" <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" />
onClick={() => { </svg>
setPhotoIndex(index); <p className="text-lg font-medium">No images yet</p>
setIsOpen(true); <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"
> >
{/* Delete button (top-right corner) */} {eventImages.map((item, index) => (
<div className="absolute top-5 right-5 flex gap-2 z-10"> <div
<button key={item.id}
onClick={(e) => showAlert(e, item)} className="cursor-pointer relative rounded-md border border-gray-200 dark:border-gray-800 shadow-sm overflow-hidden group"
className="bg-red-600 text-white text-xs px-2 py-1 rounded" onClick={() => {
setPhotoIndex(index);
setIsOpen(true);
}}
> >
<IconTrashLines /> {/* Drag Handle */}
</button> <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">
</div> <IconMenu className="w-4 h-4 text-gray-600 dark:text-gray-300" />
</div>
<img {/* Delete Button */}
src={item.src} <div className="absolute top-2 right-2 flex gap-2 z-10 opacity-0 group-hover:opacity-100 transition-opacity">
alt={`gallery-${index}`} <button
className="h-full w-full rounded-md object-cover" 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"
</div> >
))} <IconTrashLines className="w-4 h-4" />
</div> </button>
</div>
<img
src={item.src}
alt={`gallery-${index}`}
onDragStart={(e) => e.preventDefault()}
className="w-full h-auto"
/>
</div>
))}
</ReactSortable>
</>
)}
<Lightbox <Lightbox
styles={{ container: { backgroundColor: 'rgba(0,0,0,0.6)' } }} styles={{ container: { backgroundColor: 'rgba(0,0,0,0.6)' } }}

View File

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

View File

@ -22,6 +22,7 @@ import IconUser from '@/components/icon/icon-user';
import IconMail from '@/components/icon/icon-mail'; import IconMail from '@/components/icon/icon-mail';
import IconLockDots from '@/components/icon/icon-lock-dots'; import IconLockDots from '@/components/icon/icon-lock-dots';
import IconLogout from '@/components/icon/icon-logout'; import IconLogout from '@/components/icon/icon-logout';
import Cookies from 'universal-cookie';
import IconMenuDashboard from '@/components/icon/menu/icon-menu-dashboard'; import IconMenuDashboard from '@/components/icon/menu/icon-menu-dashboard';
import IconCaretDown from '@/components/icon/icon-caret-down'; import IconCaretDown from '@/components/icon/icon-caret-down';
import IconMenuApps from '@/components/icon/menu/icon-menu-apps'; import IconMenuApps from '@/components/icon/menu/icon-menu-apps';
@ -80,6 +81,29 @@ const Header = () => {
router.refresh(); 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) { function createMarkup(messages: any) {
return { __html: messages }; return { __html: messages };
} }
@ -225,10 +249,10 @@ const Header = () => {
</Link> </Link>
</li> </li>
<li className="border-t border-white-light dark:border-white-light/10"> <li className="border-t border-white-light dark:border-white-light/10">
<Link href="/auth/boxed-signin" className="!py-3 text-danger"> <button type="button" onClick={handleSignOut} className="!py-3 text-danger w-full text-left">
<IconLogout className="h-4.5 w-4.5 shrink-0 rotate-90 ltr:mr-2 rtl:ml-2" /> <IconLogout className="h-4.5 w-4.5 shrink-0 rotate-90 ltr:mr-2 rtl:ml-2" />
Sign Out Sign Out
</Link> </button>
</li> </li>
</ul> </ul>
</Dropdown> </Dropdown>

View File

@ -249,12 +249,36 @@ const Sidebar = () => {
</ul> </ul>
</li> </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" /> <IconMinus className="hidden h-5 w-4 flex-none" />
<span>{t('user_interface')}</span> <span>{t('blog')}</span>
</h2> </h2>
<li className="menu nav-item"> <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')}> <button type="button" className={`${currentMenu === 'component' ? 'active' : ''} nav-link group w-full`} onClick={() => toggleMenu('component')}>
<div className="flex items-center"> <div className="flex items-center">
<IconMenuComponents className="shrink-0 group-hover:!text-primary" /> <IconMenuComponents className="shrink-0 group-hover:!text-primary" />
@ -549,7 +573,7 @@ const Sidebar = () => {
</ul> </ul>
</AnimateHeight> </AnimateHeight>
</li> */} </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" /> <IconMinus className="hidden h-5 w-4 flex-none" />
<span>{t('user_and_pages')}</span> <span>{t('user_and_pages')}</span>

38
middleware.ts Normal file
View File

@ -0,0 +1,38 @@
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,7 +28,10 @@
"react-i18next": "^15.0.2", "react-i18next": "^15.0.2",
"react-perfect-scrollbar": "^1.5.8", "react-perfect-scrollbar": "^1.5.8",
"react-popper": "^2.3.0", "react-popper": "^2.3.0",
"react-quill": "^2.0.0",
"react-redux": "^9.1.2", "react-redux": "^9.1.2",
"react-sortablejs": "^6.1.4",
"sortablejs": "^1.15.7",
"sweetalert2": "^11.22.2", "sweetalert2": "^11.22.2",
"typescript": "^5.3.3", "typescript": "^5.3.3",
"universal-cookie": "^7.2.0", "universal-cookie": "^7.2.0",
@ -1063,6 +1066,15 @@
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz",
"integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==" "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": { "node_modules/@types/react": {
"version": "18.3.10", "version": "18.3.10",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.10.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.10.tgz",
@ -1101,6 +1113,13 @@
"@babel/runtime": "^7.9.2" "@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": { "node_modules/@types/use-sync-external-store": {
"version": "0.0.3", "version": "0.0.3",
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz",
@ -2036,11 +2055,26 @@
"node": ">= 6" "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": { "node_modules/client-only": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" "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": { "node_modules/clsx": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
@ -3134,11 +3168,29 @@
"node": ">=0.10.0" "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": { "node_modules/fast-deep-equal": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" "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": { "node_modules/fast-glob": {
"version": "3.3.2", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
@ -4815,6 +4867,12 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/parent-module": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@ -5271,6 +5329,74 @@
} }
] ]
}, },
"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": { "node_modules/react": {
"version": "18.3.1", "version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
@ -5364,6 +5490,21 @@
"react-dom": "^16.8.0 || ^17 || ^18" "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": { "node_modules/react-redux": {
"version": "9.1.2", "version": "9.1.2",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.1.2.tgz", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.1.2.tgz",
@ -5386,6 +5527,22 @@
} }
} }
}, },
"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": { "node_modules/read-cache": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@ -5691,6 +5848,12 @@
"url": "https://github.com/sponsors/isaacs" "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": { "node_modules/source-map": {
"version": "0.5.7", "version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
@ -6092,6 +6255,12 @@
"resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz",
"integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==" "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": { "node_modules/tiny-warning": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View File

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

View File

@ -1 +1,13 @@
export const Baseurl = "https://api.tamilculturewaterloo.org/api/" 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}`;
};