event images added through drive link
This commit is contained in:
parent
897dbfe20d
commit
7daefa51be
2
.env
2
.env
@ -1 +1 @@
|
||||
NEXT_PUBLIC_API_BASE_URL=http://localhost:3001/api/
|
||||
NEXT_PUBLIC_API_BASE_URL=http://localhost:3006/api/
|
||||
53
app/(auth)/signup/page.tsx
Normal file
53
app/(auth)/signup/page.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import SignupForm from '@/components/auth/SignupForm';
|
||||
import IconFacebookCircle from '@/components/icon/icon-facebook-circle';
|
||||
import IconGoogle from '@/components/icon/icon-google';
|
||||
import IconInstagram from '@/components/icon/icon-instagram';
|
||||
import IconTwitter from '@/components/icon/icon-twitter';
|
||||
import { Metadata } from 'next';
|
||||
import Link from 'next/link';
|
||||
import React from 'react';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Sign Up',
|
||||
};
|
||||
|
||||
const CoverRegister = () => {
|
||||
return (
|
||||
<div>
|
||||
<div className="absolute inset-0">
|
||||
<img src="/assets/images/auth/bg-gradient.png" alt="image" className="h-full w-full object-cover" />
|
||||
</div>
|
||||
<div className="relative flex min-h-screen items-center justify-center bg-[url(/assets/images/auth/map.png)] bg-cover bg-center bg-no-repeat px-6 py-10 dark:bg-[#060818] sm:px-16">
|
||||
<img src="/assets/images/auth/coming-soon-object1.png" alt="image" className="absolute left-0 top-1/2 h-full max-h-[893px] -translate-y-1/2" />
|
||||
<img src="/assets/images/auth/coming-soon-object2.png" alt="image" className="absolute left-24 top-0 h-40 md:left-[30%]" />
|
||||
<img src="/assets/images/auth/coming-soon-object3.png" alt="image" className="absolute right-0 top-0 h-[300px]" />
|
||||
<img src="/assets/images/auth/polygon-object.svg" alt="image" className="absolute bottom-0 end-[28%]" />
|
||||
<div className="relative flex w-full max-w-[1502px] flex-col justify-between overflow-hidden rounded-md bg-white/60 backdrop-blur-lg dark:bg-black/50 lg:min-h-[758px] lg:flex-row lg:gap-10 xl:gap-0">
|
||||
<div className="relative hidden w-full items-center justify-center bg-[linear-gradient(225deg,rgba(239,18,98,1)_0%,rgba(67,97,238,1)_100%)] p-5 lg:inline-flex lg:max-w-[835px] xl:-ms-28 ltr:xl:skew-x-[14deg] rtl:xl:skew-x-[-14deg]">
|
||||
<div className="absolute inset-y-0 w-8 from-primary/10 via-transparent to-transparent ltr:-right-10 ltr:bg-gradient-to-r rtl:-left-10 rtl:bg-gradient-to-l xl:w-16 ltr:xl:-right-20 rtl:xl:-left-20"></div>
|
||||
<div className="ltr:xl:-skew-x-[14deg] rtl:xl:skew-x-[14deg]">
|
||||
<Link href="/" className="ms-10 block w-48 lg:w-72">
|
||||
<img src="/assets/images/auth/logo-white.svg" alt="Logo" className="w-full" />
|
||||
</Link>
|
||||
<div className="mt-24 hidden w-full max-w-[430px] lg:block">
|
||||
<img src="/assets/images/auth/login.svg" alt="Cover Image" className="w-full" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative flex w-full flex-col items-center justify-center gap-6 px-4 pb-16 pt-6 sm:px-6 lg:max-w-[667px]">
|
||||
<div className="w-full max-w-[440px] lg:mt-16">
|
||||
<div className="mb-10">
|
||||
<h1 className="text-3xl font-extrabold uppercase !leading-snug text-primary md:text-4xl">Sign up</h1>
|
||||
<p className="text-base font-bold leading-normal text-white-dark">Create an account to get started</p>
|
||||
</div>
|
||||
<SignupForm />
|
||||
</div>
|
||||
<p className="absolute bottom-6 w-full text-center dark:text-white">© {new Date().getFullYear()}.VRISTO All Rights Reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CoverRegister;
|
||||
17
app/(defaults)/event-gallery-full/page.tsx
Normal file
17
app/(defaults)/event-gallery-full/page.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
'use client'
|
||||
import React from 'react';
|
||||
import FullEventGallery from '@/components/gallery/FullEventGallery';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
|
||||
const EventGalleryFullPage = () => {
|
||||
const searchParams = useSearchParams();
|
||||
const eventId = searchParams.get('eventid');
|
||||
|
||||
return (
|
||||
<div className="py-8">
|
||||
<FullEventGallery eventId={eventId} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventGalleryFullPage;
|
||||
@ -67,6 +67,7 @@ const LoginForm = () => {
|
||||
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: '/' });
|
||||
localStorage.setItem('token', response.data.data.token);
|
||||
router.push('/');
|
||||
}
|
||||
} catch (error: any) {
|
||||
|
||||
173
components/auth/SignupForm.tsx
Normal file
173
components/auth/SignupForm.tsx
Normal file
@ -0,0 +1,173 @@
|
||||
'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 IconUser from '@/components/icon/icon-user';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import React, { useState } from 'react';
|
||||
import axios from 'axios';
|
||||
import Cookies from 'universal-cookie';
|
||||
import { buildApiUrl } from '@/utils/BaseUrl.utils';
|
||||
|
||||
const cookies = new Cookies();
|
||||
|
||||
const SignupForm = () => {
|
||||
const router = useRouter();
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
email: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [errors, setErrors] = useState<{ name?: string; email?: string; password?: string; confirmPassword?: string }>({});
|
||||
const [generalError, setGeneralError] = useState('');
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData((prev) => ({ ...prev, [name]: value }));
|
||||
setErrors((prev) => ({ ...prev, [name]: '' }));
|
||||
setGeneralError('');
|
||||
};
|
||||
|
||||
const validate = () => {
|
||||
const newErrors: { name?: string; email?: string; password?: string; confirmPassword?: string } = {};
|
||||
if (!formData.name.trim()) newErrors.name = 'Name is required';
|
||||
if (!formData.email.trim()) newErrors.email = 'Email is required';
|
||||
if (!formData.password.trim()) newErrors.password = 'Password is required';
|
||||
if (formData.password !== formData.confirmPassword) newErrors.confirmPassword = 'Passwords do not match';
|
||||
return newErrors;
|
||||
};
|
||||
|
||||
const submitForm = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const validation = validate();
|
||||
if (Object.keys(validation).length > 0) {
|
||||
setErrors(validation);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setGeneralError('');
|
||||
|
||||
try {
|
||||
const payload = { name: formData.name, email: formData.email, password: formData.password };
|
||||
const response = await axios.post(buildApiUrl('auth/register'), payload);
|
||||
|
||||
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('/');
|
||||
return;
|
||||
}
|
||||
|
||||
setGeneralError(response.data.message || 'Unable to register');
|
||||
} catch (error: any) {
|
||||
console.error('Signup error', error);
|
||||
const serverMessage = error.response?.data?.message || error.response?.data?.error || error.message;
|
||||
setGeneralError(serverMessage || 'Unable to register');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form className="space-y-5 dark:text-white" onSubmit={submitForm}>
|
||||
{generalError && <p className="text-red-500 text-sm mt-1">{generalError}</p>}
|
||||
<div>
|
||||
<label htmlFor="Name">Name</label>
|
||||
<div className="relative text-white-dark">
|
||||
<input
|
||||
id="Name"
|
||||
name="name"
|
||||
type="text"
|
||||
placeholder="Enter Name"
|
||||
className="form-input ps-10 placeholder:text-white-dark"
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<span className="absolute start-4 top-1/2 -translate-y-1/2">
|
||||
<IconUser fill={true} />
|
||||
</span>
|
||||
</div>
|
||||
{errors.name && <p className="text-red-500 text-sm mt-1">{errors.name}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="Email">Email</label>
|
||||
<div className="relative text-white-dark">
|
||||
<input
|
||||
id="Email"
|
||||
name="email"
|
||||
type="email"
|
||||
placeholder="Enter Email"
|
||||
className="form-input ps-10 placeholder:text-white-dark"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<span className="absolute start-4 top-1/2 -translate-y-1/2">
|
||||
<IconMail fill={true} />
|
||||
</span>
|
||||
</div>
|
||||
{errors.email && <p className="text-red-500 text-sm mt-1">{errors.email}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="Password">Password</label>
|
||||
<div className="relative text-white-dark">
|
||||
<input
|
||||
id="Password"
|
||||
name="password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
placeholder="Enter Password"
|
||||
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>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="ConfirmPassword">Confirm Password</label>
|
||||
<div className="relative text-white-dark">
|
||||
<input
|
||||
id="ConfirmPassword"
|
||||
name="confirmPassword"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
placeholder="Confirm Password"
|
||||
className="form-input ps-10 placeholder:text-white-dark"
|
||||
value={formData.confirmPassword}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<span className="absolute start-4 top-1/2 -translate-y-1/2">
|
||||
<IconLockDots />
|
||||
</span>
|
||||
</div>
|
||||
{errors.confirmPassword && <p className="text-red-500 text-sm mt-1">{errors.confirmPassword}</p>}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-gradient !mt-6 w-full border-0 uppercase shadow-[0_10px_20px_-10px_rgba(67,97,238,0.44)]"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Signing up...' : 'Sign up'}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default SignupForm;
|
||||
@ -15,47 +15,44 @@ interface EditEventFormProps {
|
||||
const CreateEventGalleryForm: React.FC<EditEventFormProps> = ({ eventId }) => {
|
||||
const router = useRouter();
|
||||
|
||||
const [selectedImages, setSelectedImages] = useState<File[]>([]);
|
||||
const [previewUrls, setPreviewUrls] = useState<string[]>([]);
|
||||
const [linkInput, setLinkInput] = useState('');
|
||||
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 parsedLinks = React.useMemo(() => {
|
||||
return linkInput.split(/[\n,]+/).map(l => l.trim()).filter(l => l !== '').map(link => {
|
||||
if (link.includes("drive.google.com")) {
|
||||
const match = link.match(/\/d\/([a-zA-Z0-9_-]+)/) || link.match(/id=([a-zA-Z0-9_-]+)/);
|
||||
if (match && match[1]) {
|
||||
return `https://drive.google.com/thumbnail?id=${match[1]}&sz=w1000-h1000`;
|
||||
}
|
||||
|
||||
const validImages: File[] = [];
|
||||
const previews: string[] = [];
|
||||
|
||||
files.forEach((file) => {
|
||||
if (file.type.startsWith('image/')) {
|
||||
validImages.push(file);
|
||||
previews.push(URL.createObjectURL(file));
|
||||
}
|
||||
return link;
|
||||
});
|
||||
}, [linkInput]);
|
||||
|
||||
setSelectedImages(prev => [...prev, ...validImages]);
|
||||
setPreviewUrls(prev => [...prev, ...previews]);
|
||||
setErrors(prev => ({ ...prev, images: '' })); // clear error if valid
|
||||
const handleLinkInputChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setLinkInput(e.target.value);
|
||||
if (parsedLinks.length > 20) {
|
||||
setErrors({ images: 'You can only add up to 20 images at a time' });
|
||||
} else {
|
||||
setErrors(prev => ({ ...prev, images: '' }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleImageDelete = (index: number) => {
|
||||
setSelectedImages(prev => prev.filter((_, i) => i !== index));
|
||||
setPreviewUrls(prev => prev.filter((_, i) => i !== index));
|
||||
const lines = linkInput.split(/[\n,]+/).map(l => l.trim()).filter(l => l !== '');
|
||||
lines.splice(index, 1);
|
||||
setLinkInput(lines.join('\n'));
|
||||
};
|
||||
|
||||
const validateForm = (): boolean => {
|
||||
const newErrors: FormErrors = {};
|
||||
|
||||
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';
|
||||
if (parsedLinks.length === 0) {
|
||||
newErrors.images = 'Please provide at least one image link';
|
||||
} else if (parsedLinks.length > 20) {
|
||||
newErrors.images = 'Only 20 images can be added at a time';
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
@ -68,28 +65,24 @@ const CreateEventGalleryForm: React.FC<EditEventFormProps> = ({ eventId }) => {
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const formData = new FormData();
|
||||
|
||||
selectedImages.forEach((file, index) => {
|
||||
formData.append(`files`, file);
|
||||
});
|
||||
|
||||
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 body for bulk save
|
||||
// Step 1: Prepare body for bulk save
|
||||
const body = {
|
||||
eventid: Number(eventId),
|
||||
imageurl: uploadedUrls
|
||||
imageurl: parsedLinks
|
||||
};
|
||||
|
||||
console.log("Sending body:", body);
|
||||
|
||||
// Step 3: Call bulk API
|
||||
const token = localStorage.getItem('token');
|
||||
await axios.post(
|
||||
buildApiUrl('event-images/bulk'),
|
||||
body
|
||||
body,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
router.push(`/event-gallery?eventid=${eventId}`);
|
||||
@ -107,29 +100,33 @@ const CreateEventGalleryForm: React.FC<EditEventFormProps> = ({ eventId }) => {
|
||||
<form onSubmit={handleSubmit} className="max-w-4xl mx-auto p-6 bg-white rounded shadow-md">
|
||||
<h2 className="text-xl font-bold mb-4">Upload Gallery Images</h2>
|
||||
|
||||
{/* File Input */}
|
||||
{/* Link Input */}
|
||||
<div className="mb-4">
|
||||
<label htmlFor="gallery" className="block font-medium">Select Images</label>
|
||||
<input
|
||||
type="file"
|
||||
<label htmlFor="gallery" className="block font-medium mb-1">Image Links (Google Drive etc.)</label>
|
||||
<textarea
|
||||
id="gallery"
|
||||
accept="image/*"
|
||||
multiple
|
||||
onChange={handleFileChange}
|
||||
rows={5}
|
||||
placeholder="Paste your Google Drive links here, one per line..."
|
||||
value={linkInput}
|
||||
onChange={handleLinkInputChange}
|
||||
className="w-full border rounded px-3 py-2"
|
||||
/>
|
||||
{errors.images && <p className="text-red-500 text-sm">{errors.images}</p>}
|
||||
<p className="text-gray-500 text-xs mt-1">You can add up to 20 images.</p>
|
||||
</div>
|
||||
|
||||
{/* Previews */}
|
||||
{previewUrls.length > 0 && (
|
||||
{parsedLinks.length > 0 && (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
||||
{previewUrls.map((url, index) => (
|
||||
{parsedLinks.map((url, index) => (
|
||||
<div key={index} className="relative border rounded shadow overflow-hidden">
|
||||
<img
|
||||
src={url}
|
||||
alt={`Preview ${index + 1}`}
|
||||
className="w-full h-40 object-cover"
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).src = "https://placehold.co/150x150?text=Invalid+Image+Link";
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@ -49,13 +49,23 @@ const ListOfEventsGallery: React.FC<EditEventFormProps> = ({ eventId }) => {
|
||||
|
||||
const getEventGallery = async () => {
|
||||
try {
|
||||
const res = await axios.get(buildApiUrl(`event-images/event/${eventId}`));
|
||||
const formatted: GalleryImage[] = res.data?.data?.map((img: any) => ({
|
||||
const token = localStorage.getItem("token");
|
||||
const res = await axios.get(buildApiUrl(`event-images/event/${eventId}`),
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
const formatted: GalleryImage[] = res.data?.data?.map((img: any) => {
|
||||
console.log("Image URL:", img.imageurl);
|
||||
|
||||
return {
|
||||
id: img.id,
|
||||
src: img.imageurl,
|
||||
title: img?.title || '',
|
||||
description: img?.description || '',
|
||||
})) || [];
|
||||
};
|
||||
}) || [];
|
||||
setEventImages(formatted);
|
||||
} catch (error) {
|
||||
console.log('error', error);
|
||||
@ -72,7 +82,14 @@ const ListOfEventsGallery: React.FC<EditEventFormProps> = ({ eventId }) => {
|
||||
id: item.id,
|
||||
sort_order: index,
|
||||
}));
|
||||
await axios.put(buildApiUrl('event-images/reorder'), { images });
|
||||
const token = localStorage.getItem("token");
|
||||
await axios.put(buildApiUrl('event-images/reorder'),
|
||||
{ images },
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
Swal.fire({
|
||||
title: 'Saved!',
|
||||
text: 'Image order saved successfully.',
|
||||
@ -113,7 +130,13 @@ const ListOfEventsGallery: React.FC<EditEventFormProps> = ({ eventId }) => {
|
||||
}).then(async (result) => {
|
||||
if (result.isConfirmed) {
|
||||
try {
|
||||
await axios.delete(buildApiUrl(`event-images/${item.id}`));
|
||||
const token = localStorage.getItem("token");
|
||||
await axios.delete(buildApiUrl(`event-images/${item.id}`),
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
Swal.fire({
|
||||
title: 'Deleted!',
|
||||
text: 'Your file has been deleted.',
|
||||
@ -197,7 +220,11 @@ const ListOfEventsGallery: React.FC<EditEventFormProps> = ({ eventId }) => {
|
||||
src={item.src}
|
||||
alt={`gallery-${index}`}
|
||||
onDragStart={(e) => e.preventDefault()}
|
||||
className="w-full h-auto"
|
||||
className="w-full h-64 object-cover rounded-md"
|
||||
onError={(e) => {
|
||||
(e.currentTarget as HTMLImageElement).src =
|
||||
"https://placehold.co/400x300?text=Image+Not+Found";
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@ -7,14 +7,15 @@ export function middleware(req: NextRequest) {
|
||||
// Allow public and framework paths without auth
|
||||
const allowlist = [
|
||||
'/login', // login page
|
||||
|
||||
'/signup', // signup page
|
||||
'/auth/cover-register',
|
||||
'/auth/cover-login',
|
||||
];
|
||||
|
||||
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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user