s22
parent
7258809230
commit
10a2c0c2c3
|
@ -11,12 +11,12 @@ export default defineConfig({
|
|||
},
|
||||
preview: {
|
||||
allowedHosts: ['siliconpin.cs1.hz.siliconpin.com'],
|
||||
},
|
||||
}
|
||||
},
|
||||
integrations: [tailwind(), react()],
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 4000
|
||||
port: 4000,
|
||||
},
|
||||
output: 'static',
|
||||
});
|
||||
|
|
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
|
@ -19,17 +19,25 @@
|
|||
"@shadcn/ui": "^0.0.4",
|
||||
"@types/date-fns": "^2.5.3",
|
||||
"@types/react": "^19.0.12",
|
||||
"@uiw/react-md-editor": "^3.25.6",
|
||||
"astro": "^5.5.2",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"lucide-react": "^0.484.0",
|
||||
"marked": "^15.0.8",
|
||||
"minio": "^8.0.5",
|
||||
"pocketbase": "^0.25.2",
|
||||
"postcss": "^8.5.3",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-qr-code": "^2.0.15",
|
||||
"react-router-dom": "^7.5.0",
|
||||
"react-router-dom": "^7.5.1",
|
||||
"react-simplemde-editor": "^5.2.0",
|
||||
"react-to-print": "^3.0.5",
|
||||
"rehype-rewrite": "^4.0.2",
|
||||
"simplemde": "^1.11.2",
|
||||
"tailwind-merge": "^3.0.2",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
|
|
|
@ -54,29 +54,12 @@ export function ContactForm() {
|
|||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-4">
|
||||
<div className="space-y-1 sm:space-y-2">
|
||||
<Label htmlFor="name" className="text-sm sm:text-base">Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
placeholder="Your name"
|
||||
value={formState.name}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className="text-sm sm:text-base"
|
||||
/>
|
||||
<Input id="name" name="name" placeholder="Your name" value={formState.name} onChange={handleChange} required className="text-sm sm:text-base" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-1 sm:space-y-2">
|
||||
<Label htmlFor="email" className="text-sm sm:text-base">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
placeholder="your.email@example.com"
|
||||
value={formState.email}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className="text-sm sm:text-base"
|
||||
/>
|
||||
<Input id="email" name="email" type="email" placeholder="your.email@example.com" value={formState.email} onChange={handleChange} required className="text-sm sm:text-base" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -84,26 +67,12 @@ export function ContactForm() {
|
|||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-4">
|
||||
<div className="space-y-1 sm:space-y-2">
|
||||
<Label htmlFor="company" className="text-sm sm:text-base">Company</Label>
|
||||
<Input
|
||||
id="company"
|
||||
name="company"
|
||||
placeholder="Your company name"
|
||||
value={formState.company}
|
||||
onChange={handleChange}
|
||||
className="text-sm sm:text-base"
|
||||
/>
|
||||
<Input id="company" name="company" placeholder="Your company name" value={formState.company} onChange={handleChange} className="text-sm sm:text-base" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-1 sm:space-y-2">
|
||||
<Label htmlFor="service" className="text-sm sm:text-base">Service Interest</Label>
|
||||
<Select
|
||||
id="service"
|
||||
name="service"
|
||||
value={formState.service}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className="text-sm sm:text-base"
|
||||
>
|
||||
<Select id="service" name="service" value={formState.service} onChange={handleChange} required className="text-sm sm:text-base">
|
||||
<option value="" disabled>Select a service</option>
|
||||
<option value="php">PHP Hosting</option>
|
||||
<option value="nodejs">Node.js Hosting</option>
|
||||
|
|
|
@ -7,7 +7,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/
|
|||
import { Eye, EyeOff, Loader2 } from "lucide-react";
|
||||
import { Separator } from "./ui/separator";
|
||||
import { Label } from "./ui/label";
|
||||
|
||||
import { useIsLoggedIn } from '../lib/isLoggedIn';
|
||||
interface AuthStatus {
|
||||
message: string;
|
||||
isError: boolean;
|
||||
|
@ -144,7 +144,16 @@ const LoginPage = () => {
|
|||
throw error; // Re-throw the error if you want calling functions to handle it
|
||||
}
|
||||
};
|
||||
|
||||
const { isLoggedIn, loading, error } = useIsLoggedIn();
|
||||
// console.log(isLoggedIn)
|
||||
if(loading){
|
||||
return <p>Loading...</p>
|
||||
}
|
||||
if(isLoggedIn){
|
||||
return (
|
||||
window.location.href = '/'
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center p-4">
|
||||
<Card className="w-full max-w-md shadow-lg rounded-xl overflow-hidden">
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import TopicItems from "./TopicItem";
|
||||
import { useIsLoggedIn } from '../lib/isLoggedIn';
|
||||
|
||||
const topicPageDesc = 'Cutting-edge discussions on tech, digital services, news, and digital freedom. Stay informed on AI, cybersecurity, privacy, and the future of innovation.';
|
||||
|
||||
export default function TopicCreation(props) {
|
||||
const { isLoggedIn, loading: authLoading, error: authError } = useIsLoggedIn();
|
||||
const [topics, setTopics] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchTopics = async () => {
|
||||
try {
|
||||
const res = await fetch('https://host-api.cs1.hz.siliconpin.com/v1/topics/?query=my-topics', {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP error! status: ${res.status}`);
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
setTopics(data.data || []);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchTopics();
|
||||
}, []);
|
||||
|
||||
if (authLoading || loading) {
|
||||
return <div className="loading-indicator">Loading...</div>;
|
||||
}
|
||||
|
||||
if (authError || error) {
|
||||
return <div className="error-message">Error loading data: {authError || error}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoggedIn && (
|
||||
<div className="container mx-auto flex justify-end gap-x-4 mb-4">
|
||||
<a href="/topic/new" className="create-new-link">Create New</a>
|
||||
<a href="/topic/my-topic">My Topics</a>
|
||||
</div>
|
||||
)}
|
||||
<TopicItems topics={topics} mytopic={props.mytopic || false} title="SoliconPin Topics" description={topicPageDesc} />
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,484 @@
|
|||
import React, { useState, useEffect, useRef } from "react";
|
||||
import MDEditor, { commands } from '@uiw/react-md-editor';
|
||||
import { Input } from './ui/input';
|
||||
import { Label } from './ui/label';
|
||||
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from './ui/select';
|
||||
import { Button } from './ui/button';
|
||||
import { Separator } from './ui/separator';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from './ui/dialog';
|
||||
import { CustomTabs } from './ui/tabs';
|
||||
|
||||
const TOPIC_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/topics/';
|
||||
const MINIO_UPLOAD_URL = 'https://your-minio-api-endpoint/upload';
|
||||
|
||||
const NewTopic = () => {
|
||||
|
||||
// Form state
|
||||
const [formData, setFormData] = useState({ status: 'draft', category: '', title: '', content: '', imageUrl: '' });
|
||||
// UI state
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [success, setSuccess] = useState(false);
|
||||
const [imageFile, setImageFile] = useState(null);
|
||||
const [uploadProgress, setUploadProgress] = useState(0);
|
||||
const [imageDialogOpen, setImageDialogOpen] = useState(false);
|
||||
const [imageUrlInput, setImageUrlInput] = useState('');
|
||||
const [imageUploadFile, setImageUploadFile] = useState(null);
|
||||
const [imageUploadPreview, setImageUploadPreview] = useState('');
|
||||
const [editorMode, setEditorMode] = useState('edit');
|
||||
|
||||
// Upload file to MinIO
|
||||
const uploadToMinIO = async (file, onProgress) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('bucket', 'siliconpin-uploads');
|
||||
formData.append('folder', 'topic-images');
|
||||
|
||||
try {
|
||||
const response = await fetch(MINIO_UPLOAD_URL, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Upload failed');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.url;
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// // Generate slug from title
|
||||
// useEffect(() => {
|
||||
// if (formData.title) {
|
||||
// const slug = formData.title
|
||||
// .toLowerCase()
|
||||
// .replace(/[^\w\s]/g, '')
|
||||
// .replace(/\s+/g, '-');
|
||||
// setFormData(prev => ({ ...prev, slug }));
|
||||
// }
|
||||
// }, [formData.title]);
|
||||
|
||||
const customImageCommand = {
|
||||
name: 'image',
|
||||
keyCommand: 'image',
|
||||
buttonProps: { 'aria-label': 'Insert image' },
|
||||
icon: (
|
||||
<svg width="12" height="12" viewBox="0 0 20 20">
|
||||
<path fill="currentColor" d="M15 9c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm4-7H1c-.55 0-1 .45-1 1v14c0 .55.45 1 1 1h18c.55 0 1-.45 1-1V3c0-.55-.45-1-1-1zm-1 13l-6-5-2 2-4-5-4 8V4h16v11z"/>
|
||||
</svg>
|
||||
),
|
||||
execute: () => {
|
||||
setImageDialogOpen(true);
|
||||
},
|
||||
};
|
||||
|
||||
// Get all default commands and replace the image command
|
||||
const allCommands = commands.getCommands().map(cmd => {
|
||||
if (cmd.name === 'image') {
|
||||
return customImageCommand;
|
||||
}
|
||||
return cmd;
|
||||
});
|
||||
|
||||
|
||||
// Handle image URL insertion
|
||||
const handleInsertImageUrl = () => {
|
||||
if (imageUrlInput) {
|
||||
const imgMarkdown = ``;
|
||||
const textarea = document.querySelector('.w-md-editor-text-input');
|
||||
if (textarea) {
|
||||
const startPos = textarea.selectionStart;
|
||||
const endPos = textarea.selectionEnd;
|
||||
const currentValue = formData.content;
|
||||
|
||||
const newValue =
|
||||
currentValue.substring(0, startPos) +
|
||||
imgMarkdown +
|
||||
currentValue.substring(endPos);
|
||||
|
||||
setFormData(prev => ({ ...prev, content: newValue }));
|
||||
}
|
||||
setImageDialogOpen(false);
|
||||
setImageUrlInput('');
|
||||
}
|
||||
};
|
||||
|
||||
// Handle image file selection
|
||||
const handleImageFileSelect = (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
setImageUploadFile(file);
|
||||
|
||||
// Create preview
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
setImageUploadPreview(reader.result);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
};
|
||||
|
||||
// Upload image file to MinIO and insert into editor
|
||||
const handleImageUpload = async () => {
|
||||
if (!imageUploadFile) return;
|
||||
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
setUploadProgress(0);
|
||||
|
||||
const uploadedUrl = await uploadToMinIO(imageUploadFile, (progress) => {
|
||||
setUploadProgress(progress);
|
||||
});
|
||||
|
||||
// Insert markdown for the uploaded image
|
||||
const imgMarkdown = ``;
|
||||
const textarea = document.querySelector('.w-md-editor-text-input');
|
||||
if (textarea) {
|
||||
const startPos = textarea.selectionStart;
|
||||
const endPos = textarea.selectionEnd;
|
||||
const currentValue = formData.content;
|
||||
|
||||
const newValue =
|
||||
currentValue.substring(0, startPos) +
|
||||
imgMarkdown +
|
||||
currentValue.substring(endPos);
|
||||
|
||||
setFormData(prev => ({ ...prev, content: newValue }));
|
||||
}
|
||||
|
||||
setImageDialogOpen(false);
|
||||
setImageUploadFile(null);
|
||||
setImageUploadPreview('');
|
||||
} catch (error) {
|
||||
setError('Failed to upload image: ' + error.message);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Form submission
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setIsSubmitting(true);
|
||||
setError(null);
|
||||
setUploadProgress(0);
|
||||
|
||||
try {
|
||||
// Upload featured image if selected
|
||||
let imageUrl = formData.imageUrl;
|
||||
if (imageFile) {
|
||||
imageUrl = await uploadToMinIO(imageFile, (progress) => {
|
||||
setUploadProgress(progress);
|
||||
});
|
||||
}
|
||||
|
||||
// Prepare payload
|
||||
const payload = {
|
||||
...formData,
|
||||
imageUrl
|
||||
};
|
||||
|
||||
// Submit to API
|
||||
const response = await fetch(`${TOPIC_API_URL}?query=create-new-topic`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.message || 'Failed to create topic');
|
||||
}
|
||||
|
||||
setSuccess(true);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
setUploadProgress(0);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle form field changes
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
// Handle featured image file selection
|
||||
const handleFileChange = (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
setImageFile(file);
|
||||
// Preview image
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
setFormData(prev => ({ ...prev, imageUrl: reader.result }));
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
};
|
||||
|
||||
// Reset form
|
||||
const resetForm = () => {
|
||||
setFormData({
|
||||
status: 'draft',
|
||||
category: '',
|
||||
title: '',
|
||||
slug: '',
|
||||
content: '',
|
||||
imageUrl: ''
|
||||
});
|
||||
setImageFile(null);
|
||||
setSuccess(false);
|
||||
setError(null);
|
||||
};
|
||||
|
||||
// Success state
|
||||
if (success) {
|
||||
return (
|
||||
<div className="container mx-auto px-4 text-center py-8">
|
||||
<h3 className="text-xl font-semibold text-green-600 mb-4">Topic created successfully!</h3>
|
||||
<div className="flex justify-center gap-4">
|
||||
<Button onClick={resetForm}>Create Another</Button>
|
||||
<Button variant="outline" onClick={() => window.location.href = '/'}>Return Home</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="max-w-3xl mx-auto bg-neutral-800 rounded-lg shadow-md p-6">
|
||||
<h2 className="text-2xl font-bold text-[#6d9e37] mb-2">Create New Topic</h2>
|
||||
<p className="text-gray-600 mb-6">Start a new discussion in the SiliconPin community</p>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="status">Status</Label>
|
||||
<Select
|
||||
name="status"
|
||||
required
|
||||
value={formData.status}
|
||||
onValueChange={(value) => setFormData(prev => ({ ...prev, status: value }))}
|
||||
>
|
||||
<SelectTrigger id="status" className="text-sm sm:text-base w-full">
|
||||
<SelectValue placeholder="Select status" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="draft">Draft</SelectItem>
|
||||
<SelectItem value="published">Published</SelectItem>
|
||||
<SelectItem value="archived">Archived</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="category">Category *</Label>
|
||||
<Select
|
||||
name="category"
|
||||
required
|
||||
value={formData.category}
|
||||
onValueChange={(value) => setFormData(prev => ({ ...prev, category: value }))}
|
||||
>
|
||||
<SelectTrigger id="category" className="text-sm sm:text-base w-full">
|
||||
<SelectValue placeholder="Select a category" />
|
||||
</SelectTrigger>
|
||||
<SelectContent position="popper" className="z-50">
|
||||
<SelectItem value="php">PHP Hosting</SelectItem>
|
||||
<SelectItem value="nodejs">Node.js Hosting</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Title and Slug */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="title">Title *</Label>
|
||||
<Input type="text" name="title" value={formData.title} onChange={handleChange} placeholder="Enter a descriptive title" required />
|
||||
</div>
|
||||
|
||||
{/* <div className="space-y-2">
|
||||
<Label htmlFor="slug">URL Slug</Label>
|
||||
<Input type="text" name="slug" value={formData.slug} onChange={handleChange} readOnly className="bg-gray-50" />
|
||||
<p className="text-xs text-gray-500">This will be used in the topic URL</p>
|
||||
</div> */}
|
||||
|
||||
{/* Content Editor with Preview Toggle */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<Label htmlFor="content">Content *</Label>
|
||||
<button
|
||||
type="button"
|
||||
size="sm"
|
||||
onClick={() => setEditorMode(editorMode === 'edit' ? 'preview' : 'edit')}
|
||||
className={`ml-2 ${editorMode !== 'edit' ? 'bg-[#6d9e37]' : ''} text-white border border-[#6d9e37] text-[#6d9e37] px-2 py-1 rounded-md`}
|
||||
>
|
||||
{editorMode === 'edit' ? 'Preview' : 'Edit'}
|
||||
</button>
|
||||
</div>
|
||||
<div data-color-mode="light">
|
||||
<MDEditor
|
||||
value={formData.content}
|
||||
onChange={(value) => setFormData(prev => ({ ...prev, content: value || '' }))}
|
||||
height={400}
|
||||
preview={editorMode}
|
||||
commands={allCommands}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Image Upload Dialog */}
|
||||
<Dialog open={imageDialogOpen} onOpenChange={setImageDialogOpen}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Insert Image</DialogTitle>
|
||||
</DialogHeader>
|
||||
<CustomTabs
|
||||
tabs={[
|
||||
{
|
||||
label: "From URL",
|
||||
value: "url",
|
||||
content: (
|
||||
<div className="space-y-4">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Enter image URL"
|
||||
value={imageUrlInput}
|
||||
onChange={(e) => setImageUrlInput(e.target.value)}
|
||||
/>
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleInsertImageUrl}
|
||||
disabled={!imageUrlInput}
|
||||
>
|
||||
Insert Image
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
label: "Upload",
|
||||
value: "upload",
|
||||
content: (
|
||||
<div className="space-y-4">
|
||||
<Input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleImageFileSelect}
|
||||
/>
|
||||
{imageUploadPreview && (
|
||||
<div className="mt-2">
|
||||
<img
|
||||
src={imageUploadPreview}
|
||||
alt="Preview"
|
||||
className="max-h-40 rounded-md border"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{uploadProgress > 0 && uploadProgress < 100 && (
|
||||
<div className="w-full bg-gray-200 rounded-full h-2.5">
|
||||
<div
|
||||
className="bg-blue-600 h-2.5 rounded-full"
|
||||
style={{ width: `${uploadProgress}%` }}
|
||||
></div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleImageUpload}
|
||||
disabled={!imageUploadFile || isSubmitting}
|
||||
>
|
||||
{isSubmitting ? 'Uploading...' : 'Upload & Insert'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Featured Image Upload */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="image">Featured Image</Label>
|
||||
<Input
|
||||
type="file"
|
||||
id="image"
|
||||
onChange={handleFileChange}
|
||||
accept="image/*"
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
|
||||
{uploadProgress > 0 && uploadProgress < 100 && (
|
||||
<div className="w-full bg-gray-200 rounded-full h-2.5">
|
||||
<div
|
||||
className="bg-blue-600 h-2.5 rounded-full"
|
||||
style={{ width: `${uploadProgress}%` }}
|
||||
></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{formData.imageUrl && (
|
||||
<div className="mt-2">
|
||||
<img
|
||||
src={formData.imageUrl}
|
||||
alt="Preview"
|
||||
className="max-h-40 rounded-md border"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Form Actions */}
|
||||
<div className="flex justify-end gap-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={resetForm}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="min-w-32"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="animate-spin">↻</span>
|
||||
Creating...
|
||||
</span>
|
||||
) : 'Create Topic'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="p-4 bg-red-50 text-red-600 rounded-md">
|
||||
Error: {error}
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewTopic;
|
|
@ -1,8 +1,9 @@
|
|||
import React from "react";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card";
|
||||
import { marked } from 'marked';
|
||||
|
||||
export default function TopicDetail(props) {
|
||||
console.log('All topic Form allTopic', props.allTopic)
|
||||
// console.log('All topic Form allTopic', props.allTopic);
|
||||
|
||||
if (!props.topic) {
|
||||
return <div>Topic not found</div>;
|
||||
}
|
||||
|
@ -12,7 +13,7 @@ console.log('All topic Form allTopic', props.allTopic)
|
|||
<article className="max-w-4xl mx-auto">
|
||||
<img src={props.topic.img} alt={props.topic.title} className="w-full h-96 object-cover rounded-lg mb-8" />
|
||||
<h1 className="text-4xl font-bold text-[#6d9e37] mb-4">{props.topic.title}</h1>
|
||||
<p className="font-light mb-8 text-justify" dangerouslySetInnerHTML={{__html: props.topic.content}}></p>
|
||||
<div className="font-light mb-8 text-justify prose max-w-none" dangerouslySetInnerHTML={{ __html: marked.parse(props.topic.content || '') }}></div>
|
||||
</article>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,528 @@
|
|||
import React, { useState, useEffect, useRef } from "react";
|
||||
import MDEditor, { commands } from '@uiw/react-md-editor';
|
||||
import { Input } from './ui/input';
|
||||
import { Label } from './ui/label';
|
||||
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from './ui/select';
|
||||
import { Button } from './ui/button';
|
||||
import { Separator } from './ui/separator';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from './ui/dialog';
|
||||
import { CustomTabs } from './ui/tabs';
|
||||
|
||||
|
||||
const TOPIC_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/topics/';
|
||||
const MINIO_UPLOAD_URL = 'https://your-minio-api-endpoint/upload';
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const slug = urlParams.get('slug');
|
||||
// console.log('find slug from url', slug);
|
||||
export default function EditTopic (){
|
||||
// const { slug } = useParams();
|
||||
// const navigate = useNavigate();
|
||||
// Form state
|
||||
const [formData, setFormData] = useState({ status: 'draft', category: '', slug: '', title: '', content: '', imageUrl: '' });
|
||||
// UI state
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [success, setSuccess] = useState(false);
|
||||
const [imageFile, setImageFile] = useState(null);
|
||||
const [uploadProgress, setUploadProgress] = useState(0);
|
||||
const [imageDialogOpen, setImageDialogOpen] = useState(false);
|
||||
const [imageUrlInput, setImageUrlInput] = useState('');
|
||||
const [imageUploadFile, setImageUploadFile] = useState(null);
|
||||
const [imageUploadPreview, setImageUploadPreview] = useState('');
|
||||
const [editorMode, setEditorMode] = useState('edit');
|
||||
|
||||
// Fetch topic data on component mount
|
||||
useEffect(() => {
|
||||
const fetchTopic = async () => {
|
||||
try {
|
||||
const response = await fetch(`${TOPIC_API_URL}?query=get-single-topic&slug=${slug}`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch topic');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const topic = data.data[0]
|
||||
console.log('Single topic data', data)
|
||||
setFormData({
|
||||
status: topic.status || 'draft',
|
||||
category: topic.category || '',
|
||||
title: topic.title || '',
|
||||
slug: topic.slug || '',
|
||||
content: topic.content || '',
|
||||
imageUrl: topic.imageUrl || ''
|
||||
});
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchTopic();
|
||||
}, [slug]);
|
||||
|
||||
// Upload file to MinIO (same as NewTopic)
|
||||
const uploadToMinIO = async (file, onProgress) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('bucket', 'siliconpin-uploads');
|
||||
formData.append('folder', 'topic-images');
|
||||
|
||||
try {
|
||||
const response = await fetch(MINIO_UPLOAD_URL, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Upload failed');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.url;
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Generate slug from title (same as NewTopic)
|
||||
// useEffect(() => {
|
||||
// if (formData.title) {
|
||||
// const newSlug = formData.title
|
||||
// .toLowerCase()
|
||||
// .replace(/[^\w\s]/g, '')
|
||||
// .replace(/\s+/g, '-');
|
||||
// setFormData(prev => ({ ...prev, slug: newSlug }));
|
||||
// }
|
||||
// }, [formData.title]);
|
||||
|
||||
// Custom image command for MDEditor (same as NewTopic)
|
||||
const customImageCommand = {
|
||||
name: 'image',
|
||||
keyCommand: 'image',
|
||||
buttonProps: { 'aria-label': 'Insert image' },
|
||||
icon: (
|
||||
<svg width="12" height="12" viewBox="0 0 20 20">
|
||||
<path fill="currentColor" d="M15 9c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm4-7H1c-.55 0-1 .45-1 1v14c0 .55.45 1 1 1h18c.55 0 1-.45 1-1V3c0-.55-.45-1-1-1zm-1 13l-6-5-2 2-4-5-4 8V4h16v11z"/>
|
||||
</svg>
|
||||
),
|
||||
execute: () => {
|
||||
setImageDialogOpen(true);
|
||||
},
|
||||
};
|
||||
|
||||
// Get all commands (same as NewTopic)
|
||||
const allCommands = commands.getCommands().map(cmd => {
|
||||
if (cmd.name === 'image') {
|
||||
return customImageCommand;
|
||||
}
|
||||
return cmd;
|
||||
});
|
||||
|
||||
// Handle image URL insertion (same as NewTopic)
|
||||
const handleInsertImageUrl = () => {
|
||||
if (imageUrlInput) {
|
||||
const imgMarkdown = ``;
|
||||
const textarea = document.querySelector('.w-md-editor-text-input');
|
||||
if (textarea) {
|
||||
const startPos = textarea.selectionStart;
|
||||
const endPos = textarea.selectionEnd;
|
||||
const currentValue = formData.content;
|
||||
|
||||
const newValue =
|
||||
currentValue.substring(0, startPos) +
|
||||
imgMarkdown +
|
||||
currentValue.substring(endPos);
|
||||
|
||||
setFormData(prev => ({ ...prev, content: newValue }));
|
||||
}
|
||||
setImageDialogOpen(false);
|
||||
setImageUrlInput('');
|
||||
}
|
||||
};
|
||||
|
||||
// Handle image file selection (same as NewTopic)
|
||||
const handleImageFileSelect = (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
setImageUploadFile(file);
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
setImageUploadPreview(reader.result);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
};
|
||||
|
||||
// Upload image file (same as NewTopic)
|
||||
const handleImageUpload = async () => {
|
||||
if (!imageUploadFile) return;
|
||||
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
setUploadProgress(0);
|
||||
|
||||
const uploadedUrl = await uploadToMinIO(imageUploadFile, (progress) => {
|
||||
setUploadProgress(progress);
|
||||
});
|
||||
|
||||
const imgMarkdown = ``;
|
||||
const textarea = document.querySelector('.w-md-editor-text-input');
|
||||
if (textarea) {
|
||||
const startPos = textarea.selectionStart;
|
||||
const endPos = textarea.selectionEnd;
|
||||
const currentValue = formData.content;
|
||||
|
||||
const newValue =
|
||||
currentValue.substring(0, startPos) +
|
||||
imgMarkdown +
|
||||
currentValue.substring(endPos);
|
||||
|
||||
setFormData(prev => ({ ...prev, content: newValue }));
|
||||
}
|
||||
|
||||
setImageDialogOpen(false);
|
||||
setImageUploadFile(null);
|
||||
setImageUploadPreview('');
|
||||
} catch (error) {
|
||||
setError('Failed to upload image: ' + error.message);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Form submission for EDIT
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setIsSubmitting(true);
|
||||
setError(null);
|
||||
setUploadProgress(0);
|
||||
|
||||
try {
|
||||
// Upload new featured image if selected
|
||||
let imageUrl = formData.imageUrl;
|
||||
if (imageFile) {
|
||||
imageUrl = await uploadToMinIO(imageFile, (progress) => {
|
||||
setUploadProgress(progress);
|
||||
});
|
||||
}
|
||||
|
||||
// Prepare payload
|
||||
const payload = {
|
||||
...formData,
|
||||
imageUrl
|
||||
};
|
||||
|
||||
// Submit to API (PUT request for update)
|
||||
const response = await fetch(`${TOPIC_API_URL}?query=update-topic&slug=${formData.slug}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.message || 'Failed to update topic');
|
||||
}
|
||||
|
||||
setSuccess(true);
|
||||
setTimeout(() => {
|
||||
navigate(`/topic/${formData.slug}`);
|
||||
}, 1500);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
setUploadProgress(0);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle form field changes (same as NewTopic)
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
// Handle featured image file selection (same as NewTopic)
|
||||
const handleFileChange = (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
setImageFile(file);
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
setFormData(prev => ({ ...prev, imageUrl: reader.result }));
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
};
|
||||
|
||||
// Loading state
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8 text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-[#6d9e37] mx-auto"></div>
|
||||
<p className="mt-4 text-gray-600">Topic Loading...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Success state
|
||||
if (success) {
|
||||
return (
|
||||
<div className="container mx-auto px-4 text-center py-8">
|
||||
<h3 className="text-xl font-semibold text-green-600 mb-4">Topic has been successfully updated!</h3>
|
||||
<p className="mb-4">You are being automatically redirected to the topic page...</p>
|
||||
<div className="flex justify-center gap-4">
|
||||
<Button onClick={() => navigate(`/topic/${formData.slug}`)}>Preview this Topic</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="max-w-3xl mx-auto bg-neutral-800 rounded-lg shadow-md p-6">
|
||||
<h2 className="text-2xl font-bold text-[#6d9e37] mb-2">Edit Topic</h2>
|
||||
<p className="text-gray-600 mb-6">{formData.title}</p>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="status">Status</Label>
|
||||
<Select
|
||||
name="status"
|
||||
required
|
||||
value={formData.status}
|
||||
onValueChange={(value) => setFormData(prev => ({ ...prev, status: value }))}
|
||||
>
|
||||
<SelectTrigger id="status" className="text-sm sm:text-base w-full">
|
||||
<SelectValue placeholder="Select Status" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="draft">Draft</SelectItem>
|
||||
<SelectItem value="published">Publish</SelectItem>
|
||||
<SelectItem value="archived">Archive</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="category">Category *</Label>
|
||||
<Select
|
||||
name="category"
|
||||
required
|
||||
value={formData.category}
|
||||
onValueChange={(value) => setFormData(prev => ({ ...prev, category: value }))}
|
||||
>
|
||||
<SelectTrigger id="category" className="text-sm sm:text-base w-full">
|
||||
<SelectValue placeholder="Select Category" />
|
||||
</SelectTrigger>
|
||||
<SelectContent position="popper" className="z-50">
|
||||
<SelectItem value="php">PHP Hosting</SelectItem>
|
||||
<SelectItem value="nodejs">Node.js Hosting</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Title and Slug */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="title">Title *</Label>
|
||||
<Input
|
||||
type="text"
|
||||
name="title"
|
||||
value={formData.title}
|
||||
onChange={handleChange}
|
||||
placeholder="Wriet Topic Title"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="slug">URL Slug</Label>
|
||||
<input
|
||||
type="hidden"
|
||||
name="slug"
|
||||
value={formData.slug}
|
||||
onChange={handleChange}
|
||||
className="bg-gray-50"
|
||||
/>
|
||||
<p className="text-xs text-gray-500">This will be used in the topic URL</p>
|
||||
</div>
|
||||
|
||||
{/* Content Editor */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<Label htmlFor="content">Content *</Label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setEditorMode(editorMode === 'edit' ? 'preview' : 'edit')}
|
||||
className={`ml-2 ${editorMode !== 'edit' ? 'bg-[#6d9e37]' : ''} text-white border border-[#6d9e37] text-[#6d9e37] px-2 py-1 rounded-md`}
|
||||
>
|
||||
{editorMode === 'edit' ? 'Preview' : 'Edit'}
|
||||
</button>
|
||||
</div>
|
||||
<div data-color-mode="light">
|
||||
<MDEditor
|
||||
value={formData.content}
|
||||
onChange={(value) => setFormData(prev => ({ ...prev, content: value || '' }))}
|
||||
height={400}
|
||||
preview={editorMode}
|
||||
commands={allCommands}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Image Upload Dialog (same as NewTopic) */}
|
||||
<Dialog open={imageDialogOpen} onOpenChange={setImageDialogOpen}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add Image</DialogTitle>
|
||||
</DialogHeader>
|
||||
<CustomTabs
|
||||
tabs={[
|
||||
{
|
||||
label: "URL",
|
||||
value: "url",
|
||||
content: (
|
||||
<div className="space-y-4">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Image URL"
|
||||
value={imageUrlInput}
|
||||
onChange={(e) => setImageUrlInput(e.target.value)}
|
||||
/>
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleInsertImageUrl}
|
||||
disabled={!imageUrlInput}
|
||||
>
|
||||
Upload Image
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
label: "Upload",
|
||||
value: "upload",
|
||||
content: (
|
||||
<div className="space-y-4">
|
||||
<Input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleImageFileSelect}
|
||||
/>
|
||||
{imageUploadPreview && (
|
||||
<div className="mt-2">
|
||||
<img
|
||||
src={imageUploadPreview}
|
||||
alt="Preview"
|
||||
className="max-h-40 rounded-md border"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{uploadProgress > 0 && uploadProgress < 100 && (
|
||||
<div className="w-full bg-gray-200 rounded-full h-2.5">
|
||||
<div
|
||||
className="bg-blue-600 h-2.5 rounded-full"
|
||||
style={{ width: `${uploadProgress}%` }}
|
||||
></div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleImageUpload}
|
||||
disabled={!imageUploadFile || isSubmitting}
|
||||
>
|
||||
{isSubmitting ? 'Uploading...' : 'Upload'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Featured Image Upload */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="image">Featured Image</Label>
|
||||
<Input
|
||||
type="file"
|
||||
id="image"
|
||||
onChange={handleFileChange}
|
||||
accept="image/*"
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
|
||||
{uploadProgress > 0 && uploadProgress < 100 && (
|
||||
<div className="w-full bg-gray-200 rounded-full h-2.5">
|
||||
<div
|
||||
className="bg-blue-600 h-2.5 rounded-full"
|
||||
style={{ width: `${uploadProgress}%` }}
|
||||
></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{formData.imageUrl && (
|
||||
<div className="mt-2">
|
||||
<img
|
||||
src={formData.imageUrl}
|
||||
alt="Preview"
|
||||
className="max-h-40 rounded-md border"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">Current Image</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Form Actions */}
|
||||
<div className="flex justify-end gap-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => navigate(-1)}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="min-w-32 bg-[#6d9e37] hover:bg-[#5a8a2a]"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="animate-spin">↻</span>
|
||||
Uploading...
|
||||
</span>
|
||||
) : 'Update'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="p-4 bg-red-50 text-red-600 rounded-md">
|
||||
Error: {error}
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -1,33 +1,75 @@
|
|||
import React from "react";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card";
|
||||
import { Button } from "./ui/button";
|
||||
import { useIsLoggedIn } from '../lib/isLoggedIn';
|
||||
import { Pencil, Trash2 } from "lucide-react"; // Import icons for better visual
|
||||
|
||||
export default function TopicItems(props) {
|
||||
const { isLoggedIn, loading, error, sessionData } = useIsLoggedIn();
|
||||
|
||||
if (loading) {
|
||||
return <div className="loading-indicator">Loading...</div>;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <div className="error-message">Error loading authentication status</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="container mx-auto px-4">
|
||||
<div className="py-8 text-center">
|
||||
<h2 className="text-3xl sm:text-4xl font-bold text-[#6d9e37] mb-3 sm:mb-4">
|
||||
{props.title}
|
||||
</h2>
|
||||
<p className="text-lg sm:text-xl max-w-3xl mx-auto text-neutral-300">
|
||||
{props.description}
|
||||
</p>
|
||||
<h2 className="text-3xl sm:text-4xl font-bold text-[#6d9e37] mb-3 sm:mb-4">{props.title}</h2>
|
||||
<p className="text-lg sm:text-xl max-w-3xl mx-auto text-neutral-300">{props.description}</p>
|
||||
</div>
|
||||
|
||||
{
|
||||
props.topics.length > 0 ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{props.topics.map((topic) => (
|
||||
{
|
||||
props.topics.map((topic) => (
|
||||
<a href={`/topic/${topic.slug}`} key={topic.id} className="hover:scale-[1.02] transition-transform duration-200 ">
|
||||
<Card className="h-full flex flex-col">
|
||||
<img src={topic.img} alt={topic.title} className="aspect-video object-cover rounded-t-lg" loading="lazy" />
|
||||
<Card className="h-full flex flex-col group relative">
|
||||
<div className="relative">
|
||||
<img src={topic.img} alt={topic.title} className="aspect-video object-cover rounded-t-lg relative" loading="lazy" />
|
||||
</div>
|
||||
<CardContent className="flex-1 p-6">
|
||||
<CardTitle className="mb-2">{topic.title}</CardTitle>
|
||||
<CardDescription className="line-clamp-3">
|
||||
<CardTitle className="mb-2 line-clamp-1">{topic.title}</CardTitle>
|
||||
<CardDescription className="line-clamp-3 mb-4">
|
||||
{topic.description}
|
||||
</CardDescription>
|
||||
{
|
||||
// console.log('props.mytopic', props.mytopic)
|
||||
}
|
||||
{isLoggedIn && sessionData.user_email === topic.user && props.mytopic === true && (
|
||||
<div className="flex justify-end gap-2 mt-4 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
|
||||
<Button variant="outline" size="sm" className="gap-1"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
window.location.href = `/topic/edit?slug=${topic.slug}`
|
||||
// Handle edit action
|
||||
}}
|
||||
>
|
||||
<Pencil className="h-4 w-4" />Edit
|
||||
</Button>
|
||||
<Button size="sm" className="gap-1 bg-red-500 hover:bg-red-600"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
// Handle delete action
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />Delete
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</a>
|
||||
))}
|
||||
))
|
||||
}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-center">No Topic Found <a href="/topic/new" className="text-[#6d9e37]"> Click Here</a> to Create Once</p>
|
||||
)
|
||||
}
|
||||
</section>
|
||||
);
|
||||
}
|
|
@ -1,10 +1,29 @@
|
|||
import React, {useEffect, useState} from "react";
|
||||
import React from "react";
|
||||
import TopicItems from "./TopicItem";
|
||||
let topicPageDesc = 'Cutting-edge discussions on tech, digital services, news, and digital freedom. Stay informed on AI, cybersecurity, privacy, and the future of innovation.';
|
||||
import { useIsLoggedIn } from '../lib/isLoggedIn';
|
||||
|
||||
const topicPageDesc = 'Cutting-edge discussions on tech, digital services, news, and digital freedom. Stay informed on AI, cybersecurity, privacy, and the future of innovation.';
|
||||
|
||||
export default function TopicCreation(props) {
|
||||
const { isLoggedIn, loading, error } = useIsLoggedIn();
|
||||
|
||||
if (loading) {
|
||||
return <div className="loading-indicator">Loading...</div>;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <div className="error-message">Error loading authentication status</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoggedIn && (
|
||||
<div className="container mx-auto flex justify-end gap-x-4">
|
||||
<a href="/topic/new" className="create-new-link">Create New</a>
|
||||
<a href="/topic/my-topic">My Topics</a>
|
||||
</div>
|
||||
)}
|
||||
<TopicItems topics={props.topics} title="SoliconPin Topics" description={topicPageDesc} />
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
|
@ -25,7 +25,7 @@ export default function ProfilePage() {
|
|||
const [userData, setUserData] = useState<UserData | null>(null);
|
||||
const [invoiceList, setInvoiceList] = useState<any[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const USER_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/index.php';
|
||||
const USER_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/';
|
||||
const INVOICE_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/invoice/';
|
||||
useEffect(() => {
|
||||
const fetchSessionData = async () => {
|
||||
|
@ -73,18 +73,29 @@ export default function ProfilePage() {
|
|||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
fetchSessionData();
|
||||
getInvoiceListData();
|
||||
}, []);
|
||||
|
||||
const sessionLogOut = () => {
|
||||
fetch(`${USER_API_URL}?query=logout`, {
|
||||
method: 'GET',
|
||||
credentials: 'include'
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if(data.success === true){
|
||||
window.location.href = '/';
|
||||
}
|
||||
console.log('Logout Console', data.success);
|
||||
})
|
||||
}
|
||||
if (error) {
|
||||
return <div>Error: {error}</div>;
|
||||
}
|
||||
|
||||
if (!userData) {
|
||||
return <div>Loading profile data...</div>;
|
||||
return (<div>Loading profile data...</div>);
|
||||
}
|
||||
return (
|
||||
<div className="space-y-6 container mx-auto">
|
||||
|
@ -148,8 +159,8 @@ export default function ProfilePage() {
|
|||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
invoiceList.map((invoice) => (
|
||||
<tr key={invoice.id}>
|
||||
invoiceList.map((invoice, index) => (
|
||||
<tr key={index}>
|
||||
<td>{invoice.invoice_number}</td>
|
||||
<td>{invoice.invoice_date}</td>
|
||||
<td>{invoice.notes ? invoice.notes : ''}</td>
|
||||
|
@ -200,6 +211,7 @@ export default function ProfilePage() {
|
|||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col space-y-4">
|
||||
<Button onClick={sessionLogOut} className="bg-red-500 hover:bg-red-600">Logout</Button>
|
||||
<Button className="bg-red-500 hover:bg-red-600">Delete account</Button>
|
||||
<Button variant="outline">Export data</Button>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,137 @@
|
|||
import * as React from "react";
|
||||
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
const Select = SelectPrimitive.Root;
|
||||
const SelectGroup = SelectPrimitive.Group;
|
||||
const SelectValue = SelectPrimitive.Value;
|
||||
|
||||
const SelectTrigger = React.forwardRef(
|
||||
({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-10 w-full items-center justify-between rounded-md border border-neutral-600 bg-neutral-800 px-3 py-2 text-sm",
|
||||
"focus:outline-none focus:ring-2 focus:ring-[#6d9e37] focus:ring-offset-2 focus:ring-offset-neutral-900",
|
||||
"disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDownIcon className="h-4 w-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
)
|
||||
);
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
||||
|
||||
const SelectContent = React.forwardRef(
|
||||
({ className, children, position = "popper", ...props }, ref) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-50 min-w-[8rem] overflow-hidden rounded-md border border-neutral-600 bg-neutral-800 shadow-md animate-in fade-in-80",
|
||||
position === "popper" && "translate-y-1",
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
)
|
||||
);
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
||||
|
||||
const SelectLabel = React.forwardRef(
|
||||
({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName;
|
||||
|
||||
const SelectItem = React.forwardRef(
|
||||
({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none",
|
||||
"focus:bg-neutral-700 focus:text-white",
|
||||
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
)
|
||||
);
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
||||
|
||||
const SelectSeparator = React.forwardRef(
|
||||
({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-neutral-600", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|
||||
|
||||
const ChevronDownIcon = React.forwardRef((props, ref) => (
|
||||
<svg
|
||||
ref={ref}
|
||||
width="15"
|
||||
height="15"
|
||||
viewBox="0 0 15 15"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M3.13523 6.15803C3.3241 5.95657 3.64052 5.94637 3.84197 6.13523L7.5 9.56464L11.158 6.13523C11.3595 5.94637 11.6759 5.95657 11.8648 6.15803C12.0536 6.35949 12.0434 6.67591 11.842 6.86477L7.84197 10.6148C7.64964 10.7951 7.35036 10.7951 7.15803 10.6148L3.15803 6.86477C2.95657 6.67591 2.94637 6.35949 3.13523 6.15803Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
));
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
};
|
||||
|
||||
export {
|
||||
Select as SelectProps,
|
||||
SelectGroup as SelectGroupProps,
|
||||
SelectValue as SelectValueProps,
|
||||
SelectTrigger as SelectTriggerProps,
|
||||
SelectContent as SelectContentProps,
|
||||
SelectLabel as SelectLabelProps,
|
||||
SelectItem as SelectItemProps,
|
||||
SelectSeparator as SelectSeparatorProps,
|
||||
};
|
|
@ -1,130 +0,0 @@
|
|||
import * as React from "react";
|
||||
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
const Select = SelectPrimitive.Root;
|
||||
|
||||
const SelectGroup = SelectPrimitive.Group;
|
||||
|
||||
const SelectValue = SelectPrimitive.Value;
|
||||
|
||||
const SelectTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-10 w-full items-center justify-between rounded-md border border-neutral-600 bg-neutral-800 px-3 py-2 text-sm",
|
||||
"focus:outline-none focus:ring-2 focus:ring-[#6d9e37] focus:ring-offset-2 focus:ring-offset-neutral-900",
|
||||
"disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDownIcon className="h-4 w-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
));
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
||||
|
||||
const SelectContent = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-50 min-w-[8rem] overflow-hidden rounded-md border border-neutral-600 bg-neutral-800 shadow-md animate-in fade-in-80",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SelectPrimitive.Viewport className="p-1">
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
));
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
||||
|
||||
const SelectLabel = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName;
|
||||
|
||||
const SelectItem = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none",
|
||||
"focus:bg-neutral-700 focus:text-white",
|
||||
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
));
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
||||
|
||||
const SelectSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-neutral-600", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|
||||
|
||||
// ChevronDownIcon component
|
||||
const ChevronDownIcon = React.forwardRef<
|
||||
SVGSVGElement,
|
||||
React.SVGProps<SVGSVGElement>
|
||||
>((props, ref) => (
|
||||
<svg
|
||||
ref={ref}
|
||||
width="15"
|
||||
height="15"
|
||||
viewBox="0 0 15 15"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M3.13523 6.15803C3.3241 5.95657 3.64052 5.94637 3.84197 6.13523L7.5 9.56464L11.158 6.13523C11.3595 5.94637 11.6759 5.95657 11.8648 6.15803C12.0536 6.35949 12.0434 6.67591 11.842 6.86477L7.84197 10.6148C7.64964 10.7951 7.35036 10.7951 7.15803 10.6148L3.15803 6.86477C2.95657 6.67591 2.94637 6.35949 3.13523 6.15803Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
));
|
||||
ChevronDownIcon.displayName = "ChevronDownIcon";
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
};
|
|
@ -1,33 +1,48 @@
|
|||
import * as React from "react";
|
||||
import * as Tabs from "@radix-ui/react-tabs";
|
||||
import * as RadixTabs from "@radix-ui/react-tabs";
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
export interface CustomTabsProps {
|
||||
// Main Tabs component
|
||||
const Tabs = RadixTabs.Root;
|
||||
|
||||
// Subcomponents
|
||||
const TabsList = RadixTabs.List;
|
||||
const TabsTrigger = RadixTabs.Trigger;
|
||||
const TabsContent = RadixTabs.Content;
|
||||
|
||||
// CustomTabs component (your existing implementation)
|
||||
interface CustomTabsProps {
|
||||
tabs: { label: string; value: string; content: React.ReactNode }[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const CustomTabs: React.FC<CustomTabsProps> = ({ tabs, className }) => {
|
||||
return (
|
||||
<Tabs.Root defaultValue={tabs[0]?.value} className={cn("w-full", className)}>
|
||||
<Tabs.List className="flex border-b border-neutral-600 bg-neutral-800">
|
||||
<Tabs defaultValue={tabs[0]?.value} className={cn("w-full", className)}>
|
||||
<TabsList className="flex border-b border-neutral-600 bg-neutral-800">
|
||||
{tabs.map((tab) => (
|
||||
<Tabs.Trigger
|
||||
<TabsTrigger
|
||||
key={tab.value}
|
||||
value={tab.value}
|
||||
className="px-4 py-2 text-sm text-neutral-400 hover:text-white focus-visible:ring-2 focus-visible:ring-[#6d9e37]"
|
||||
>
|
||||
{tab.label}
|
||||
</Tabs.Trigger>
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</Tabs.List>
|
||||
</TabsList>
|
||||
{tabs.map((tab) => (
|
||||
<Tabs.Content key={tab.value} value={tab.value} className="p-4 text-neutral-300">
|
||||
<TabsContent key={tab.value} value={tab.value} className="p-4 text-neutral-300">
|
||||
{tab.content}
|
||||
</Tabs.Content>
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs.Root>
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
|
||||
export { CustomTabs };
|
||||
export {
|
||||
Tabs,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
TabsContent,
|
||||
CustomTabs
|
||||
};
|
|
@ -98,12 +98,8 @@ const organizationSchema = {
|
|||
<a href="/services" class="hover:text-[#6d9e37] transition-colors">Services</a>
|
||||
<a href="/topic" class="hover:text-[#6d9e37] transition-colors">Topic</a>
|
||||
<a href="/contact" class="hover:text-[#6d9e37] transition-colors">Contact</a>
|
||||
<a
|
||||
href="/get-started"
|
||||
class="inline-flex items-center justify-center rounded-md bg-[#6d9e37] px-4 py-2 text-sm font-medium text-white hover:bg-[#598035] transition-colors"
|
||||
>
|
||||
Get Started
|
||||
</a>
|
||||
<a href="/profile" class="hover:text-[#6d9e37] transition-colors">Profile</a>
|
||||
<a href="/get-started" class="inline-flex items-center justify-center rounded-md bg-[#6d9e37] px-4 py-2 text-sm font-medium text-white hover:bg-[#598035] transition-colors">Get Started</a>
|
||||
</nav>
|
||||
<!-- Mobile menu button -->
|
||||
<div class="md:hidden relative">
|
||||
|
@ -140,9 +136,7 @@ const organizationSchema = {
|
|||
</div>
|
||||
|
||||
<div class="hidden sm:block">
|
||||
<a href="/suggestion-or-report" class="text-[#6d9e37] hover:text-white transition-colors">
|
||||
Suggestion or Report
|
||||
</a>
|
||||
<a href="/suggestion-or-report" class="text-[#6d9e37] hover:text-white transition-colors">Suggestion or Report</a>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-5 mt-4 sm:mt-0">
|
||||
|
@ -237,12 +231,12 @@ const organizationSchema = {
|
|||
<span class="text-xs mt-1">Start</span>
|
||||
</a>
|
||||
<a href="/about-us" class="flex flex-col items-center justify-center w-full h-full text-[#6d9e37] hover:text-white transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><circle cx="12" cy="10" r="3"></circle><path d="M7 20.662V19c0-1.18.822-2.2 2-2.5a5.5 5.5 0 0 1 6 0c1.178.3 2 1.323 2 2.5v1.662"></path></svg>
|
||||
<svg width="20px" height="20px" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#6d9e37"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <title>about</title> <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g id="about-white" fill="#6d9e37" transform="translate(42.666667, 42.666667)"> <path d="M213.333333,3.55271368e-14 C95.51296,3.55271368e-14 3.55271368e-14,95.51168 3.55271368e-14,213.333333 C3.55271368e-14,331.153707 95.51296,426.666667 213.333333,426.666667 C331.154987,426.666667 426.666667,331.153707 426.666667,213.333333 C426.666667,95.51168 331.154987,3.55271368e-14 213.333333,3.55271368e-14 Z M213.333333,384 C119.227947,384 42.6666667,307.43872 42.6666667,213.333333 C42.6666667,119.227947 119.227947,42.6666667 213.333333,42.6666667 C307.44,42.6666667 384,119.227947 384,213.333333 C384,307.43872 307.44,384 213.333333,384 Z M240.04672,128 C240.04672,143.46752 228.785067,154.666667 213.55008,154.666667 C197.698773,154.666667 186.713387,143.46752 186.713387,127.704107 C186.713387,112.5536 197.99616,101.333333 213.55008,101.333333 C228.785067,101.333333 240.04672,112.5536 240.04672,128 Z M192.04672,192 L234.713387,192 L234.713387,320 L192.04672,320 L192.04672,192 Z" id="Shape"> </path> </g> </g> </g></svg>
|
||||
<span class="text-xs mt-1">About</span>
|
||||
</a>
|
||||
<a href="/contact" class="flex flex-col items-center justify-center w-full h-full text-[#6d9e37] hover:text-white transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline></svg>
|
||||
<span class="text-xs mt-1">Contact</span>
|
||||
<a href="/profile" class="flex flex-col items-center justify-center w-full h-full text-[#6d9e37] hover:text-white transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><circle cx="12" cy="10" r="3"></circle><path d="M7 20.662V19c0-1.18.822-2.2 2-2.5a5.5 5.5 0 0 1 6 0c1.178.3 2 1.323 2 2.5v1.662"></path></svg>
|
||||
<span class="text-xs mt-1">Profile</span>
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
|
||||
const USER_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/index.php';
|
||||
|
||||
export const useIsLoggedIn = () => {
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(null); // null means "unknown"
|
||||
const [sessionData, setSessionData] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const checkLoginStatus = async () => {
|
||||
try {
|
||||
const response = await fetch(`${USER_API_URL}?query=isLoggedIn`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
const data = await response.json();
|
||||
setIsLoggedIn(!!data?.isLoggedIn);
|
||||
setSessionData(data?.session_data || null);
|
||||
setLoading(false);
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
setIsLoggedIn(false);
|
||||
setSessionData(null);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
checkLoginStatus();
|
||||
}, []);
|
||||
|
||||
return { isLoggedIn, sessionData, loading, error };
|
||||
};
|
||||
|
||||
const IsLoggedIn = ({ children, fallback = null }) => {
|
||||
const { isLoggedIn, loading, error } = useIsLoggedIn();
|
||||
|
||||
if (loading) return <div>Loading...</div>;
|
||||
if (error) return <div>Error checking login status</div>;
|
||||
|
||||
return isLoggedIn ? children : fallback;
|
||||
};
|
||||
|
||||
export default IsLoggedIn;
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
import Layout from "../../layouts/Layout.astro";
|
||||
import EditTopic from "../../components/TopicEdit";
|
||||
---
|
||||
<Layout title="Edit Topic | SiliconPin" >
|
||||
<EditTopic client:load client:only="react" />
|
||||
</Layout>
|
|
@ -17,7 +17,7 @@ try {
|
|||
|
||||
const data = await response.json();
|
||||
topics = data.data || [];
|
||||
console.log('Topic Data', data);
|
||||
// console.log('Topic Data', data);
|
||||
} catch (error) {
|
||||
console.error('An error occurred', error);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
import Layout from "../../layouts/Layout.astro";
|
||||
import MyTopicList from "../../components/MyTopic";
|
||||
---
|
||||
|
||||
<Layout title="Topics">
|
||||
<MyTopicList client:load mytopic={true} />
|
||||
</Layout>
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
import Layout from "../../layouts/Layout.astro";
|
||||
import NewTopic from "../../components/NewTopic";
|
||||
---
|
||||
|
||||
<Layout title="Create New Topic | Siliconpin">
|
||||
<NewTopic client:load client:only="react" />
|
||||
</Layout>
|
Loading…
Reference in New Issue