Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
897dbfe20d | ||
|
|
38313f6340 | ||
|
|
ca698ad1df | ||
|
|
83b891d8d8 | ||
|
|
4b9797e368 | ||
|
|
9f25fe3228 | ||
|
|
947f8114d2 | ||
|
|
4ab9fd5264 |
1
.env.example
Normal file
1
.env.example
Normal file
@ -0,0 +1 @@
|
||||
NEXT_PUBLIC_API_BASE_URL=http://localhost:3000/api/
|
||||
104
app/(defaults)/(blog)/blog/page.tsx
Normal file
104
app/(defaults)/(blog)/blog/page.tsx
Normal 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;
|
||||
217
app/(defaults)/(blog)/create-blog/page.tsx
Normal file
217
app/(defaults)/(blog)/create-blog/page.tsx
Normal 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
6
app/auth/login/page.tsx
Normal 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');
|
||||
}
|
||||
@ -1,8 +1,13 @@
|
||||
'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();
|
||||
@ -13,7 +18,9 @@ const LoginForm = () => {
|
||||
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 }>({});
|
||||
|
||||
// Generic handler for inputs
|
||||
@ -43,7 +50,7 @@ const LoginForm = () => {
|
||||
return newErrors;
|
||||
};
|
||||
|
||||
const submitForm = (e: React.FormEvent) => {
|
||||
const submitForm = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const validationErrors = validate();
|
||||
|
||||
@ -52,8 +59,25 @@ const LoginForm = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Form Data:', formData);
|
||||
router.push('/');
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@ -85,15 +109,22 @@ const LoginForm = () => {
|
||||
<input
|
||||
id="Password"
|
||||
name="password"
|
||||
type="password"
|
||||
type={showPassword ? "text" : "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}
|
||||
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>
|
||||
@ -103,8 +134,9 @@ 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}
|
||||
>
|
||||
Sign in
|
||||
{loading ? 'Signing in...' : 'Sign in'}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
'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;
|
||||
@ -85,9 +87,17 @@ const CreateEventForm: React.FC = () => {
|
||||
}
|
||||
|
||||
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: {
|
||||
"Content-Type": "multipart/form-data", // important for file upload
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
console.log("ImageUpload", ImageUpload)
|
||||
@ -99,7 +109,11 @@ const CreateEventForm: React.FC = () => {
|
||||
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)
|
||||
showMessage("Event Created Successfully", "success")
|
||||
router?.push(`/`)
|
||||
|
||||
@ -3,6 +3,7 @@ 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;
|
||||
@ -16,10 +17,18 @@ 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[] = [];
|
||||
|
||||
@ -32,6 +41,7 @@ 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) => {
|
||||
@ -44,6 +54,8 @@ 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);
|
||||
@ -54,6 +66,7 @@ const CreateEventGalleryForm: React.FC<EditEventFormProps> = ({ eventId }) => {
|
||||
e.preventDefault();
|
||||
if (!validateForm()) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const formData = new FormData();
|
||||
|
||||
@ -61,27 +74,32 @@ const CreateEventGalleryForm: React.FC<EditEventFormProps> = ({ eventId }) => {
|
||||
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)
|
||||
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 = {
|
||||
eventid: Number(eventId), // ensure number
|
||||
imageurl: uploadedUrls // API may expect 'imageurls' not 'imageurl'
|
||||
eventid: Number(eventId),
|
||||
imageurl: uploadedUrls
|
||||
};
|
||||
|
||||
console.log("Sending body:", body);
|
||||
|
||||
// Step 3: Call bulk API
|
||||
await axios.post(
|
||||
`https://api.tamilculturewaterloo.org/api/event-images/bulk`,
|
||||
buildApiUrl('event-images/bulk'),
|
||||
body
|
||||
);
|
||||
|
||||
router.push(`/event-gallery?eventid=${eventId}`);
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
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">
|
||||
<button
|
||||
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>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@ -3,6 +3,7 @@ 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;
|
||||
@ -43,7 +44,7 @@ const EditEventForm: React.FC<EditEventFormProps> = ({ eventId }) => {
|
||||
|
||||
const getEvent = async () => {
|
||||
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;
|
||||
|
||||
setFormData({
|
||||
@ -124,7 +125,7 @@ const EditEventForm: React.FC<EditEventFormProps> = ({ eventId }) => {
|
||||
imgFormData.append('file', formData.eventimageurl);
|
||||
|
||||
const uploadRes = await axios.post(
|
||||
'https://api.tamilculturewaterloo.org/api/upload/single',
|
||||
buildApiUrl('upload/single'),
|
||||
imgFormData,
|
||||
{ headers: { 'Content-Type': 'multipart/form-data' } }
|
||||
);
|
||||
@ -140,7 +141,7 @@ const EditEventForm: React.FC<EditEventFormProps> = ({ eventId }) => {
|
||||
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);
|
||||
router.push('/');
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
'use client';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useRef, 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,7 +8,22 @@ 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;
|
||||
@ -19,7 +34,12 @@ const ListOfEventsGallery: React.FC<EditEventFormProps> = ({ eventId }) => {
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
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(() => {
|
||||
if (eventId) {
|
||||
@ -29,23 +49,58 @@ const ListOfEventsGallery: React.FC<EditEventFormProps> = ({ eventId }) => {
|
||||
|
||||
const getEventGallery = async () => {
|
||||
try {
|
||||
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
|
||||
const res = await axios.get(buildApiUrl(`event-images/event/${eventId}`));
|
||||
const formatted: GalleryImage[] = res.data?.data?.map((img: any) => ({
|
||||
id: img.id,
|
||||
src: img.imageurl,
|
||||
title: img?.title || '',
|
||||
description: img?.description || '',
|
||||
}));
|
||||
setEventImages(formatted || []);
|
||||
})) || [];
|
||||
setEventImages(formatted);
|
||||
} catch (error) {
|
||||
console.log("error", error);
|
||||
console.log('error', error);
|
||||
}
|
||||
};
|
||||
|
||||
const showAlert = async (e: React.MouseEvent, item: any) => {
|
||||
e.stopPropagation(); // ✅ Prevents triggering the parent onClick (lightbox)
|
||||
const handleReorder = (newList: GalleryImage[]) => {
|
||||
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({
|
||||
icon: 'warning',
|
||||
@ -58,14 +113,14 @@ const ListOfEventsGallery: React.FC<EditEventFormProps> = ({ eventId }) => {
|
||||
}).then(async (result) => {
|
||||
if (result.isConfirmed) {
|
||||
try {
|
||||
await axios.delete(`https://api.tamilculturewaterloo.org/api/event-images/${item.id}`);
|
||||
await axios.delete(buildApiUrl(`event-images/${item.id}`));
|
||||
Swal.fire({
|
||||
title: 'Deleted!',
|
||||
text: 'Your file has been deleted.',
|
||||
icon: 'success',
|
||||
customClass: { popup: 'sweet-alerts' },
|
||||
});
|
||||
getEventGallery(); // refresh after delete
|
||||
getEventGallery();
|
||||
} catch (error) {
|
||||
Swal.fire({
|
||||
title: 'Error!',
|
||||
@ -80,45 +135,75 @@ const ListOfEventsGallery: React.FC<EditEventFormProps> = ({ eventId }) => {
|
||||
|
||||
return (
|
||||
<div className="panel">
|
||||
<div className='flex justify-between items-center'>
|
||||
<h5 className="mb-5 text-lg font-semibold dark:text-white-light">Gallery</h5>
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h5 className="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-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
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<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);
|
||||
}}
|
||||
{/* 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"
|
||||
>
|
||||
{/* 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"
|
||||
{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);
|
||||
}}
|
||||
>
|
||||
<IconTrashLines />
|
||||
</button>
|
||||
</div>
|
||||
{/* 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>
|
||||
|
||||
<img
|
||||
src={item.src}
|
||||
alt={`gallery-${index}`}
|
||||
className="h-full w-full rounded-md object-cover"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</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>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Lightbox
|
||||
styles={{ container: { backgroundColor: 'rgba(0,0,0,0.6)' } }}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
'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';
|
||||
@ -7,6 +8,7 @@ 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',
|
||||
@ -23,7 +25,16 @@ const ListOfEvents = () => {
|
||||
|
||||
const getEvents = async () => {
|
||||
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)
|
||||
setEvents(eventRes?.data?.data)
|
||||
} catch (error) {
|
||||
@ -59,9 +70,13 @@ const ListOfEvents = () => {
|
||||
padding: '2em',
|
||||
customClass: { popup: 'sweet-alerts' },
|
||||
}).then(async (result) => {
|
||||
if (result.isConfirmed) {
|
||||
if (result.isConfirmed) {
|
||||
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({
|
||||
title: 'Deleted!',
|
||||
text: 'Your file has been deleted.',
|
||||
|
||||
@ -22,6 +22,7 @@ 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';
|
||||
@ -80,6 +81,29 @@ 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 };
|
||||
}
|
||||
@ -225,10 +249,10 @@ const Header = () => {
|
||||
</Link>
|
||||
</li>
|
||||
<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" />
|
||||
Sign Out
|
||||
</Link>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</Dropdown>
|
||||
|
||||
@ -249,12 +249,36 @@ 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('user_interface')}</span>
|
||||
<span>{t('blog')}</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" />
|
||||
@ -549,7 +573,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>
|
||||
|
||||
38
middleware.ts
Normal file
38
middleware.ts
Normal 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
169
package-lock.json
generated
@ -28,7 +28,10 @@
|
||||
"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",
|
||||
@ -1063,6 +1066,15 @@
|
||||
"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",
|
||||
@ -1101,6 +1113,13 @@
|
||||
"@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",
|
||||
@ -2036,11 +2055,26 @@
|
||||
"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",
|
||||
@ -3134,11 +3168,29 @@
|
||||
"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",
|
||||
@ -4815,6 +4867,12 @@
|
||||
"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",
|
||||
@ -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": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||
@ -5364,6 +5490,21 @@
|
||||
"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",
|
||||
@ -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": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||
@ -5691,6 +5848,12 @@
|
||||
"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",
|
||||
@ -6092,6 +6255,12 @@
|
||||
"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",
|
||||
|
||||
@ -29,7 +29,10 @@
|
||||
"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",
|
||||
|
||||
BIN
public/assets/images/blog/image-1.jpg
Normal file
BIN
public/assets/images/blog/image-1.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
@ -677,3 +677,10 @@ 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;
|
||||
}
|
||||
|
||||
@ -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}`;
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user