fix
This commit is contained in:
parent
ffb9dadeb4
commit
5d0a2ebf78
|
|
@ -1,7 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { Editor } from '@tinymce/tinymce-react';
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { Editor } from "@tinymce/tinymce-react";
|
||||
|
||||
interface OptimizedEditorProps {
|
||||
initialData?: string;
|
||||
|
|
@ -13,10 +13,10 @@ interface OptimizedEditorProps {
|
|||
}
|
||||
|
||||
const OptimizedEditor: React.FC<OptimizedEditorProps> = ({
|
||||
initialData = '',
|
||||
initialData = "",
|
||||
onChange,
|
||||
height = 400,
|
||||
placeholder = 'Start typing...',
|
||||
placeholder = "Start typing...",
|
||||
disabled = false,
|
||||
readOnly = false,
|
||||
}) => {
|
||||
|
|
@ -42,14 +42,30 @@ const OptimizedEditor: React.FC<OptimizedEditorProps> = ({
|
|||
height,
|
||||
menubar: false,
|
||||
plugins: [
|
||||
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
|
||||
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
|
||||
'insertdatetime', 'media', 'table', 'code', 'help', 'wordcount'
|
||||
"advlist",
|
||||
"autolink",
|
||||
"lists",
|
||||
"link",
|
||||
"image",
|
||||
"charmap",
|
||||
"preview",
|
||||
"anchor",
|
||||
"searchreplace",
|
||||
"visualblocks",
|
||||
"code",
|
||||
"fullscreen",
|
||||
"insertdatetime",
|
||||
"media",
|
||||
"table",
|
||||
"code",
|
||||
"help",
|
||||
"wordcount",
|
||||
],
|
||||
toolbar: 'undo redo | blocks | ' +
|
||||
'bold italic forecolor | alignleft aligncenter ' +
|
||||
'alignright alignjustify | bullist numlist outdent indent | ' +
|
||||
'removeformat | table | code | help',
|
||||
toolbar:
|
||||
"undo redo | blocks | " +
|
||||
"bold italic forecolor | alignleft aligncenter " +
|
||||
"alignright alignjustify | bullist numlist outdent indent | " +
|
||||
"removeformat | table | code | help",
|
||||
content_style: `
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
|
|
@ -63,24 +79,24 @@ const OptimizedEditor: React.FC<OptimizedEditorProps> = ({
|
|||
}
|
||||
`,
|
||||
placeholder,
|
||||
readonly: readOnly,
|
||||
// readonly: readOnly,
|
||||
branding: false,
|
||||
elementpath: false,
|
||||
resize: false,
|
||||
statusbar: false,
|
||||
// Performance optimizations
|
||||
cache_suffix: '?v=1.0',
|
||||
cache_suffix: "?v=1.0",
|
||||
browser_spellcheck: false,
|
||||
gecko_spellcheck: false,
|
||||
// Auto-save feature
|
||||
auto_save: true,
|
||||
auto_save_interval: '30s',
|
||||
auto_save_interval: "30s",
|
||||
// Better mobile support
|
||||
mobile: {
|
||||
theme: 'silver',
|
||||
plugins: ['lists', 'autolink', 'link', 'image', 'table'],
|
||||
toolbar: 'bold italic | bullist numlist | link image'
|
||||
}
|
||||
theme: "silver",
|
||||
plugins: ["lists", "autolink", "link", "image", "table"],
|
||||
toolbar: "bold italic | bullist numlist | link image",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import React, { useRef, useState, useEffect } from 'react';
|
||||
import { Editor } from '@tinymce/tinymce-react';
|
||||
import React, { useRef, useState, useEffect } from "react";
|
||||
import { Editor } from "@tinymce/tinymce-react";
|
||||
|
||||
interface TinyMCEEditorProps {
|
||||
initialData?: string;
|
||||
|
|
@ -11,7 +11,7 @@ interface TinyMCEEditorProps {
|
|||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
readOnly?: boolean;
|
||||
features?: 'basic' | 'standard' | 'full';
|
||||
features?: "basic" | "standard" | "full";
|
||||
toolbar?: string;
|
||||
language?: string;
|
||||
uploadUrl?: string;
|
||||
|
|
@ -22,21 +22,21 @@ interface TinyMCEEditorProps {
|
|||
}
|
||||
|
||||
const TinyMCEEditor: React.FC<TinyMCEEditorProps> = ({
|
||||
initialData = '',
|
||||
initialData = "",
|
||||
onChange,
|
||||
onReady,
|
||||
height = 400,
|
||||
placeholder = 'Start typing...',
|
||||
placeholder = "Start typing...",
|
||||
disabled = false,
|
||||
readOnly = false,
|
||||
features = 'standard',
|
||||
features = "standard",
|
||||
toolbar,
|
||||
language = 'en',
|
||||
language = "en",
|
||||
uploadUrl,
|
||||
uploadHeaders,
|
||||
className = '',
|
||||
className = "",
|
||||
autoSave = true,
|
||||
autoSaveInterval = 30000
|
||||
autoSaveInterval = 30000,
|
||||
}) => {
|
||||
const editorRef = useRef<any>(null);
|
||||
const [isEditorLoaded, setIsEditorLoaded] = useState(false);
|
||||
|
|
@ -47,35 +47,74 @@ const TinyMCEEditor: React.FC<TinyMCEEditorProps> = ({
|
|||
const getFeatureConfig = (featureLevel: string) => {
|
||||
const configs = {
|
||||
basic: {
|
||||
plugins: ['lists', 'link', 'autolink', 'wordcount'],
|
||||
toolbar: 'bold italic | bullist numlist | link',
|
||||
menubar: false
|
||||
plugins: ["lists", "link", "autolink", "wordcount"],
|
||||
toolbar: "bold italic | bullist numlist | link",
|
||||
menubar: false,
|
||||
},
|
||||
standard: {
|
||||
plugins: [
|
||||
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
|
||||
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
|
||||
'insertdatetime', 'media', 'table', 'help', 'wordcount'
|
||||
"advlist",
|
||||
"autolink",
|
||||
"lists",
|
||||
"link",
|
||||
"image",
|
||||
"charmap",
|
||||
"preview",
|
||||
"anchor",
|
||||
"searchreplace",
|
||||
"visualblocks",
|
||||
"code",
|
||||
"fullscreen",
|
||||
"insertdatetime",
|
||||
"media",
|
||||
"table",
|
||||
"help",
|
||||
"wordcount",
|
||||
],
|
||||
toolbar: 'undo redo | blocks | ' +
|
||||
'bold italic forecolor | alignleft aligncenter ' +
|
||||
'alignright alignjustify | bullist numlist outdent indent | ' +
|
||||
'removeformat | table | code | help',
|
||||
menubar: false
|
||||
toolbar:
|
||||
"undo redo | blocks | " +
|
||||
"bold italic forecolor | alignleft aligncenter " +
|
||||
"alignright alignjustify | bullist numlist outdent indent | " +
|
||||
"removeformat | table | code | help",
|
||||
menubar: false,
|
||||
},
|
||||
full: {
|
||||
plugins: [
|
||||
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
|
||||
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
|
||||
'insertdatetime', 'media', 'table', 'help', 'wordcount', 'emoticons',
|
||||
'paste', 'textcolor', 'colorpicker', 'hr', 'pagebreak', 'nonbreaking',
|
||||
'toc', 'imagetools', 'textpattern', 'codesample'
|
||||
"advlist",
|
||||
"autolink",
|
||||
"lists",
|
||||
"link",
|
||||
"image",
|
||||
"charmap",
|
||||
"preview",
|
||||
"anchor",
|
||||
"searchreplace",
|
||||
"visualblocks",
|
||||
"code",
|
||||
"fullscreen",
|
||||
"insertdatetime",
|
||||
"media",
|
||||
"table",
|
||||
"help",
|
||||
"wordcount",
|
||||
"emoticons",
|
||||
"paste",
|
||||
"textcolor",
|
||||
"colorpicker",
|
||||
"hr",
|
||||
"pagebreak",
|
||||
"nonbreaking",
|
||||
"toc",
|
||||
"imagetools",
|
||||
"textpattern",
|
||||
"codesample",
|
||||
],
|
||||
toolbar: 'undo redo | formatselect | bold italic backcolor | ' +
|
||||
'alignleft aligncenter alignright alignjustify | ' +
|
||||
'bullist numlist outdent indent | removeformat | help',
|
||||
menubar: 'file edit view insert format tools table help'
|
||||
}
|
||||
toolbar:
|
||||
"undo redo | formatselect | bold italic backcolor | " +
|
||||
"alignleft aligncenter alignright alignjustify | " +
|
||||
"bullist numlist outdent indent | removeformat | help",
|
||||
menubar: "file edit view insert format tools table help",
|
||||
},
|
||||
};
|
||||
return configs[featureLevel as keyof typeof configs] || configs.standard;
|
||||
};
|
||||
|
|
@ -95,7 +134,7 @@ const TinyMCEEditor: React.FC<TinyMCEEditorProps> = ({
|
|||
}
|
||||
|
||||
// Set up word count tracking
|
||||
editor.on('keyup', () => {
|
||||
editor.on("keyup", () => {
|
||||
const count = editor.plugins.wordcount.body.getCharacterCount();
|
||||
setWordCount(count);
|
||||
});
|
||||
|
|
@ -104,24 +143,24 @@ const TinyMCEEditor: React.FC<TinyMCEEditorProps> = ({
|
|||
if (autoSave && !readOnly) {
|
||||
setInterval(() => {
|
||||
const content = editor.getContent();
|
||||
localStorage.setItem('tinymce-autosave', content);
|
||||
localStorage.setItem("tinymce-autosave", content);
|
||||
setLastSaved(new Date());
|
||||
}, autoSaveInterval);
|
||||
}
|
||||
|
||||
// Fix cursor jumping issues
|
||||
editor.on('keyup', (e: any) => {
|
||||
editor.on("keyup", (e: any) => {
|
||||
// Prevent cursor jumping on content changes
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
editor.on('input', (e: any) => {
|
||||
editor.on("input", (e: any) => {
|
||||
// Prevent unnecessary re-renders
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
// Handle paste events properly
|
||||
editor.on('paste', (e: any) => {
|
||||
editor.on("paste", (e: any) => {
|
||||
// Allow default paste behavior
|
||||
return true;
|
||||
});
|
||||
|
|
@ -130,23 +169,23 @@ const TinyMCEEditor: React.FC<TinyMCEEditorProps> = ({
|
|||
const handleImageUpload = (blobInfo: any, progress: any) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!uploadUrl) {
|
||||
reject('No upload URL configured');
|
||||
reject("No upload URL configured");
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', blobInfo.blob(), blobInfo.filename());
|
||||
formData.append("file", blobInfo.blob(), blobInfo.filename());
|
||||
|
||||
fetch(uploadUrl, {
|
||||
method: 'POST',
|
||||
method: "POST",
|
||||
headers: uploadHeaders || {},
|
||||
body: formData
|
||||
body: formData,
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
.then((response) => response.json())
|
||||
.then((result) => {
|
||||
resolve(result.url);
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
|
|
@ -165,7 +204,7 @@ const TinyMCEEditor: React.FC<TinyMCEEditorProps> = ({
|
|||
resize: false,
|
||||
statusbar: !readOnly,
|
||||
// Performance optimizations
|
||||
cache_suffix: '?v=1.0',
|
||||
cache_suffix: "?v=1.0",
|
||||
browser_spellcheck: false,
|
||||
gecko_spellcheck: false,
|
||||
// Content styling
|
||||
|
|
@ -188,40 +227,41 @@ const TinyMCEEditor: React.FC<TinyMCEEditorProps> = ({
|
|||
// Image upload configuration
|
||||
images_upload_handler: uploadUrl ? handleImageUpload : undefined,
|
||||
automatic_uploads: !!uploadUrl,
|
||||
file_picker_types: 'image',
|
||||
file_picker_types: "image",
|
||||
// Better mobile support
|
||||
mobile: {
|
||||
theme: 'silver',
|
||||
plugins: ['lists', 'autolink', 'link', 'image', 'table'],
|
||||
toolbar: 'bold italic | bullist numlist | link image'
|
||||
theme: "silver",
|
||||
plugins: ["lists", "autolink", "link", "image", "table"],
|
||||
toolbar: "bold italic | bullist numlist | link image",
|
||||
},
|
||||
// Paste configuration
|
||||
paste_as_text: false,
|
||||
paste_enable_default_filters: true,
|
||||
paste_word_valid_elements: 'b,strong,i,em,h1,h2,h3,h4,h5,h6',
|
||||
paste_retain_style_properties: 'color background-color font-size font-weight',
|
||||
paste_word_valid_elements: "b,strong,i,em,h1,h2,h3,h4,h5,h6",
|
||||
paste_retain_style_properties:
|
||||
"color background-color font-size font-weight",
|
||||
// Table configuration
|
||||
table_default_styles: {
|
||||
width: '100%'
|
||||
width: "100%",
|
||||
},
|
||||
table_default_attributes: {
|
||||
border: '1'
|
||||
border: "1",
|
||||
},
|
||||
// Code configuration
|
||||
codesample_languages: [
|
||||
{ text: 'HTML/XML', value: 'markup' },
|
||||
{ text: 'JavaScript', value: 'javascript' },
|
||||
{ text: 'CSS', value: 'css' },
|
||||
{ text: 'PHP', value: 'php' },
|
||||
{ text: 'Python', value: 'python' },
|
||||
{ text: 'Java', value: 'java' },
|
||||
{ text: 'C', value: 'c' },
|
||||
{ text: 'C++', value: 'cpp' }
|
||||
{ text: "HTML/XML", value: "markup" },
|
||||
{ text: "JavaScript", value: "javascript" },
|
||||
{ text: "CSS", value: "css" },
|
||||
{ text: "PHP", value: "php" },
|
||||
{ text: "Python", value: "python" },
|
||||
{ text: "Java", value: "java" },
|
||||
{ text: "C", value: "c" },
|
||||
{ text: "C++", value: "cpp" },
|
||||
],
|
||||
// ...feature config
|
||||
...featureConfig,
|
||||
// Custom toolbar if provided
|
||||
...(toolbar && { toolbar })
|
||||
...(toolbar && { toolbar }),
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -232,7 +272,7 @@ const TinyMCEEditor: React.FC<TinyMCEEditorProps> = ({
|
|||
onEditorChange={handleEditorChange}
|
||||
disabled={disabled}
|
||||
apiKey={process.env.NEXT_PUBLIC_TINYMCE_API_KEY}
|
||||
init={editorConfig}
|
||||
// init={editorConfig}
|
||||
/>
|
||||
|
||||
{/* Status bar */}
|
||||
|
|
@ -240,7 +280,7 @@ const TinyMCEEditor: React.FC<TinyMCEEditorProps> = ({
|
|||
<div className="text-xs text-gray-500 mt-2 flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<span>
|
||||
{autoSave && !readOnly ? 'Auto-save enabled' : 'Read-only mode'}
|
||||
{autoSave && !readOnly ? "Auto-save enabled" : "Read-only mode"}
|
||||
</span>
|
||||
{lastSaved && autoSave && !readOnly && (
|
||||
<span>• Last saved: {lastSaved.toLocaleTimeString()}</span>
|
||||
|
|
@ -255,7 +295,12 @@ const TinyMCEEditor: React.FC<TinyMCEEditorProps> = ({
|
|||
|
||||
{/* Performance indicator */}
|
||||
<div className="text-xs text-gray-400 mt-1">
|
||||
Bundle size: {features === 'basic' ? '~150KB' : features === 'standard' ? '~200KB' : '~300KB'}
|
||||
Bundle size:{" "}
|
||||
{features === "basic"
|
||||
? "~150KB"
|
||||
: features === "standard"
|
||||
? "~200KB"
|
||||
: "~300KB"}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
// import {createSharedPathnamesNavigation} from 'next-intl/navigation';
|
||||
// import {locales} from '@/config';
|
||||
|
||||
// export const {Link, redirect, usePathname, useRouter} =
|
||||
// createSharedPathnamesNavigation({locales,});
|
||||
|
|
@ -1,213 +1,75 @@
|
|||
"use client"
|
||||
"use client";
|
||||
import * as React from "react";
|
||||
import { DayPicker } from "react-day-picker";
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
} from "lucide-react"
|
||||
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button, buttonVariants } from "@/components/ui/button"
|
||||
import { cn } from "@/lib/utils";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
|
||||
function Calendar({
|
||||
className,
|
||||
classNames,
|
||||
showOutsideDays = true,
|
||||
captionLayout = "label",
|
||||
buttonVariant = "ghost",
|
||||
formatters,
|
||||
components,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DayPicker> & {
|
||||
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
|
||||
}) {
|
||||
const defaultClassNames = getDefaultClassNames()
|
||||
|
||||
}: React.ComponentProps<typeof DayPicker>) {
|
||||
return (
|
||||
<DayPicker
|
||||
showOutsideDays={showOutsideDays}
|
||||
className={cn(
|
||||
"bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
|
||||
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
|
||||
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
|
||||
className
|
||||
)}
|
||||
captionLayout={captionLayout}
|
||||
formatters={{
|
||||
formatMonthDropdown: (date) =>
|
||||
date.toLocaleString("default", { month: "short" }),
|
||||
...formatters,
|
||||
}}
|
||||
className={cn("p-3", className)}
|
||||
classNames={{
|
||||
root: cn("w-fit", defaultClassNames.root),
|
||||
months: cn(
|
||||
"flex gap-4 flex-col md:flex-row relative",
|
||||
defaultClassNames.months
|
||||
months: "flex flex-col sm:flex-row gap-2",
|
||||
month: "flex flex-col gap-4",
|
||||
caption: "flex justify-center pt-1 relative items-center w-full",
|
||||
caption_label: "text-sm font-medium",
|
||||
nav: "flex items-center gap-1",
|
||||
nav_button: cn(
|
||||
buttonVariants({ variant: "outline" }),
|
||||
"size-7 bg-transparent p-0 opacity-50 hover:opacity-100"
|
||||
),
|
||||
month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
|
||||
nav: cn(
|
||||
"flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between",
|
||||
defaultClassNames.nav
|
||||
),
|
||||
button_previous: cn(
|
||||
buttonVariants({ variant: buttonVariant }),
|
||||
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
|
||||
defaultClassNames.button_previous
|
||||
),
|
||||
button_next: cn(
|
||||
buttonVariants({ variant: buttonVariant }),
|
||||
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
|
||||
defaultClassNames.button_next
|
||||
),
|
||||
month_caption: cn(
|
||||
"flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)",
|
||||
defaultClassNames.month_caption
|
||||
),
|
||||
dropdowns: cn(
|
||||
"w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5",
|
||||
defaultClassNames.dropdowns
|
||||
),
|
||||
dropdown_root: cn(
|
||||
"relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md",
|
||||
defaultClassNames.dropdown_root
|
||||
),
|
||||
dropdown: cn(
|
||||
"absolute bg-popover inset-0 opacity-0",
|
||||
defaultClassNames.dropdown
|
||||
),
|
||||
caption_label: cn(
|
||||
"select-none font-medium",
|
||||
captionLayout === "label"
|
||||
? "text-sm"
|
||||
: "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5",
|
||||
defaultClassNames.caption_label
|
||||
),
|
||||
table: "w-full border-collapse",
|
||||
weekdays: cn("flex", defaultClassNames.weekdays),
|
||||
weekday: cn(
|
||||
"text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none",
|
||||
defaultClassNames.weekday
|
||||
),
|
||||
week: cn("flex w-full mt-2", defaultClassNames.week),
|
||||
week_number_header: cn(
|
||||
"select-none w-(--cell-size)",
|
||||
defaultClassNames.week_number_header
|
||||
),
|
||||
week_number: cn(
|
||||
"text-[0.8rem] select-none text-muted-foreground",
|
||||
defaultClassNames.week_number
|
||||
nav_button_previous: "absolute left-1",
|
||||
nav_button_next: "absolute right-1",
|
||||
table: "w-full border-collapse space-x-1",
|
||||
head_row: "flex",
|
||||
head_cell:
|
||||
"text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
|
||||
row: "flex w-full mt-2",
|
||||
cell: cn(
|
||||
"relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-range-end)]:rounded-r-md",
|
||||
props.mode === "range"
|
||||
? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
|
||||
: "[&:has([aria-selected])]:rounded-md"
|
||||
),
|
||||
day: cn(
|
||||
"relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
|
||||
defaultClassNames.day
|
||||
buttonVariants({ variant: "ghost" }),
|
||||
"size-8 p-0 font-normal aria-selected:opacity-100"
|
||||
),
|
||||
range_start: cn(
|
||||
"rounded-l-md bg-accent",
|
||||
defaultClassNames.range_start
|
||||
),
|
||||
range_middle: cn("rounded-none", defaultClassNames.range_middle),
|
||||
range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
|
||||
today: cn(
|
||||
"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
|
||||
defaultClassNames.today
|
||||
),
|
||||
outside: cn(
|
||||
"text-muted-foreground aria-selected:text-muted-foreground",
|
||||
defaultClassNames.outside
|
||||
),
|
||||
disabled: cn(
|
||||
"text-muted-foreground opacity-50",
|
||||
defaultClassNames.disabled
|
||||
),
|
||||
hidden: cn("invisible", defaultClassNames.hidden),
|
||||
day_range_start:
|
||||
"day-range-start aria-selected:bg-primary aria-selected:text-primary-foreground",
|
||||
day_range_end:
|
||||
"day-range-end aria-selected:bg-primary aria-selected:text-primary-foreground",
|
||||
day_selected:
|
||||
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
|
||||
day_today: "bg-accent text-accent-foreground",
|
||||
day_outside:
|
||||
"day-outside text-muted-foreground aria-selected:text-muted-foreground",
|
||||
day_disabled: "text-muted-foreground opacity-50",
|
||||
day_range_middle:
|
||||
"aria-selected:bg-accent aria-selected:text-accent-foreground",
|
||||
day_hidden: "invisible",
|
||||
...classNames,
|
||||
}}
|
||||
components={{
|
||||
Root: ({ className, rootRef, ...props }) => {
|
||||
return (
|
||||
<div
|
||||
data-slot="calendar"
|
||||
ref={rootRef}
|
||||
className={cn(className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
},
|
||||
Chevron: ({ className, orientation, ...props }) => {
|
||||
if (orientation === "left") {
|
||||
return (
|
||||
<ChevronLeftIcon className={cn("size-4", className)} {...props} />
|
||||
)
|
||||
components={
|
||||
{
|
||||
// IconLeft: ({ className, ...props }) => (
|
||||
// <ChevronLeft className={cn("size-4", className)} {...props} />
|
||||
// ),
|
||||
// IconRight: ({ className, ...props }) => (
|
||||
// <ChevronRight className={cn("size-4", className)} {...props} />
|
||||
// ),
|
||||
}
|
||||
|
||||
if (orientation === "right") {
|
||||
return (
|
||||
<ChevronRightIcon
|
||||
className={cn("size-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ChevronDownIcon className={cn("size-4", className)} {...props} />
|
||||
)
|
||||
},
|
||||
DayButton: CalendarDayButton,
|
||||
WeekNumber: ({ children, ...props }) => {
|
||||
return (
|
||||
<td {...props}>
|
||||
<div className="flex size-(--cell-size) items-center justify-center text-center">
|
||||
{children}
|
||||
</div>
|
||||
</td>
|
||||
)
|
||||
},
|
||||
...components,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CalendarDayButton({
|
||||
className,
|
||||
day,
|
||||
modifiers,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DayButton>) {
|
||||
const defaultClassNames = getDefaultClassNames()
|
||||
|
||||
const ref = React.useRef<HTMLButtonElement>(null)
|
||||
React.useEffect(() => {
|
||||
if (modifiers.focused) ref.current?.focus()
|
||||
}, [modifiers.focused])
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
data-day={day.date.toLocaleDateString()}
|
||||
data-selected-single={
|
||||
modifiers.selected &&
|
||||
!modifiers.range_start &&
|
||||
!modifiers.range_end &&
|
||||
!modifiers.range_middle
|
||||
}
|
||||
data-range-start={modifiers.range_start}
|
||||
data-range-end={modifiers.range_end}
|
||||
data-range-middle={modifiers.range_middle}
|
||||
className={cn(
|
||||
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70",
|
||||
defaultClassNames.day,
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Calendar, CalendarDayButton }
|
||||
export { Calendar };
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import {
|
||||
FacebookLoginResponse,
|
||||
FacebookLoginError,
|
||||
FacebookUser,
|
||||
FacebookSDKInitOptions
|
||||
} from '@/types/facebook-login';
|
||||
FacebookSDKInitOptions,
|
||||
} from "@/types/facebook-login";
|
||||
|
||||
export interface UseFacebookLoginOptions extends FacebookSDKInitOptions {}
|
||||
|
||||
|
|
@ -13,19 +13,25 @@ export const useFacebookLogin = (options: UseFacebookLoginOptions) => {
|
|||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||
const [user, setUser] = useState<FacebookUser | null>(null);
|
||||
|
||||
const { appId, version = 'v18.0', cookie = true, xfbml = true, autoLogAppEvents = true } = options;
|
||||
const {
|
||||
appId,
|
||||
version = "v18.0",
|
||||
cookie = true,
|
||||
xfbml = true,
|
||||
autoLogAppEvents = true,
|
||||
} = options;
|
||||
|
||||
// Initialize Facebook SDK
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
// Load Facebook SDK if not already loaded
|
||||
if (!window.FB) {
|
||||
const script = document.createElement('script');
|
||||
const script = document.createElement("script");
|
||||
script.src = `https://connect.facebook.net/en_US/sdk.js`;
|
||||
script.async = true;
|
||||
script.defer = true;
|
||||
script.crossOrigin = 'anonymous';
|
||||
script.crossOrigin = "anonymous";
|
||||
|
||||
window.fbAsyncInit = () => {
|
||||
window.FB.init({
|
||||
|
|
@ -39,7 +45,7 @@ export const useFacebookLogin = (options: UseFacebookLoginOptions) => {
|
|||
|
||||
// Check login status
|
||||
window.FB.getLoginStatus((response: any) => {
|
||||
if (response.status === 'connected') {
|
||||
if (response.status === "connected") {
|
||||
setIsLoggedIn(true);
|
||||
getUserInfo(response.authResponse.accessToken);
|
||||
}
|
||||
|
|
@ -57,38 +63,51 @@ export const useFacebookLogin = (options: UseFacebookLoginOptions) => {
|
|||
}, [appId, cookie, xfbml, version, autoLogAppEvents]);
|
||||
|
||||
const getUserInfo = useCallback((accessToken: string) => {
|
||||
window.FB.api('/me', { fields: 'name,email,picture' }, (response: FacebookUser) => {
|
||||
window.FB.api(
|
||||
"/me",
|
||||
{ fields: "name,email,picture" },
|
||||
(response: FacebookUser) => {
|
||||
if (response && !response.error) {
|
||||
setUser(response);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}, []);
|
||||
|
||||
const login = useCallback((permissions: string[] = ['public_profile', 'email']) => {
|
||||
const login = useCallback(
|
||||
(permissions: string[] = ["public_profile", "email"]) => {
|
||||
return new Promise<FacebookLoginResponse>((resolve, reject) => {
|
||||
if (!window.FB) {
|
||||
reject(new Error('Facebook SDK not loaded'));
|
||||
reject(new Error("Facebook SDK not loaded"));
|
||||
return;
|
||||
}
|
||||
|
||||
window.FB.login((response: any) => {
|
||||
if (response.status === 'connected') {
|
||||
window.FB.login(
|
||||
(response: any) => {
|
||||
if (response.status === "connected") {
|
||||
setIsLoggedIn(true);
|
||||
getUserInfo(response.authResponse.accessToken);
|
||||
resolve(response.authResponse);
|
||||
} else if (response.status === 'not_authorized') {
|
||||
reject({ error: 'not_authorized', errorDescription: 'User denied permissions' });
|
||||
} else {
|
||||
reject({ error: 'unknown', errorDescription: 'Login failed' });
|
||||
}
|
||||
}, { scope: permissions.join(',') });
|
||||
} else if (response.status === "not_authorized") {
|
||||
reject({
|
||||
error: "not_authorized",
|
||||
errorDescription: "User denied permissions",
|
||||
});
|
||||
}, [getUserInfo]);
|
||||
} else {
|
||||
reject({ error: "unknown", errorDescription: "Login failed" });
|
||||
}
|
||||
},
|
||||
{ scope: permissions.join(",") }
|
||||
);
|
||||
});
|
||||
},
|
||||
[getUserInfo]
|
||||
);
|
||||
|
||||
const logout = useCallback(() => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (!window.FB) {
|
||||
reject(new Error('Facebook SDK not loaded'));
|
||||
reject(new Error("Facebook SDK not loaded"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -103,7 +122,7 @@ export const useFacebookLogin = (options: UseFacebookLoginOptions) => {
|
|||
const getLoginStatus = useCallback(() => {
|
||||
return new Promise<any>((resolve, reject) => {
|
||||
if (!window.FB) {
|
||||
reject(new Error('Facebook SDK not loaded'));
|
||||
reject(new Error("Facebook SDK not loaded"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
import { useRouter } from "@/components/navigation";
|
||||
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
RegistrationFormData,
|
||||
|
|
@ -53,6 +53,7 @@ import {
|
|||
RegistrationRateLimiter,
|
||||
REGISTRATION_CONSTANTS,
|
||||
} from "@/lib/registration-utils";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
// Global rate limiter instance
|
||||
const registrationRateLimiter = new RegistrationRateLimiter();
|
||||
|
|
@ -68,7 +69,7 @@ export const useOTP = () => {
|
|||
}, []);
|
||||
|
||||
const stopTimer = useCallback(() => {
|
||||
setTimer(prev => ({
|
||||
setTimer((prev) => ({
|
||||
...prev,
|
||||
isActive: false,
|
||||
isExpired: true,
|
||||
|
|
@ -78,7 +79,7 @@ export const useOTP = () => {
|
|||
// Timer effect
|
||||
useEffect(() => {
|
||||
if (!timer.isActive || timer.countdown <= 0) {
|
||||
setTimer(prev => ({
|
||||
setTimer((prev) => ({
|
||||
...prev,
|
||||
isActive: false,
|
||||
isExpired: true,
|
||||
|
|
@ -87,7 +88,7 @@ export const useOTP = () => {
|
|||
}
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setTimer(prev => ({
|
||||
setTimer((prev) => ({
|
||||
...prev,
|
||||
countdown: Math.max(0, prev.countdown - 1000),
|
||||
}));
|
||||
|
|
@ -96,7 +97,8 @@ export const useOTP = () => {
|
|||
return () => clearInterval(interval);
|
||||
}, [timer.isActive, timer.countdown]);
|
||||
|
||||
const requestOTPCode = useCallback(async (
|
||||
const requestOTPCode = useCallback(
|
||||
async (
|
||||
email: string,
|
||||
category: UserCategory,
|
||||
memberIdentity?: string
|
||||
|
|
@ -108,8 +110,11 @@ export const useOTP = () => {
|
|||
// Check rate limiting
|
||||
const identifier = `${email}-${category}`;
|
||||
if (!registrationRateLimiter.canAttempt(identifier)) {
|
||||
const remainingAttempts = registrationRateLimiter.getRemainingAttempts(identifier);
|
||||
throw new Error(`Too many OTP requests. Please try again later. Remaining attempts: ${remainingAttempts}`);
|
||||
const remainingAttempts =
|
||||
registrationRateLimiter.getRemainingAttempts(identifier);
|
||||
throw new Error(
|
||||
`Too many OTP requests. Please try again later. Remaining attempts: ${remainingAttempts}`
|
||||
);
|
||||
}
|
||||
|
||||
const data = {
|
||||
|
|
@ -143,9 +148,12 @@ export const useOTP = () => {
|
|||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [startTimer]);
|
||||
},
|
||||
[startTimer]
|
||||
);
|
||||
|
||||
const verifyOTPCode = useCallback(async (
|
||||
const verifyOTPCode = useCallback(
|
||||
async (
|
||||
email: string,
|
||||
otp: string,
|
||||
category: UserCategory,
|
||||
|
|
@ -184,15 +192,20 @@ export const useOTP = () => {
|
|||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [stopTimer]);
|
||||
},
|
||||
[stopTimer]
|
||||
);
|
||||
|
||||
const resendOTP = useCallback(async (
|
||||
const resendOTP = useCallback(
|
||||
async (
|
||||
email: string,
|
||||
category: UserCategory,
|
||||
memberIdentity?: string
|
||||
): Promise<boolean> => {
|
||||
return await requestOTPCode(email, category, memberIdentity);
|
||||
}, [requestOTPCode]);
|
||||
},
|
||||
[requestOTPCode]
|
||||
);
|
||||
|
||||
return {
|
||||
requestOTP: requestOTPCode,
|
||||
|
|
@ -301,7 +314,8 @@ export const useInstituteData = (category?: number) => {
|
|||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchInstitutes = useCallback(async (categoryId?: number) => {
|
||||
const fetchInstitutes = useCallback(
|
||||
async (categoryId?: number) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
|
@ -320,9 +334,12 @@ export const useInstituteData = (category?: number) => {
|
|||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [category]);
|
||||
},
|
||||
[category]
|
||||
);
|
||||
|
||||
const saveInstitute = useCallback(async (instituteData: InstituteData): Promise<number> => {
|
||||
const saveInstitute = useCallback(
|
||||
async (instituteData: InstituteData): Promise<number> => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
|
@ -348,7 +365,9 @@ export const useInstituteData = (category?: number) => {
|
|||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [category]);
|
||||
},
|
||||
[category]
|
||||
);
|
||||
|
||||
// Load institutes on mount if category is provided
|
||||
useEffect(() => {
|
||||
|
|
@ -371,7 +390,8 @@ export const useUserDataValidation = () => {
|
|||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const validateJournalistData = useCallback(async (certificateNumber: string): Promise<any> => {
|
||||
const validateJournalistData = useCallback(
|
||||
async (certificateNumber: string): Promise<any> => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
|
@ -379,21 +399,27 @@ export const useUserDataValidation = () => {
|
|||
const response = await getDataJournalist(certificateNumber);
|
||||
|
||||
if (response?.error) {
|
||||
throw new Error(response.message || "Invalid journalist certificate number");
|
||||
throw new Error(
|
||||
response.message || "Invalid journalist certificate number"
|
||||
);
|
||||
}
|
||||
|
||||
return response?.data?.data;
|
||||
} catch (error: any) {
|
||||
const errorMessage = error?.message || "Failed to validate journalist data";
|
||||
const errorMessage =
|
||||
error?.message || "Failed to validate journalist data";
|
||||
setError(errorMessage);
|
||||
showRegistrationError(error, "Failed to validate journalist data");
|
||||
throw error;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const validatePersonnelData = useCallback(async (policeNumber: string): Promise<any> => {
|
||||
const validatePersonnelData = useCallback(
|
||||
async (policeNumber: string): Promise<any> => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
|
@ -406,14 +432,17 @@ export const useUserDataValidation = () => {
|
|||
|
||||
return response?.data?.data;
|
||||
} catch (error: any) {
|
||||
const errorMessage = error?.message || "Failed to validate personnel data";
|
||||
const errorMessage =
|
||||
error?.message || "Failed to validate personnel data";
|
||||
setError(errorMessage);
|
||||
showRegistrationError(error, "Failed to validate personnel data");
|
||||
throw error;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return {
|
||||
validateJournalistData,
|
||||
|
|
@ -429,7 +458,8 @@ export const useRegistration = () => {
|
|||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const submitRegistration = useCallback(async (
|
||||
const submitRegistration = useCallback(
|
||||
async (
|
||||
data: RegistrationFormData,
|
||||
category: UserCategory,
|
||||
userData: any,
|
||||
|
|
@ -443,7 +473,10 @@ export const useRegistration = () => {
|
|||
const sanitizedData = sanitizeRegistrationData(data);
|
||||
|
||||
// Validate password
|
||||
const passwordValidation = validatePassword(sanitizedData.password, sanitizedData.passwordConf);
|
||||
const passwordValidation = validatePassword(
|
||||
sanitizedData.password,
|
||||
sanitizedData.passwordConf
|
||||
);
|
||||
if (!passwordValidation.isValid) {
|
||||
throw new Error(passwordValidation.errors[0]);
|
||||
}
|
||||
|
|
@ -455,15 +488,22 @@ export const useRegistration = () => {
|
|||
}
|
||||
|
||||
// Transform data for API
|
||||
const transformedData = transformRegistrationData(sanitizedData, category, userData, instituteId);
|
||||
const transformedData = transformRegistrationData(
|
||||
sanitizedData,
|
||||
category,
|
||||
userData,
|
||||
instituteId
|
||||
);
|
||||
|
||||
const response = await postRegistration(transformedData);
|
||||
console.log("PPPP", transformedData)
|
||||
console.log("PPPP", transformedData);
|
||||
if (response?.error) {
|
||||
throw new Error(response.message || "Registration failed");
|
||||
}
|
||||
|
||||
showRegistrationSuccess("Registration successful! Please check your email for verification.");
|
||||
showRegistrationSuccess(
|
||||
"Registration successful! Please check your email for verification."
|
||||
);
|
||||
|
||||
// Redirect to login page
|
||||
setTimeout(() => {
|
||||
|
|
@ -479,7 +519,9 @@ export const useRegistration = () => {
|
|||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [router]);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
return {
|
||||
submitRegistration,
|
||||
|
|
@ -490,14 +532,21 @@ export const useRegistration = () => {
|
|||
|
||||
// Hook for form validation
|
||||
export const useFormValidation = () => {
|
||||
const validateIdentityForm = useCallback((
|
||||
data: JournalistRegistrationData | PersonnelRegistrationData | GeneralRegistrationData,
|
||||
const validateIdentityForm = useCallback(
|
||||
(
|
||||
data:
|
||||
| JournalistRegistrationData
|
||||
| PersonnelRegistrationData
|
||||
| GeneralRegistrationData,
|
||||
category: UserCategory
|
||||
): { isValid: boolean; errors: string[] } => {
|
||||
return validateIdentityData(data, category);
|
||||
}, []);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const validateProfileForm = useCallback((data: RegistrationFormData): { isValid: boolean; errors: string[] } => {
|
||||
const validateProfileForm = useCallback(
|
||||
(data: RegistrationFormData): { isValid: boolean; errors: string[] } => {
|
||||
const errors: string[] = [];
|
||||
|
||||
// Validate required fields
|
||||
|
|
@ -551,7 +600,10 @@ export const useFormValidation = () => {
|
|||
if (!data.password) {
|
||||
errors.push("Password is required");
|
||||
} else {
|
||||
const passwordValidation = validatePassword(data.password, data.passwordConf);
|
||||
const passwordValidation = validatePassword(
|
||||
data.password,
|
||||
data.passwordConf
|
||||
);
|
||||
if (!passwordValidation.isValid) {
|
||||
errors.push(passwordValidation.errors[0]);
|
||||
}
|
||||
|
|
@ -561,7 +613,9 @@ export const useFormValidation = () => {
|
|||
isValid: errors.length === 0,
|
||||
errors,
|
||||
};
|
||||
}, []);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return {
|
||||
validateIdentityForm,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,374 @@
|
|||
import {
|
||||
RegistrationFormData,
|
||||
JournalistRegistrationData,
|
||||
PersonnelRegistrationData,
|
||||
GeneralRegistrationData,
|
||||
InstituteData,
|
||||
UserCategory,
|
||||
PasswordValidation,
|
||||
TimerState,
|
||||
Association
|
||||
} from "@/types/registration";
|
||||
import { toast } from "sonner";
|
||||
|
||||
// Constants
|
||||
export const REGISTRATION_CONSTANTS = {
|
||||
OTP_TIMEOUT: 60000, // 1 minute in milliseconds
|
||||
MAX_OTP_ATTEMPTS: 3,
|
||||
PASSWORD_MIN_LENGTH: 8,
|
||||
USERNAME_MIN_LENGTH: 3,
|
||||
USERNAME_MAX_LENGTH: 50,
|
||||
} as const;
|
||||
|
||||
// Association data
|
||||
export const ASSOCIATIONS: Association[] = [
|
||||
{ id: "1", name: "PWI (Persatuan Wartawan Indonesia)", value: "PWI" },
|
||||
{ id: "2", name: "IJTI (Ikatan Jurnalis Televisi Indonesia)", value: "IJTI" },
|
||||
{ id: "3", name: "PFI (Pewarta Foto Indonesia)", value: "PFI" },
|
||||
{ id: "4", name: "AJI (Asosiasi Jurnalis Indonesia)", value: "AJI" },
|
||||
{ id: "5", name: "Other Identity", value: "Wartawan" },
|
||||
];
|
||||
|
||||
// Password validation utility
|
||||
export const validatePassword = (password: string, confirmPassword?: string): PasswordValidation => {
|
||||
const errors: string[] = [];
|
||||
let strength: 'weak' | 'medium' | 'strong' = 'weak';
|
||||
|
||||
// Check minimum length
|
||||
if (password.length < REGISTRATION_CONSTANTS.PASSWORD_MIN_LENGTH) {
|
||||
errors.push(`Password must be at least ${REGISTRATION_CONSTANTS.PASSWORD_MIN_LENGTH} characters`);
|
||||
}
|
||||
|
||||
// Check for uppercase letter
|
||||
if (!/[A-Z]/.test(password)) {
|
||||
errors.push("Password must contain at least one uppercase letter");
|
||||
}
|
||||
|
||||
// Check for lowercase letter
|
||||
if (!/[a-z]/.test(password)) {
|
||||
errors.push("Password must contain at least one lowercase letter");
|
||||
}
|
||||
|
||||
// Check for number
|
||||
if (!/\d/.test(password)) {
|
||||
errors.push("Password must contain at least one number");
|
||||
}
|
||||
|
||||
// Check for special character
|
||||
if (!/[@$!%*?&]/.test(password)) {
|
||||
errors.push("Password must contain at least one special character (@$!%*?&)");
|
||||
}
|
||||
|
||||
// Check password confirmation
|
||||
if (confirmPassword && password !== confirmPassword) {
|
||||
errors.push("Passwords don't match");
|
||||
}
|
||||
|
||||
// Determine strength
|
||||
if (password.length >= 12 && errors.length === 0) {
|
||||
strength = 'strong';
|
||||
} else if (password.length >= 8 && errors.length <= 1) {
|
||||
strength = 'medium';
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors,
|
||||
strength,
|
||||
};
|
||||
};
|
||||
|
||||
// Username validation utility
|
||||
export const validateUsername = (username: string): { isValid: boolean; error?: string } => {
|
||||
if (username.length < REGISTRATION_CONSTANTS.USERNAME_MIN_LENGTH) {
|
||||
return { isValid: false, error: `Username must be at least ${REGISTRATION_CONSTANTS.USERNAME_MIN_LENGTH} characters` };
|
||||
}
|
||||
|
||||
if (username.length > REGISTRATION_CONSTANTS.USERNAME_MAX_LENGTH) {
|
||||
return { isValid: false, error: `Username must be less than ${REGISTRATION_CONSTANTS.USERNAME_MAX_LENGTH} characters` };
|
||||
}
|
||||
|
||||
if (!/^[a-zA-Z0-9._-]+$/.test(username)) {
|
||||
return { isValid: false, error: "Username can only contain letters, numbers, dots, underscores, and hyphens" };
|
||||
}
|
||||
|
||||
return { isValid: true };
|
||||
};
|
||||
|
||||
// Email validation utility
|
||||
export const validateEmail = (email: string): { isValid: boolean; error?: string } => {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
|
||||
if (!email) {
|
||||
return { isValid: false, error: "Email is required" };
|
||||
}
|
||||
|
||||
if (!emailRegex.test(email)) {
|
||||
return { isValid: false, error: "Please enter a valid email address" };
|
||||
}
|
||||
|
||||
return { isValid: true };
|
||||
};
|
||||
|
||||
// Phone number validation utility
|
||||
export const validatePhoneNumber = (phoneNumber: string): { isValid: boolean; error?: string } => {
|
||||
const phoneRegex = /^[0-9+\-\s()]+$/;
|
||||
|
||||
if (!phoneNumber) {
|
||||
return { isValid: false, error: "Phone number is required" };
|
||||
}
|
||||
|
||||
if (!phoneRegex.test(phoneNumber)) {
|
||||
return { isValid: false, error: "Please enter a valid phone number" };
|
||||
}
|
||||
|
||||
return { isValid: true };
|
||||
};
|
||||
|
||||
// Timer utility
|
||||
export const createTimer = (duration: number = REGISTRATION_CONSTANTS.OTP_TIMEOUT): TimerState => {
|
||||
return {
|
||||
countdown: duration,
|
||||
isActive: true,
|
||||
isExpired: false,
|
||||
};
|
||||
};
|
||||
|
||||
export const formatTime = (milliseconds: number): string => {
|
||||
const minutes = Math.floor(milliseconds / 60000);
|
||||
const seconds = Math.floor((milliseconds % 60000) / 1000);
|
||||
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
// Data sanitization utility
|
||||
export const sanitizeRegistrationData = (data: RegistrationFormData): RegistrationFormData => {
|
||||
return {
|
||||
...data,
|
||||
firstName: data.firstName.trim(),
|
||||
username: data.username.trim().toLowerCase(),
|
||||
email: data.email.trim().toLowerCase(),
|
||||
phoneNumber: data.phoneNumber.trim(),
|
||||
address: data.address.trim(),
|
||||
};
|
||||
};
|
||||
|
||||
export const sanitizeInstituteData = (data: InstituteData): InstituteData => {
|
||||
return {
|
||||
id : data.id,
|
||||
name: data.name.trim(),
|
||||
address: data.address.trim(),
|
||||
};
|
||||
};
|
||||
|
||||
// Category validation utility
|
||||
export const isValidCategory = (category: string): category is UserCategory => {
|
||||
return category === "6" || category === "7" || category === "general";
|
||||
};
|
||||
|
||||
export const getCategoryLabel = (category: UserCategory): string => {
|
||||
switch (category) {
|
||||
case "6":
|
||||
return "Journalist";
|
||||
case "7":
|
||||
return "Personnel";
|
||||
case "general":
|
||||
return "Public";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
};
|
||||
|
||||
// Category to role ID conversion utility
|
||||
export const getCategoryRoleId = (category: UserCategory): number => {
|
||||
switch (category) {
|
||||
case "6":
|
||||
return 6; // Journalist
|
||||
case "7":
|
||||
return 7; // Personnel
|
||||
case "general":
|
||||
return 5; // Public (general)
|
||||
default:
|
||||
return 5; // Default to public
|
||||
}
|
||||
};
|
||||
|
||||
// Error handling utilities
|
||||
export const showRegistrationError = (error: any, defaultMessage: string = "Registration failed") => {
|
||||
const message = error?.message || error?.data?.message || defaultMessage;
|
||||
toast.error(message);
|
||||
console.error("Registration error:", error);
|
||||
};
|
||||
|
||||
export const showRegistrationSuccess = (message: string = "Registration successful") => {
|
||||
toast.success(message);
|
||||
};
|
||||
|
||||
export const showRegistrationInfo = (message: string) => {
|
||||
toast.info(message);
|
||||
};
|
||||
|
||||
// Data transformation utilities
|
||||
export const transformRegistrationData = (
|
||||
data: RegistrationFormData,
|
||||
category: UserCategory,
|
||||
userData: any,
|
||||
instituteId?: number
|
||||
): any => {
|
||||
const baseData = {
|
||||
firstName: data.firstName,
|
||||
lastName: data.firstName, // Using firstName as lastName for now
|
||||
username: data.username,
|
||||
phoneNumber: data.phoneNumber,
|
||||
email: data.email,
|
||||
address: data.address,
|
||||
provinceId: Number(data.provinsi),
|
||||
cityId: Number(data.kota),
|
||||
districtId: Number(data.kecamatan),
|
||||
password: data.password,
|
||||
roleId: getCategoryRoleId(category),
|
||||
};
|
||||
|
||||
// Add category-specific data
|
||||
if (category === "6") {
|
||||
return {
|
||||
...baseData,
|
||||
memberIdentity: userData?.journalistCertificate,
|
||||
instituteId: instituteId || 1,
|
||||
};
|
||||
} else if (category === "7") {
|
||||
return {
|
||||
...baseData,
|
||||
memberIdentity: userData?.policeNumber,
|
||||
instituteId: 1,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...baseData,
|
||||
memberIdentity: null,
|
||||
instituteId: 1,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Form data utilities
|
||||
export const createInitialFormData = (category: UserCategory) => {
|
||||
const baseData = {
|
||||
firstName: "",
|
||||
username: "",
|
||||
phoneNumber: "",
|
||||
email: "",
|
||||
address: "",
|
||||
provinsi: "",
|
||||
kota: "",
|
||||
kecamatan: "",
|
||||
password: "",
|
||||
passwordConf: "",
|
||||
};
|
||||
|
||||
if (category === "6") {
|
||||
return {
|
||||
...baseData,
|
||||
journalistCertificate: "",
|
||||
association: "",
|
||||
};
|
||||
} else if (category === "7") {
|
||||
return {
|
||||
...baseData,
|
||||
policeNumber: "",
|
||||
};
|
||||
}
|
||||
|
||||
return baseData;
|
||||
};
|
||||
|
||||
// Validation utilities
|
||||
export const validateIdentityData = (
|
||||
data: JournalistRegistrationData | PersonnelRegistrationData | GeneralRegistrationData,
|
||||
category: UserCategory
|
||||
): { isValid: boolean; errors: string[] } => {
|
||||
const errors: string[] = [];
|
||||
|
||||
// Email validation
|
||||
const emailValidation = validateEmail(data.email);
|
||||
if (!emailValidation.isValid) {
|
||||
errors.push(emailValidation.error!);
|
||||
}
|
||||
|
||||
// Category-specific validation
|
||||
if (category === "6") {
|
||||
const journalistData = data as JournalistRegistrationData;
|
||||
if (!journalistData.journalistCertificate?.trim()) {
|
||||
errors.push("Journalist certificate number is required");
|
||||
}
|
||||
if (!journalistData.association?.trim()) {
|
||||
errors.push("Association is required");
|
||||
}
|
||||
} else if (category === "7") {
|
||||
const personnelData = data as PersonnelRegistrationData;
|
||||
if (!personnelData.policeNumber?.trim()) {
|
||||
errors.push("Police number is required");
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors,
|
||||
};
|
||||
};
|
||||
|
||||
// Rate limiting utility
|
||||
export class RegistrationRateLimiter {
|
||||
private attempts: Map<string, { count: number; lastAttempt: number }> = new Map();
|
||||
private readonly maxAttempts: number;
|
||||
private readonly windowMs: number;
|
||||
|
||||
constructor(maxAttempts: number = REGISTRATION_CONSTANTS.MAX_OTP_ATTEMPTS, windowMs: number = 300000) {
|
||||
this.maxAttempts = maxAttempts;
|
||||
this.windowMs = windowMs;
|
||||
}
|
||||
|
||||
canAttempt(identifier: string): boolean {
|
||||
const attempt = this.attempts.get(identifier);
|
||||
if (!attempt) return true;
|
||||
|
||||
const now = Date.now();
|
||||
if (now - attempt.lastAttempt > this.windowMs) {
|
||||
this.attempts.delete(identifier);
|
||||
return true;
|
||||
}
|
||||
|
||||
return attempt.count < this.maxAttempts;
|
||||
}
|
||||
|
||||
recordAttempt(identifier: string): void {
|
||||
const attempt = this.attempts.get(identifier);
|
||||
const now = Date.now();
|
||||
|
||||
if (attempt) {
|
||||
attempt.count++;
|
||||
attempt.lastAttempt = now;
|
||||
} else {
|
||||
this.attempts.set(identifier, { count: 1, lastAttempt: now });
|
||||
}
|
||||
}
|
||||
|
||||
getRemainingAttempts(identifier: string): number {
|
||||
const attempt = this.attempts.get(identifier);
|
||||
if (!attempt) return this.maxAttempts;
|
||||
|
||||
const now = Date.now();
|
||||
if (now - attempt.lastAttempt > this.windowMs) {
|
||||
this.attempts.delete(identifier);
|
||||
return this.maxAttempts;
|
||||
}
|
||||
|
||||
return Math.max(0, this.maxAttempts - attempt.count);
|
||||
}
|
||||
|
||||
reset(identifier: string): void {
|
||||
this.attempts.delete(identifier);
|
||||
}
|
||||
}
|
||||
|
||||
// Export rate limiter instance
|
||||
export const registrationRateLimiter = new RegistrationRateLimiter();
|
||||
|
|
@ -21,6 +21,7 @@
|
|||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.2.5",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@tinymce/tinymce-react": "^6.3.0",
|
||||
"@types/crypto-js": "^4.2.0",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/next": "^9.0.0",
|
||||
|
|
@ -33,6 +34,7 @@
|
|||
"embla-carousel-react": "^8.6.0",
|
||||
"framer-motion": "^12.23.3",
|
||||
"input-otp": "^1.4.2",
|
||||
"jotai": "^2.14.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"lucide-react": "^0.525.0",
|
||||
"next": "15.3.5",
|
||||
|
|
@ -2800,6 +2802,24 @@
|
|||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tinymce/tinymce-react": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@tinymce/tinymce-react/-/tinymce-react-6.3.0.tgz",
|
||||
"integrity": "sha512-E++xnn0XzDzpKr40jno2Kj7umfAE6XfINZULEBBeNjTMvbACWzA6CjiR6V8eTDc9yVmdVhIPqVzV4PqD5TZ/4g==",
|
||||
"dependencies": {
|
||||
"prop-types": "^15.6.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.0.0 || ^18.0.0 || ^17.0.1 || ^16.7.0",
|
||||
"react-dom": "^19.0.0 || ^18.0.0 || ^17.0.1 || ^16.7.0",
|
||||
"tinymce": "^8.0.0 || ^7.0.0 || ^6.0.0 || ^5.5.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"tinymce": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@types/color-convert": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/color-convert/-/color-convert-2.0.4.tgz",
|
||||
|
|
@ -3952,6 +3972,34 @@
|
|||
"jiti": "lib/jiti-cli.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/jotai": {
|
||||
"version": "2.14.0",
|
||||
"resolved": "https://registry.npmjs.org/jotai/-/jotai-2.14.0.tgz",
|
||||
"integrity": "sha512-JQkNkTnqjk1BlSUjHfXi+pGG/573bVN104gp6CymhrWDseZGDReTNniWrLhJ+zXbM6pH+82+UNJ2vwYQUkQMWQ==",
|
||||
"engines": {
|
||||
"node": ">=12.20.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": ">=7.0.0",
|
||||
"@babel/template": ">=7.0.0",
|
||||
"@types/react": ">=17.0.0",
|
||||
"react": ">=17.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@babel/core": {
|
||||
"optional": true
|
||||
},
|
||||
"@babel/template": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/js-base64": {
|
||||
"version": "3.7.8",
|
||||
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.8.tgz",
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.2.5",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@tinymce/tinymce-react": "^6.3.0",
|
||||
"@types/crypto-js": "^4.2.0",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/next": "^9.0.0",
|
||||
|
|
@ -34,6 +35,7 @@
|
|||
"embla-carousel-react": "^8.6.0",
|
||||
"framer-motion": "^12.23.3",
|
||||
"input-otp": "^1.4.2",
|
||||
"jotai": "^2.14.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"lucide-react": "^0.525.0",
|
||||
"next": "15.3.5",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
export interface FacebookLoginResponse {
|
||||
accessToken: string;
|
||||
userID: string;
|
||||
expiresIn: number;
|
||||
signedRequest: string;
|
||||
graphDomain: string;
|
||||
data_access_expiration_time: number;
|
||||
}
|
||||
|
||||
export interface FacebookLoginError {
|
||||
error: string;
|
||||
errorDescription: string;
|
||||
}
|
||||
|
||||
export interface FacebookUser {
|
||||
id: string;
|
||||
name: string;
|
||||
email?: string;
|
||||
picture?: {
|
||||
data: {
|
||||
url: string;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
};
|
||||
error?: any;
|
||||
}
|
||||
|
||||
export interface FacebookSDKInitOptions {
|
||||
appId: string;
|
||||
version?: string;
|
||||
cookie?: boolean;
|
||||
xfbml?: boolean;
|
||||
autoLogAppEvents?: boolean;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
FB: {
|
||||
init: (options: FacebookSDKInitOptions) => void;
|
||||
login: (callback: (response: any) => void, options?: { scope: string }) => void;
|
||||
logout: (callback: (response: any) => void) => void;
|
||||
getLoginStatus: (callback: (response: any) => void) => void;
|
||||
api: (path: string, params: any, callback: (response: any) => void) => void;
|
||||
};
|
||||
fbAsyncInit: () => void;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,247 @@
|
|||
import { z } from "zod";
|
||||
|
||||
// Base schemas for validation
|
||||
export const registrationSchema = z.object({
|
||||
firstName: z
|
||||
.string()
|
||||
.min(1, { message: "Full name is required" })
|
||||
.min(2, { message: "Full name must be at least 2 characters" })
|
||||
.max(100, { message: "Full name must be less than 100 characters" })
|
||||
.regex(/^[a-zA-Z\s]+$/, { message: "Full name can only contain letters and spaces" }),
|
||||
username: z
|
||||
.string()
|
||||
.min(1, { message: "Username is required" })
|
||||
.min(3, { message: "Username must be at least 3 characters" })
|
||||
.max(50, { message: "Username must be less than 50 characters" })
|
||||
.regex(/^[a-zA-Z0-9._-]+$/, { message: "Username can only contain letters, numbers, dots, underscores, and hyphens" }),
|
||||
phoneNumber: z
|
||||
.string()
|
||||
.min(1, { message: "Phone number is required" })
|
||||
.regex(/^[0-9+\-\s()]+$/, { message: "Please enter a valid phone number" }),
|
||||
email: z
|
||||
.string()
|
||||
.min(1, { message: "Email is required" })
|
||||
.email({ message: "Please enter a valid email address" }),
|
||||
address: z
|
||||
.string()
|
||||
.min(1, { message: "Address is required" })
|
||||
.min(10, { message: "Address must be at least 10 characters" })
|
||||
.max(500, { message: "Address must be less than 500 characters" }),
|
||||
provinsi: z
|
||||
.string()
|
||||
.min(1, { message: "Province is required" }),
|
||||
kota: z
|
||||
.string()
|
||||
.min(1, { message: "City is required" }),
|
||||
kecamatan: z
|
||||
.string()
|
||||
.min(1, { message: "Subdistrict is required" }),
|
||||
password: z
|
||||
.string()
|
||||
.min(1, { message: "Password is required" })
|
||||
.min(8, { message: "Password must be at least 8 characters" })
|
||||
.max(100, { message: "Password must be less than 100 characters" })
|
||||
.regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/, {
|
||||
message: "Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character"
|
||||
}),
|
||||
passwordConf: z
|
||||
.string()
|
||||
.min(1, { message: "Password confirmation is required" }),
|
||||
}).refine((data) => data.password === data.passwordConf, {
|
||||
message: "Passwords don't match",
|
||||
path: ["passwordConf"],
|
||||
});
|
||||
|
||||
export const journalistRegistrationSchema = z.object({
|
||||
journalistCertificate: z
|
||||
.string()
|
||||
.min(1, { message: "Journalist certificate number is required" })
|
||||
.min(5, { message: "Journalist certificate number must be at least 5 characters" }),
|
||||
association: z
|
||||
.string()
|
||||
.min(1, { message: "Association is required" }),
|
||||
email: z
|
||||
.string()
|
||||
.min(1, { message: "Email is required" })
|
||||
.email({ message: "Please enter a valid email address" }),
|
||||
});
|
||||
|
||||
export const personnelRegistrationSchema = z.object({
|
||||
policeNumber: z
|
||||
.string()
|
||||
.min(1, { message: "Police number is required" })
|
||||
.min(5, { message: "Police number must be at least 5 characters" }),
|
||||
email: z
|
||||
.string()
|
||||
.min(1, { message: "Email is required" })
|
||||
.email({ message: "Please enter a valid email address" }),
|
||||
});
|
||||
|
||||
export const generalRegistrationSchema = z.object({
|
||||
email: z
|
||||
.string()
|
||||
.min(1, { message: "Email is required" })
|
||||
.email({ message: "Please enter a valid email address" }),
|
||||
});
|
||||
|
||||
export const instituteSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z
|
||||
.string()
|
||||
.min(1, { message: "Institute name is required" })
|
||||
.min(2, { message: "Institute name must be at least 2 characters" }),
|
||||
address: z
|
||||
.string()
|
||||
.min(1, { message: "Institute address is required" })
|
||||
.min(10, { message: "Institute address must be at least 10 characters" }),
|
||||
});
|
||||
|
||||
// Inferred types from schemas
|
||||
export type RegistrationFormData = z.infer<typeof registrationSchema>;
|
||||
export type JournalistRegistrationData = z.infer<typeof journalistRegistrationSchema>;
|
||||
export type PersonnelRegistrationData = z.infer<typeof personnelRegistrationSchema>;
|
||||
export type GeneralRegistrationData = z.infer<typeof generalRegistrationSchema>;
|
||||
export type InstituteData = z.infer<typeof instituteSchema>;
|
||||
|
||||
// API response types
|
||||
export interface RegistrationResponse {
|
||||
data?: {
|
||||
id: string;
|
||||
message: string;
|
||||
};
|
||||
error?: boolean;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface OTPRequestResponse {
|
||||
data?: {
|
||||
message: string;
|
||||
};
|
||||
error?: boolean;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface OTPVerificationResponse {
|
||||
data?: {
|
||||
message: string;
|
||||
userData?: any;
|
||||
};
|
||||
error?: boolean;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface InstituteResponse {
|
||||
data?: {
|
||||
data: InstituteData[];
|
||||
};
|
||||
error?: boolean;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface LocationData {
|
||||
id: number;
|
||||
name: string;
|
||||
provName?: string;
|
||||
cityName?: string;
|
||||
disName?: string;
|
||||
}
|
||||
|
||||
export interface LocationResponse {
|
||||
data?: {
|
||||
data: LocationData[];
|
||||
};
|
||||
error?: boolean;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
// Registration step types
|
||||
export type RegistrationStep = "identity" | "otp" | "profile";
|
||||
|
||||
// User category types
|
||||
export type UserCategory = "6" | "7" | "general"; // 6=Journalist, 7=Personnel, general=Public
|
||||
|
||||
// Component props types
|
||||
export interface RegistrationLayoutProps {
|
||||
children: React.ReactNode;
|
||||
currentStep: RegistrationStep;
|
||||
totalSteps: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface IdentityFormProps {
|
||||
category: UserCategory;
|
||||
onSuccess: (data: JournalistRegistrationData | PersonnelRegistrationData | GeneralRegistrationData) => void;
|
||||
onError: (error: string) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface RegistrationOTPFormProps {
|
||||
email: string;
|
||||
category: UserCategory;
|
||||
memberIdentity?: string;
|
||||
onSuccess: (userData: any) => void;
|
||||
onError: (error: string) => void;
|
||||
onResend: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface ProfileFormProps {
|
||||
userData: any;
|
||||
category: UserCategory;
|
||||
onSuccess: (data: RegistrationFormData) => void;
|
||||
onError: (error: string) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface InstituteFormProps {
|
||||
onInstituteChange: (institute: InstituteData | null) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface LocationSelectorProps {
|
||||
onProvinceChange: (provinceId: string) => void;
|
||||
onCityChange: (cityId: string) => void;
|
||||
onDistrictChange: (districtId: string) => void;
|
||||
selectedProvince?: string;
|
||||
selectedCity?: string;
|
||||
selectedDistrict?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// Registration state types
|
||||
export interface RegistrationState {
|
||||
currentStep: RegistrationStep;
|
||||
category: UserCategory;
|
||||
identityData: JournalistRegistrationData | PersonnelRegistrationData | GeneralRegistrationData | null;
|
||||
userData: any;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export interface RegistrationContextType extends RegistrationState {
|
||||
setStep: (step: RegistrationStep) => void;
|
||||
setIdentityData: (data: JournalistRegistrationData | PersonnelRegistrationData | GeneralRegistrationData) => void;
|
||||
setUserData: (data: any) => void;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
// Association types
|
||||
export interface Association {
|
||||
id: string;
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
// Timer types
|
||||
export interface TimerState {
|
||||
countdown: number;
|
||||
isActive: boolean;
|
||||
isExpired: boolean;
|
||||
}
|
||||
|
||||
// Password validation types
|
||||
export interface PasswordValidation {
|
||||
isValid: boolean;
|
||||
errors: string[];
|
||||
strength: 'weak' | 'medium' | 'strong';
|
||||
}
|
||||
Loading…
Reference in New Issue