mirror of
https://github.com/MAKS-IT-COM/maksit-certs-ui.git
synced 2025-12-30 19:50:07 +01:00
(feature): formating rules
This commit is contained in:
parent
e321a3237f
commit
7d7ebd298a
@ -1,3 +1,12 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
"extends": [
|
||||
"next/core-web-vitals",
|
||||
"plugin:prettier/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"prettier/prettier": "error",
|
||||
"semi": "off",
|
||||
"quotes": "off",
|
||||
"indent": "off"
|
||||
}
|
||||
}
|
||||
|
||||
25
src/ClientApp/.prettierignore
Normal file
25
src/ClientApp/.prettierignore
Normal file
@ -0,0 +1,25 @@
|
||||
# Ignore artifacts:
|
||||
build
|
||||
coverage
|
||||
dist
|
||||
node_modules
|
||||
|
||||
# Ignore all configuration files:
|
||||
*.config.js
|
||||
*.config.ts
|
||||
|
||||
# Ignore specific files:
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
||||
# Ignore logs:
|
||||
*.log
|
||||
|
||||
# Ignore minified files:
|
||||
*.min.js
|
||||
|
||||
# Ignore compiled code:
|
||||
*.d.ts
|
||||
|
||||
# Ignore specific directories:
|
||||
public
|
||||
7
src/ClientApp/.prettierrc
Normal file
7
src/ClientApp/.prettierrc
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"semi": false,
|
||||
"tabWidth": 2,
|
||||
"endOfLine": "lf",
|
||||
"trailingComma": "none"
|
||||
}
|
||||
23
src/ClientApp/.vscode/settings.json
vendored
Normal file
23
src/ClientApp/.vscode/settings.json
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"editor.tabSize": 2,
|
||||
// "editor.codeActionsOnSave": {
|
||||
// "source.fixAll.eslint": true
|
||||
// },
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"eslint.validate": [
|
||||
"javascript",
|
||||
"typescript",
|
||||
"typescriptreact"
|
||||
]
|
||||
}
|
||||
@ -1,17 +1,14 @@
|
||||
enum ApiRoutes {
|
||||
|
||||
CACHE_ACCOUNTS = 'api/cache/accounts',
|
||||
CACHE_ACCOUNT = 'api/cache/account/{accountId}',
|
||||
CACHE_ACCOUNT_CONTACTS = 'api/cache/account/{accountId}/contacts',
|
||||
CACHE_ACCOUNT_CONTACT = 'api/cache/account/{accountId}/contacts/{index}',
|
||||
CACHE_ACCOUNT_HOSTNAMES = 'api/cache/account/{accountId}/hostnames',
|
||||
|
||||
|
||||
CACHE_ACCOUNT_HOSTNAMES = 'api/cache/account/{accountId}/hostnames'
|
||||
|
||||
// CERTS_FLOW_CONFIGURE_CLIENT = `api/CertsFlow/ConfigureClient`,
|
||||
// CERTS_FLOW_TERMS_OF_SERVICE = `api/CertsFlow/TermsOfService/{sessionId}`,
|
||||
// CERTS_FLOW_INIT = `api/CertsFlow/Init/{sessionId}/{accountId}`,
|
||||
// CERTS_FLOW_NEW_ORDER = `api/CertsFlow/NewOrder/{sessionId}`,
|
||||
// CERTS_FLOW_NEW_ORDER = `api/CertsFlow/NewOrder/{sessionId}`,
|
||||
// CERTS_FLOW_GET_ORDER = `api/CertsFlow/GetOrder/{sessionId}`,
|
||||
// CERTS_FLOW_GET_CERTIFICATES = `api/CertsFlow/GetCertificates/{sessionId}`,
|
||||
// CERTS_FLOW_APPLY_CERTIFICATES = `api/CertsFlow/ApplyCertificates/{sessionId}`,
|
||||
@ -19,15 +16,11 @@ enum ApiRoutes {
|
||||
}
|
||||
|
||||
const GetApiRoute = (route: ApiRoutes, ...args: string[]): string => {
|
||||
let result: string = route;
|
||||
args.forEach(arg => {
|
||||
result = result.replace(/{.*?}/, arg);
|
||||
});
|
||||
return `http://localhost:5000/${result}`;
|
||||
let result: string = route
|
||||
args.forEach((arg) => {
|
||||
result = result.replace(/{.*?}/, arg)
|
||||
})
|
||||
return `http://localhost:5000/${result}`
|
||||
}
|
||||
|
||||
|
||||
export {
|
||||
GetApiRoute,
|
||||
ApiRoutes
|
||||
}
|
||||
export { GetApiRoute, ApiRoutes }
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
const ContactPage = () => {
|
||||
return (<>
|
||||
<h1 className="text-2xl font-bold">Contact Us</h1>
|
||||
<p>This is the contact page content.</p>
|
||||
</>
|
||||
return (
|
||||
<>
|
||||
<h1 className="text-2xl font-bold">Contact Us</h1>
|
||||
<p>This is the contact page content.</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
41
src/ClientApp/app/functions/deepCopy.ts
Normal file
41
src/ClientApp/app/functions/deepCopy.ts
Normal file
@ -0,0 +1,41 @@
|
||||
const deepCopy = <T>(input: T): T => {
|
||||
const map = new Map()
|
||||
|
||||
const clone = (item: any): any => {
|
||||
if (item === null || typeof item !== 'object') {
|
||||
return item
|
||||
}
|
||||
|
||||
if (map.has(item)) {
|
||||
return map.get(item)
|
||||
}
|
||||
|
||||
let result: any
|
||||
|
||||
if (Array.isArray(item)) {
|
||||
result = []
|
||||
map.set(item, result)
|
||||
item.forEach((element, index) => {
|
||||
result[index] = clone(element)
|
||||
})
|
||||
} else if (item instanceof Date) {
|
||||
result = new Date(item)
|
||||
map.set(item, result)
|
||||
} else if (item instanceof RegExp) {
|
||||
result = new RegExp(item)
|
||||
map.set(item, result)
|
||||
} else {
|
||||
result = Object.create(Object.getPrototypeOf(item))
|
||||
map.set(item, result)
|
||||
Object.keys(item).forEach((key) => {
|
||||
result[key] = clone(item[key])
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
return clone(input)
|
||||
}
|
||||
|
||||
export { deepCopy }
|
||||
3
src/ClientApp/app/functions/index.ts
Normal file
3
src/ClientApp/app/functions/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { deepCopy } from './deepCopy'
|
||||
|
||||
export { deepCopy }
|
||||
@ -1,4 +1,4 @@
|
||||
"use client"
|
||||
'use client'
|
||||
|
||||
import React, { FC, useState, useEffect, useRef } from 'react'
|
||||
import { SideMenu } from '@/components/sidemenu'
|
||||
@ -13,8 +13,8 @@ import { store } from '@/redux/store'
|
||||
import './globals.css'
|
||||
|
||||
const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
title: 'Create Next App',
|
||||
description: 'Generated by create next app'
|
||||
}
|
||||
|
||||
const Layout: FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
@ -75,16 +75,16 @@ const Layout: FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
<Loader />
|
||||
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
<SideMenu isCollapsed={isSidebarCollapsed} toggleSidebar={manuallyToggleSidebar} />
|
||||
|
||||
<SideMenu
|
||||
isCollapsed={isSidebarCollapsed}
|
||||
toggleSidebar={manuallyToggleSidebar}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col flex-1 overflow-hidden">
|
||||
<TopMenu onToggleOffCanvas={toggleOffCanvas} />
|
||||
<main className="flex-1 p-4 overflow-y-auto">
|
||||
{children}
|
||||
</main>
|
||||
<main className="flex-1 p-4 overflow-y-auto">{children}</main>
|
||||
<Footer className="flex-shrink-0" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<OffCanvas isOpen={isOffCanvasOpen} onClose={toggleOffCanvas} />
|
||||
|
||||
@ -1,328 +1,416 @@
|
||||
"use client"
|
||||
'use client'
|
||||
|
||||
import { ApiRoutes, GetApiRoute } from "@/ApiRoutes"
|
||||
import { httpService } from "@/services/httpService"
|
||||
import { FormEvent, useEffect, useRef, useState } from "react"
|
||||
import { useValidation, isValidEmail, isValidHostname } from "@/hooks/useValidation"
|
||||
import { CustomButton, CustomInput } from "@/controls"
|
||||
import { TrashIcon, PlusIcon } from "@heroicons/react/24/solid"
|
||||
import { GetAccountResponse } from "@/models/letsEncryptServer/cache/responses/GetAccountResponse"
|
||||
|
||||
interface CacheAccountHostname {
|
||||
hostname: string
|
||||
expires: Date
|
||||
isUpcomingExpire: boolean
|
||||
}
|
||||
|
||||
interface CacheAccount {
|
||||
accountId: string
|
||||
description?: string
|
||||
contacts: string[]
|
||||
hostnames: CacheAccountHostname[]
|
||||
isEditMode: boolean
|
||||
}
|
||||
import { ApiRoutes, GetApiRoute } from '@/ApiRoutes'
|
||||
import { httpService } from '@/services/httpService'
|
||||
import { FormEvent, useEffect, useRef, useState } from 'react'
|
||||
import {
|
||||
useValidation,
|
||||
isValidEmail,
|
||||
isValidHostname
|
||||
} from '@/hooks/useValidation'
|
||||
import { CustomButton, CustomInput } from '@/controls'
|
||||
import { TrashIcon, PlusIcon } from '@heroicons/react/24/solid'
|
||||
import { GetAccountResponse } from '@/models/letsEncryptServer/cache/responses/GetAccountResponse'
|
||||
import { deepCopy } from './functions'
|
||||
import { CacheAccount } from '@/entities/CacheAccount'
|
||||
|
||||
export default function Page() {
|
||||
const [accounts, setAccounts] = useState<CacheAccount[]>([])
|
||||
const [initialAccounts, setInitialAccounts] = useState<CacheAccount[]>([])
|
||||
const [accounts, setAccounts] = useState<CacheAccount[]>([])
|
||||
const [initialAccounts, setInitialAccounts] = useState<CacheAccount[]>([])
|
||||
|
||||
const {
|
||||
value: newContact,
|
||||
error: contactError,
|
||||
handleChange: handleContactChange
|
||||
} = useValidation({
|
||||
initialValue:"",
|
||||
validateFn: isValidEmail,
|
||||
errorMessage: "Invalid email format."
|
||||
})
|
||||
const {
|
||||
value: newHostname,
|
||||
error: hostnameError,
|
||||
handleChange: handleHostnameChange
|
||||
} = useValidation({
|
||||
initialValue: "",
|
||||
validateFn: isValidHostname,
|
||||
errorMessage: "Invalid hostname format."})
|
||||
const {
|
||||
value: newContact,
|
||||
error: contactError,
|
||||
handleChange: handleContactChange
|
||||
} = useValidation({
|
||||
initialValue: '',
|
||||
validateFn: isValidEmail,
|
||||
errorMessage: 'Invalid email format.'
|
||||
})
|
||||
const {
|
||||
value: newHostname,
|
||||
error: hostnameError,
|
||||
handleChange: handleHostnameChange
|
||||
} = useValidation({
|
||||
initialValue: '',
|
||||
validateFn: isValidHostname,
|
||||
errorMessage: 'Invalid hostname format.'
|
||||
})
|
||||
|
||||
const init = useRef(false)
|
||||
const init = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (init.current) return
|
||||
useEffect(() => {
|
||||
if (init.current) return
|
||||
|
||||
console.log('Fetching accounts')
|
||||
|
||||
console.log("Fetching accounts")
|
||||
const fetchAccounts = async () => {
|
||||
const newAccounts: CacheAccount[] = []
|
||||
const accounts = await httpService.get<GetAccountResponse[]>(
|
||||
GetApiRoute(ApiRoutes.CACHE_ACCOUNTS)
|
||||
)
|
||||
|
||||
const fetchAccounts = async () => {
|
||||
const newAccounts: CacheAccount[] = []
|
||||
const accounts = await httpService.get<GetAccountResponse []>(GetApiRoute(ApiRoutes.CACHE_ACCOUNTS))
|
||||
accounts?.forEach((account) => {
|
||||
newAccounts.push({
|
||||
accountId: account.accountId,
|
||||
contacts: account.contacts,
|
||||
hostnames: account.hostnames.map((h) => ({
|
||||
hostname: h.hostname,
|
||||
expires: new Date(h.expires),
|
||||
isUpcomingExpire: h.isUpcomingExpire
|
||||
})),
|
||||
isEditMode: false
|
||||
})
|
||||
})
|
||||
|
||||
accounts?.forEach((account) => {
|
||||
newAccounts.push({
|
||||
accountId: account.accountId,
|
||||
contacts: account.contacts,
|
||||
hostnames: account.hostnames.map(h => ({
|
||||
hostname: h.hostname,
|
||||
expires: new Date(h.expires),
|
||||
isUpcomingExpire: h.isUpcomingExpire
|
||||
})),
|
||||
isEditMode: false
|
||||
})
|
||||
});
|
||||
|
||||
setAccounts(newAccounts)
|
||||
setInitialAccounts(JSON.parse(JSON.stringify(newAccounts))) // Clone initial state
|
||||
}
|
||||
|
||||
fetchAccounts()
|
||||
init.current = true
|
||||
}, [])
|
||||
|
||||
const toggleEditMode = (accountId: string) => {
|
||||
setAccounts(accounts.map(account =>
|
||||
account.accountId === accountId ? { ...account, isEditMode: !account.isEditMode } : account
|
||||
))
|
||||
setAccounts(newAccounts)
|
||||
setInitialAccounts(deepCopy(newAccounts)) // Clone initial state
|
||||
}
|
||||
|
||||
const deleteAccount = (accountId: string) => {
|
||||
setAccounts(accounts.filter(account => account.accountId !== accountId))
|
||||
fetchAccounts()
|
||||
init.current = true
|
||||
}, [])
|
||||
|
||||
// TODO: Revoke all certificates
|
||||
// TODO: Remove from cache
|
||||
}
|
||||
|
||||
const deleteContact = (accountId: string, contact: string) => {
|
||||
const account = accounts.find(account => account.accountId === accountId)
|
||||
if (account?.contacts.length ?? 0 < 1) return
|
||||
|
||||
// TODO: Remove from cache
|
||||
httpService.delete(GetApiRoute(ApiRoutes.CACHE_ACCOUNT_CONTACT, accountId, contact))
|
||||
|
||||
setAccounts(accounts.map(account =>
|
||||
account.accountId === accountId
|
||||
? { ...account, contacts: account.contacts.filter(c => c !== contact) }
|
||||
: account
|
||||
))
|
||||
}
|
||||
|
||||
const addContact = (accountId: string) => {
|
||||
if (newContact.trim() === "" || contactError) {
|
||||
return
|
||||
}
|
||||
|
||||
if (accounts.find(account => account.accountId === accountId)?.contacts.includes(newContact.trim()))
|
||||
return
|
||||
|
||||
setAccounts(accounts.map(account =>
|
||||
account.accountId === accountId
|
||||
? { ...account, contacts: [...account.contacts, newContact.trim()] }
|
||||
: account
|
||||
))
|
||||
handleContactChange("")
|
||||
}
|
||||
|
||||
const deleteHostname = (accountId: string, hostname: string) => {
|
||||
const account = accounts.find(account => account.accountId === accountId)
|
||||
if (account?.hostnames.length ?? 0 < 1) return
|
||||
|
||||
// TODO: Revoke certificate
|
||||
// TODO: Remove from cache
|
||||
|
||||
setAccounts(accounts.map(account =>
|
||||
account.accountId === accountId
|
||||
? { ...account, hostnames: account.hostnames.filter(h => h.hostname !== hostname) }
|
||||
: account
|
||||
))
|
||||
}
|
||||
|
||||
const addHostname = (accountId: string) => {
|
||||
if (newHostname.trim() === "" || hostnameError) {
|
||||
return
|
||||
}
|
||||
|
||||
if (accounts.find(account => account.accountId === accountId)?.hostnames.some(h => h.hostname === newHostname.trim()))
|
||||
return
|
||||
|
||||
setAccounts(accounts.map(account =>
|
||||
account.accountId === accountId
|
||||
? { ...account, hostnames: [...account.hostnames, { hostname: newHostname.trim(), expires: new Date(), isUpcomingExpire: false }] }
|
||||
: account
|
||||
))
|
||||
handleHostnameChange("")
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: FormEvent<HTMLFormElement>, accountId: string) => {
|
||||
e.preventDefault()
|
||||
|
||||
const account = accounts.find(acc => acc.accountId === accountId)
|
||||
const initialAccount = initialAccounts.find(acc => acc.accountId === accountId)
|
||||
|
||||
if (!account || !initialAccount) return
|
||||
|
||||
const contactChanges = {
|
||||
added: account.contacts.filter(contact => !initialAccount.contacts.includes(contact)),
|
||||
removed: initialAccount.contacts.filter(contact => !account.contacts.includes(contact))
|
||||
}
|
||||
|
||||
const hostnameChanges = {
|
||||
added: account.hostnames.filter(hostname => !initialAccount.hostnames.some(h => h.hostname === hostname.hostname)),
|
||||
removed: initialAccount.hostnames.filter(hostname => !account.hostnames.some(h => h.hostname === hostname.hostname))
|
||||
}
|
||||
|
||||
// Handle contact changes
|
||||
if (contactChanges.added.length > 0) {
|
||||
// TODO: POST new contacts
|
||||
console.log("Added contacts:", contactChanges.added)
|
||||
}
|
||||
if (contactChanges.removed.length > 0) {
|
||||
// TODO: DELETE removed contacts
|
||||
console.log("Removed contacts:", contactChanges.removed)
|
||||
}
|
||||
|
||||
// Handle hostname changes
|
||||
if (hostnameChanges.added.length > 0) {
|
||||
// TODO: POST new hostnames
|
||||
console.log("Added hostnames:", hostnameChanges.added)
|
||||
}
|
||||
if (hostnameChanges.removed.length > 0) {
|
||||
// TODO: DELETE removed hostnames
|
||||
console.log("Removed hostnames:", hostnameChanges.removed)
|
||||
}
|
||||
|
||||
// Save current state as initial state
|
||||
setInitialAccounts(JSON.parse(JSON.stringify(accounts)))
|
||||
toggleEditMode(accountId)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-4">
|
||||
<h1 className="text-4xl font-bold text-center mb-8">LetsEncrypt Auto Renew</h1>
|
||||
{
|
||||
accounts.map(account => (
|
||||
<div key={account.accountId} className="bg-white shadow-lg rounded-lg p-6 mb-6">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-2xl font-semibold">Account: {account.accountId}</h2>
|
||||
<CustomButton onClick={() => toggleEditMode(account.accountId)} className="bg-blue-500 text-white px-3 py-1 rounded">
|
||||
{account.isEditMode ? "View Mode" : "Edit Mode"}
|
||||
</CustomButton>
|
||||
</div>
|
||||
{account.isEditMode ? (
|
||||
<form onSubmit={(e) => handleSubmit(e, account.accountId)}>
|
||||
<div className="mb-4">
|
||||
<h3 className="text-xl font-medium mb-2">Description:</h3>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<h3 className="text-xl font-medium mb-2">Contacts:</h3>
|
||||
<ul className="list-disc list-inside pl-4 mb-2">
|
||||
{
|
||||
account.contacts.map(contact => (
|
||||
<li key={contact} className="text-gray-700 flex justify-between items-center mb-2">
|
||||
{contact}
|
||||
<button onClick={() => deleteContact(account.accountId, contact)} className="bg-red-500 text-white px-2 py-1 rounded ml-4 h-10">
|
||||
<TrashIcon className="h-5 w-5 text-white" />
|
||||
</button>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
<div className="flex items-center mb-4">
|
||||
<CustomInput
|
||||
value={newContact}
|
||||
onChange={handleContactChange}
|
||||
placeholder="Add new contact"
|
||||
type="email"
|
||||
error={contactError}
|
||||
title="New Contact"
|
||||
inputClassName="border p-2 rounded w-full"
|
||||
errorClassName="text-red-500 text-sm mt-1"
|
||||
className="mr-2 flex-grow"
|
||||
/>
|
||||
<button onClick={() => addContact(account.accountId)} className="bg-green-500 text-white p-2 rounded ml-2 h-10 flex items-center">
|
||||
<PlusIcon className="h-5 w-5 text-white" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-medium mb-2">Hostnames:</h3>
|
||||
<ul className="list-disc list-inside pl-4 mb-2">
|
||||
{
|
||||
account.hostnames.map(hostname => (
|
||||
<li key={hostname.hostname} className="text-gray-700 flex justify-between items-center mb-2">
|
||||
<div>
|
||||
{hostname.hostname} - {hostname.expires.toDateString()} -
|
||||
<span className={`ml-2 px-2 py-1 rounded ${hostname.isUpcomingExpire ? 'bg-yellow-200 text-yellow-800' : 'bg-green-200 text-green-800'}`}>
|
||||
{hostname.isUpcomingExpire ? 'Upcoming' : 'Not Upcoming'}
|
||||
</span>
|
||||
</div>
|
||||
<button onClick={() => deleteHostname(account.accountId, hostname.hostname)} className="bg-red-500 text-white px-2 py-1 rounded ml-4 h-10">
|
||||
<TrashIcon className="h-5 w-5 text-white" />
|
||||
</button>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
<div className="flex items-center">
|
||||
<CustomInput
|
||||
value={newHostname}
|
||||
onChange={handleHostnameChange}
|
||||
placeholder="Add new hostname"
|
||||
type="text"
|
||||
error={hostnameError}
|
||||
title="New Hostname"
|
||||
inputClassName="border p-2 rounded w-full"
|
||||
errorClassName="text-red-500 text-sm mt-1"
|
||||
className="mr-2 flex-grow"
|
||||
/>
|
||||
<button onClick={() => addHostname(account.accountId)} className="bg-green-500 text-white p-2 rounded ml-2 h-10 flex items-center">
|
||||
<PlusIcon className="h-5 w-5 text-white" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between mt-4">
|
||||
<button onClick={() => deleteAccount(account.accountId)} className="bg-red-500 text-white px-3 py-1 rounded">
|
||||
<TrashIcon className="h-5 w-5 text-white" />
|
||||
</button>
|
||||
<CustomButton type="submit" className="bg-green-500 text-white px-3 py-1 rounded">
|
||||
Submit
|
||||
</CustomButton>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<>
|
||||
<div className="mb-4">
|
||||
<h3 className="text-xl font-medium mb-2">Description:</h3>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<h3 className="text-xl font-medium mb-2">Contacts:</h3>
|
||||
<ul className="list-disc list-inside pl-4 mb-2">
|
||||
{
|
||||
account.contacts.map(contact => (
|
||||
<li key={contact} className="text-gray-700 mb-2">
|
||||
{contact}
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-medium mb-2">Hostnames:</h3>
|
||||
<ul className="list-disc list-inside pl-4 mb-2">
|
||||
{
|
||||
account.hostnames.map(hostname => (
|
||||
<li key={hostname.hostname} className="text-gray-700 mb-2">
|
||||
{hostname.hostname} - {hostname.expires.toDateString()} -
|
||||
<span className={`ml-2 px-2 py-1 rounded ${hostname.isUpcomingExpire ? 'bg-yellow-200 text-yellow-800' : 'bg-green-200 text-green-800'}`}>
|
||||
{hostname.isUpcomingExpire ? 'Upcoming' : 'Not Upcoming'}
|
||||
</span>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
const toggleEditMode = (accountId: string) => {
|
||||
setAccounts(
|
||||
accounts.map((account) =>
|
||||
account.accountId === accountId
|
||||
? { ...account, isEditMode: !account.isEditMode }
|
||||
: account
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const deleteAccount = (accountId: string) => {
|
||||
setAccounts(accounts.filter((account) => account.accountId !== accountId))
|
||||
|
||||
// TODO: Revoke all certificates
|
||||
// TODO: Remove from cache
|
||||
}
|
||||
|
||||
const deleteContact = (accountId: string, contact: string) => {
|
||||
const account = accounts.find((account) => account.accountId === accountId)
|
||||
if (account?.contacts.length ?? 0 < 1) return
|
||||
|
||||
// TODO: Remove from cache
|
||||
httpService.delete(
|
||||
GetApiRoute(ApiRoutes.CACHE_ACCOUNT_CONTACT, accountId, contact)
|
||||
)
|
||||
|
||||
setAccounts(
|
||||
accounts.map((account) =>
|
||||
account.accountId === accountId
|
||||
? {
|
||||
...account,
|
||||
contacts: account.contacts.filter((c) => c !== contact)
|
||||
}
|
||||
: account
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const addContact = (accountId: string) => {
|
||||
if (newContact.trim() === '' || contactError) {
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
accounts
|
||||
.find((account) => account.accountId === accountId)
|
||||
?.contacts.includes(newContact.trim())
|
||||
)
|
||||
return
|
||||
|
||||
setAccounts(
|
||||
accounts.map((account) =>
|
||||
account.accountId === accountId
|
||||
? { ...account, contacts: [...account.contacts, newContact.trim()] }
|
||||
: account
|
||||
)
|
||||
)
|
||||
handleContactChange('')
|
||||
}
|
||||
|
||||
const deleteHostname = (accountId: string, hostname: string) => {
|
||||
const account = accounts.find((account) => account.accountId === accountId)
|
||||
if (account?.hostnames.length ?? 0 < 1) return
|
||||
|
||||
// TODO: Revoke certificate
|
||||
// TODO: Remove from cache
|
||||
|
||||
setAccounts(
|
||||
accounts.map((account) =>
|
||||
account.accountId === accountId
|
||||
? {
|
||||
...account,
|
||||
hostnames: account.hostnames.filter(
|
||||
(h) => h.hostname !== hostname
|
||||
)
|
||||
}
|
||||
: account
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const addHostname = (accountId: string) => {
|
||||
if (newHostname.trim() === '' || hostnameError) {
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
accounts
|
||||
.find((account) => account.accountId === accountId)
|
||||
?.hostnames.some((h) => h.hostname === newHostname.trim())
|
||||
)
|
||||
return
|
||||
|
||||
setAccounts(
|
||||
accounts.map((account) =>
|
||||
account.accountId === accountId
|
||||
? {
|
||||
...account,
|
||||
hostnames: [
|
||||
...account.hostnames,
|
||||
{
|
||||
hostname: newHostname.trim(),
|
||||
expires: new Date(),
|
||||
isUpcomingExpire: false
|
||||
}
|
||||
]
|
||||
}
|
||||
: account
|
||||
)
|
||||
)
|
||||
handleHostnameChange('')
|
||||
}
|
||||
|
||||
const handleSubmit = async (
|
||||
e: FormEvent<HTMLFormElement>,
|
||||
accountId: string
|
||||
) => {
|
||||
e.preventDefault()
|
||||
|
||||
const account = accounts.find((acc) => acc.accountId === accountId)
|
||||
const initialAccount = initialAccounts.find(
|
||||
(acc) => acc.accountId === accountId
|
||||
)
|
||||
|
||||
if (!account || !initialAccount) return
|
||||
|
||||
const contactChanges = {
|
||||
added: account.contacts.filter(
|
||||
(contact) => !initialAccount.contacts.includes(contact)
|
||||
),
|
||||
removed: initialAccount.contacts.filter(
|
||||
(contact) => !account.contacts.includes(contact)
|
||||
)
|
||||
}
|
||||
|
||||
const hostnameChanges = {
|
||||
added: account.hostnames.filter(
|
||||
(hostname) =>
|
||||
!initialAccount.hostnames.some(
|
||||
(h) => h.hostname === hostname.hostname
|
||||
)
|
||||
),
|
||||
removed: initialAccount.hostnames.filter(
|
||||
(hostname) =>
|
||||
!account.hostnames.some((h) => h.hostname === hostname.hostname)
|
||||
)
|
||||
}
|
||||
|
||||
// Handle contact changes
|
||||
if (contactChanges.added.length > 0) {
|
||||
// TODO: POST new contacts
|
||||
console.log('Added contacts:', contactChanges.added)
|
||||
}
|
||||
if (contactChanges.removed.length > 0) {
|
||||
// TODO: DELETE removed contacts
|
||||
console.log('Removed contacts:', contactChanges.removed)
|
||||
}
|
||||
|
||||
// Handle hostname changes
|
||||
if (hostnameChanges.added.length > 0) {
|
||||
// TODO: POST new hostnames
|
||||
console.log('Added hostnames:', hostnameChanges.added)
|
||||
}
|
||||
if (hostnameChanges.removed.length > 0) {
|
||||
// TODO: DELETE removed hostnames
|
||||
console.log('Removed hostnames:', hostnameChanges.removed)
|
||||
}
|
||||
|
||||
// Save current state as initial state
|
||||
setInitialAccounts(deepCopy(accounts))
|
||||
toggleEditMode(accountId)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-4">
|
||||
<h1 className="text-4xl font-bold text-center mb-8">
|
||||
LetsEncrypt Auto Renew
|
||||
</h1>
|
||||
{accounts.map((account) => (
|
||||
<div
|
||||
key={account.accountId}
|
||||
className="bg-white shadow-lg rounded-lg p-6 mb-6"
|
||||
>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-2xl font-semibold">
|
||||
Account: {account.accountId}
|
||||
</h2>
|
||||
<CustomButton
|
||||
onClick={() => toggleEditMode(account.accountId)}
|
||||
className="bg-blue-500 text-white px-3 py-1 rounded"
|
||||
>
|
||||
{account.isEditMode ? 'View Mode' : 'Edit Mode'}
|
||||
</CustomButton>
|
||||
</div>
|
||||
{account.isEditMode ? (
|
||||
<form onSubmit={(e) => handleSubmit(e, account.accountId)}>
|
||||
<div className="mb-4">
|
||||
<h3 className="text-xl font-medium mb-2">Description:</h3>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<h3 className="text-xl font-medium mb-2">Contacts:</h3>
|
||||
<ul className="list-disc list-inside pl-4 mb-2">
|
||||
{account.contacts.map((contact) => (
|
||||
<li
|
||||
key={contact}
|
||||
className="text-gray-700 flex justify-between items-center mb-2"
|
||||
>
|
||||
{contact}
|
||||
<button
|
||||
onClick={() =>
|
||||
deleteContact(account.accountId, contact)
|
||||
}
|
||||
className="bg-red-500 text-white px-2 py-1 rounded ml-4 h-10"
|
||||
>
|
||||
<TrashIcon className="h-5 w-5 text-white" />
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="flex items-center mb-4">
|
||||
<CustomInput
|
||||
value={newContact}
|
||||
onChange={handleContactChange}
|
||||
placeholder="Add new contact"
|
||||
type="email"
|
||||
error={contactError}
|
||||
title="New Contact"
|
||||
inputClassName="border p-2 rounded w-full"
|
||||
errorClassName="text-red-500 text-sm mt-1"
|
||||
className="mr-2 flex-grow"
|
||||
/>
|
||||
<button
|
||||
onClick={() => addContact(account.accountId)}
|
||||
className="bg-green-500 text-white p-2 rounded ml-2 h-10 flex items-center"
|
||||
>
|
||||
<PlusIcon className="h-5 w-5 text-white" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-medium mb-2">Hostnames:</h3>
|
||||
<ul className="list-disc list-inside pl-4 mb-2">
|
||||
{account.hostnames.map((hostname) => (
|
||||
<li
|
||||
key={hostname.hostname}
|
||||
className="text-gray-700 flex justify-between items-center mb-2"
|
||||
>
|
||||
<div>
|
||||
{hostname.hostname} - {hostname.expires.toDateString()}{' '}
|
||||
-
|
||||
<span
|
||||
className={`ml-2 px-2 py-1 rounded ${hostname.isUpcomingExpire ? 'bg-yellow-200 text-yellow-800' : 'bg-green-200 text-green-800'}`}
|
||||
>
|
||||
{hostname.isUpcomingExpire
|
||||
? 'Upcoming'
|
||||
: 'Not Upcoming'}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() =>
|
||||
deleteHostname(account.accountId, hostname.hostname)
|
||||
}
|
||||
className="bg-red-500 text-white px-2 py-1 rounded ml-4 h-10"
|
||||
>
|
||||
<TrashIcon className="h-5 w-5 text-white" />
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="flex items-center">
|
||||
<CustomInput
|
||||
value={newHostname}
|
||||
onChange={handleHostnameChange}
|
||||
placeholder="Add new hostname"
|
||||
type="text"
|
||||
error={hostnameError}
|
||||
title="New Hostname"
|
||||
inputClassName="border p-2 rounded w-full"
|
||||
errorClassName="text-red-500 text-sm mt-1"
|
||||
className="mr-2 flex-grow"
|
||||
/>
|
||||
<button
|
||||
onClick={() => addHostname(account.accountId)}
|
||||
className="bg-green-500 text-white p-2 rounded ml-2 h-10 flex items-center"
|
||||
>
|
||||
<PlusIcon className="h-5 w-5 text-white" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between mt-4">
|
||||
<button
|
||||
onClick={() => deleteAccount(account.accountId)}
|
||||
className="bg-red-500 text-white px-3 py-1 rounded"
|
||||
>
|
||||
<TrashIcon className="h-5 w-5 text-white" />
|
||||
</button>
|
||||
<CustomButton
|
||||
type="submit"
|
||||
className="bg-green-500 text-white px-3 py-1 rounded"
|
||||
>
|
||||
Submit
|
||||
</CustomButton>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<>
|
||||
<div className="mb-4">
|
||||
<h3 className="text-xl font-medium mb-2">Description:</h3>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<h3 className="text-xl font-medium mb-2">Contacts:</h3>
|
||||
<ul className="list-disc list-inside pl-4 mb-2">
|
||||
{account.contacts.map((contact) => (
|
||||
<li key={contact} className="text-gray-700 mb-2">
|
||||
{contact}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-medium mb-2">Hostnames:</h3>
|
||||
<ul className="list-disc list-inside pl-4 mb-2">
|
||||
{account.hostnames.map((hostname) => (
|
||||
<li key={hostname.hostname} className="text-gray-700 mb-2">
|
||||
{hostname.hostname} - {hostname.expires.toDateString()} -
|
||||
<span
|
||||
className={`ml-2 px-2 py-1 rounded ${hostname.isUpcomingExpire ? 'bg-yellow-200 text-yellow-800' : 'bg-green-200 text-green-800'}`}
|
||||
>
|
||||
{hostname.isUpcomingExpire
|
||||
? 'Upcoming'
|
||||
: 'Not Upcoming'}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,209 +1,233 @@
|
||||
"use client"
|
||||
'use client'
|
||||
|
||||
import { ApiRoutes, GetApiRoute } from "@/ApiRoutes"
|
||||
import { httpService } from "@/services/httpService"
|
||||
import { FormEvent, useEffect, useRef, useState } from "react"
|
||||
import { useValidation, isValidContact, isValidHostname } from "@/hooks/useValidation"
|
||||
import { CustomButton, CustomInput } from "@/controls"
|
||||
import { FaTrash, FaPlus } from "react-icons/fa"
|
||||
import { GetAccountResponse } from "@/models/letsEncryptServer/cache/responses/GetAccountResponse"
|
||||
|
||||
interface CacheAccountHostname {
|
||||
hostname: string
|
||||
expires: Date
|
||||
isUpcomingExpire: boolean
|
||||
}
|
||||
import { ApiRoutes, GetApiRoute } from '@/ApiRoutes'
|
||||
import { httpService } from '@/services/httpService'
|
||||
import { FormEvent, useEffect, useRef, useState } from 'react'
|
||||
import {
|
||||
useValidation,
|
||||
isValidContact,
|
||||
isValidHostname
|
||||
} from '@/hooks/useValidation'
|
||||
import { CustomButton, CustomInput } from '@/controls'
|
||||
import { FaTrash, FaPlus } from 'react-icons/fa'
|
||||
import { GetAccountResponse } from '@/models/letsEncryptServer/cache/responses/GetAccountResponse'
|
||||
import { deepCopy } from '../functions'
|
||||
|
||||
interface CacheAccount {
|
||||
accountId: string
|
||||
description?: string
|
||||
contacts: string[]
|
||||
hostnames: CacheAccountHostname[]
|
||||
isEditMode: boolean
|
||||
description?: string
|
||||
contacts: string[]
|
||||
hostnames: string[]
|
||||
}
|
||||
|
||||
const RegisterPage = () => {
|
||||
const [accounts, setAccounts] = useState<CacheAccount[]>([])
|
||||
const [initialAccounts, setInitialAccounts] = useState<CacheAccount[]>([])
|
||||
const [description, setDescription] = useState("")
|
||||
const [contacts, setContacts] = useState<string[]>([])
|
||||
const [hostnames, setHostnames] = useState<string[]>([])
|
||||
const [account, setAccount] = useState<CacheAccount | null>(null)
|
||||
|
||||
const {
|
||||
value: newContact,
|
||||
error: contactError,
|
||||
handleChange: handleContactChange,
|
||||
reset: resetContact
|
||||
} = useValidation({
|
||||
initialValue: "",
|
||||
validateFn: isValidContact,
|
||||
errorMessage: "Invalid contact. Must be a valid email or phone number."
|
||||
const {
|
||||
value: newContact,
|
||||
error: contactError,
|
||||
handleChange: handleContactChange,
|
||||
reset: resetContact
|
||||
} = useValidation({
|
||||
initialValue: '',
|
||||
validateFn: isValidContact,
|
||||
errorMessage: 'Invalid contact. Must be a valid email or phone number.'
|
||||
})
|
||||
|
||||
const {
|
||||
value: newHostname,
|
||||
error: hostnameError,
|
||||
handleChange: handleHostnameChange,
|
||||
reset: resetHostname
|
||||
} = useValidation({
|
||||
initialValue: '',
|
||||
validateFn: isValidHostname,
|
||||
errorMessage: 'Invalid hostname format.'
|
||||
})
|
||||
|
||||
const init = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (init.current) return
|
||||
|
||||
init.current = true
|
||||
}, [])
|
||||
|
||||
const handleDescription = (description: string) => {}
|
||||
|
||||
const handleAddContact = () => {
|
||||
if (newContact !== '' || contactError) return
|
||||
|
||||
setAccount((prev) => {
|
||||
const newAccount: CacheAccount =
|
||||
prev !== null
|
||||
? deepCopy(prev)
|
||||
: {
|
||||
contacts: [],
|
||||
hostnames: []
|
||||
}
|
||||
|
||||
newAccount.contacts.push(newContact)
|
||||
|
||||
return newAccount
|
||||
})
|
||||
|
||||
const {
|
||||
value: newHostname,
|
||||
error: hostnameError,
|
||||
handleChange: handleHostnameChange,
|
||||
reset: resetHostname
|
||||
} = useValidation({
|
||||
initialValue: "",
|
||||
validateFn: isValidHostname,
|
||||
errorMessage: "Invalid hostname format."
|
||||
resetContact()
|
||||
}
|
||||
|
||||
const handleAddHostname = () => {
|
||||
if (newHostname !== '' || hostnameError) return
|
||||
|
||||
setAccount((prev) => {
|
||||
const newAccount: CacheAccount =
|
||||
prev !== null
|
||||
? deepCopy(prev)
|
||||
: {
|
||||
contacts: [],
|
||||
hostnames: []
|
||||
}
|
||||
|
||||
newAccount.hostnames.push(newHostname)
|
||||
|
||||
return newAccount
|
||||
})
|
||||
|
||||
const init = useRef(false)
|
||||
resetHostname()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (init.current) return
|
||||
const handleDeleteContact = (contact: string) => {
|
||||
setAccount((prev) => {
|
||||
if (prev === null) return null
|
||||
|
||||
const fetchAccounts = async () => {
|
||||
const newAccounts: CacheAccount[] = []
|
||||
const accounts = await httpService.get<GetAccountResponse[]>(GetApiRoute(ApiRoutes.CACHE_ACCOUNTS))
|
||||
const newAccount = deepCopy(prev)
|
||||
newAccount.contacts = newAccount.contacts.filter((c) => c !== contact)
|
||||
|
||||
accounts?.forEach((account) => {
|
||||
newAccounts.push({
|
||||
accountId: account.accountId,
|
||||
contacts: account.contacts,
|
||||
hostnames: account.hostnames.map(h => ({
|
||||
hostname: h.hostname,
|
||||
expires: new Date(h.expires),
|
||||
isUpcomingExpire: h.isUpcomingExpire
|
||||
})),
|
||||
isEditMode: false
|
||||
})
|
||||
})
|
||||
return newAccount
|
||||
})
|
||||
}
|
||||
|
||||
setAccounts(newAccounts)
|
||||
setInitialAccounts(JSON.parse(JSON.stringify(newAccounts))) // Clone initial state
|
||||
}
|
||||
const handleDeleteHostname = (hostname: string) => {
|
||||
setAccount((prev) => {
|
||||
if (prev === null) return null
|
||||
|
||||
fetchAccounts()
|
||||
init.current = true
|
||||
}, [])
|
||||
const newAccount = deepCopy(prev)
|
||||
newAccount.hostnames = newAccount.hostnames.filter((h) => h !== hostname)
|
||||
|
||||
const handleAddContact = () => {
|
||||
if (newContact.trim() !== "" && !contactError) {
|
||||
setContacts([...contacts, newContact.trim()])
|
||||
resetContact()
|
||||
}
|
||||
}
|
||||
return newAccount
|
||||
})
|
||||
}
|
||||
|
||||
const handleAddHostname = () => {
|
||||
if (newHostname.trim() !== "" && !hostnameError) {
|
||||
setHostnames([...hostnames, newHostname.trim()])
|
||||
resetHostname()
|
||||
}
|
||||
}
|
||||
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
|
||||
const handleDeleteContact = (contact: string) => {
|
||||
setContacts(contacts.filter(c => c !== contact))
|
||||
}
|
||||
console.log(account)
|
||||
}
|
||||
|
||||
const handleDeleteHostname = (hostname: string) => {
|
||||
setHostnames(hostnames.filter(h => h !== hostname))
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (!description || contacts.length === 0 || hostnames.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const newAccount = {
|
||||
description,
|
||||
contacts,
|
||||
hostnames: hostnames.map(hostname => ({ hostname, expires: new Date(), isUpcomingExpire: false }))
|
||||
}
|
||||
|
||||
// TODO: Implement API call to create new account
|
||||
console.log("New account data:", newAccount)
|
||||
|
||||
// Reset form fields
|
||||
setDescription("")
|
||||
setContacts([])
|
||||
setHostnames([])
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-4">
|
||||
<h1 className="text-4xl font-bold text-center mb-8">Register LetsEncrypt Account</h1>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mb-4">
|
||||
<CustomInput
|
||||
type="text"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="Account Description"
|
||||
title="Description"
|
||||
inputClassName="border p-2 rounded w-full"
|
||||
className="mb-4"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<h3 className="text-xl font-medium mb-2">Contacts:</h3>
|
||||
<ul className="list-disc list-inside pl-4 mb-2">
|
||||
{contacts.map(contact => (
|
||||
<li key={contact} className="text-gray-700 flex justify-between items-center mb-2">
|
||||
{contact}
|
||||
<button type="button" onClick={() => handleDeleteContact(contact)} className="bg-red-500 text-white px-2 py-1 rounded ml-4">
|
||||
<FaTrash />
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="flex items-center mb-4">
|
||||
<CustomInput
|
||||
value={newContact}
|
||||
onChange={handleContactChange}
|
||||
placeholder="Add contact"
|
||||
type="text"
|
||||
error={contactError}
|
||||
title="New Contact"
|
||||
inputClassName="border p-2 rounded w-full"
|
||||
errorClassName="text-red-500 text-sm mt-1"
|
||||
className="mr-2 flex-grow"
|
||||
/>
|
||||
<button type="button" onClick={handleAddContact} className="bg-green-500 text-white p-2 rounded ml-2 h-10 flex items-center">
|
||||
<FaPlus />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<h3 className="text-xl font-medium mb-2">Hostnames:</h3>
|
||||
<ul className="list-disc list-inside pl-4 mb-2">
|
||||
{hostnames.map(hostname => (
|
||||
<li key={hostname} className="text-gray-700 flex justify-between items-center mb-2">
|
||||
{hostname}
|
||||
<button type="button" onClick={() => handleDeleteHostname(hostname)} className="bg-red-500 text-white px-2 py-1 rounded ml-4">
|
||||
<FaTrash />
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="flex items-center">
|
||||
<CustomInput
|
||||
value={newHostname}
|
||||
onChange={handleHostnameChange}
|
||||
placeholder="Add hostname"
|
||||
type="text"
|
||||
error={hostnameError}
|
||||
title="New Hostname"
|
||||
inputClassName="border p-2 rounded w-full"
|
||||
errorClassName="text-red-500 text-sm mt-1"
|
||||
className="mr-2 flex-grow"
|
||||
/>
|
||||
<button type="button" onClick={handleAddHostname} className="bg-green-500 text-white p-2 rounded ml-2 h-10 flex items-center">
|
||||
<FaPlus />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<CustomButton type="submit" className="bg-green-500 text-white px-3 py-1 rounded">
|
||||
Create Account
|
||||
</CustomButton>
|
||||
</form>
|
||||
return (
|
||||
<div className="container mx-auto p-4">
|
||||
<h1 className="text-4xl font-bold text-center mb-8">
|
||||
Register LetsEncrypt Account
|
||||
</h1>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mb-4">
|
||||
<CustomInput
|
||||
type="text"
|
||||
value={account?.description ?? ''}
|
||||
onChange={handleDescription}
|
||||
placeholder="Account Description"
|
||||
title="Description"
|
||||
inputClassName="border p-2 rounded w-full"
|
||||
className="mb-4"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
<div className="mb-4">
|
||||
<h3 className="text-xl font-medium mb-2">Contacts:</h3>
|
||||
<ul className="list-disc list-inside pl-4 mb-2">
|
||||
{account?.contacts.map((contact) => (
|
||||
<li
|
||||
key={contact}
|
||||
className="text-gray-700 flex justify-between items-center mb-2"
|
||||
>
|
||||
{contact}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleDeleteContact(contact)}
|
||||
className="bg-red-500 text-white px-2 py-1 rounded ml-4"
|
||||
>
|
||||
<FaTrash />
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="flex items-center mb-4">
|
||||
<CustomInput
|
||||
value={newContact}
|
||||
onChange={handleContactChange}
|
||||
placeholder="Add contact"
|
||||
type="text"
|
||||
error={contactError}
|
||||
title="New Contact"
|
||||
inputClassName="border p-2 rounded w-full"
|
||||
errorClassName="text-red-500 text-sm mt-1"
|
||||
className="mr-2 flex-grow"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAddContact}
|
||||
className="bg-green-500 text-white p-2 rounded ml-2 h-10 flex items-center"
|
||||
>
|
||||
<FaPlus />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<h3 className="text-xl font-medium mb-2">Hostnames:</h3>
|
||||
<ul className="list-disc list-inside pl-4 mb-2">
|
||||
{account?.hostnames.map((hostname) => (
|
||||
<li
|
||||
key={hostname}
|
||||
className="text-gray-700 flex justify-between items-center mb-2"
|
||||
>
|
||||
{hostname}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleDeleteHostname(hostname)}
|
||||
className="bg-red-500 text-white px-2 py-1 rounded ml-4"
|
||||
>
|
||||
<FaTrash />
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="flex items-center">
|
||||
<CustomInput
|
||||
value={newHostname}
|
||||
onChange={handleHostnameChange}
|
||||
placeholder="Add hostname"
|
||||
type="text"
|
||||
error={hostnameError}
|
||||
title="New Hostname"
|
||||
inputClassName="border p-2 rounded w-full"
|
||||
errorClassName="text-red-500 text-sm mt-1"
|
||||
className="mr-2 flex-grow"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAddHostname}
|
||||
className="bg-green-500 text-white p-2 rounded ml-2 h-10 flex items-center"
|
||||
>
|
||||
<FaPlus />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<CustomButton
|
||||
type="submit"
|
||||
className="bg-green-500 text-white px-3 py-1 rounded"
|
||||
>
|
||||
Create Account
|
||||
</CustomButton>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RegisterPage
|
||||
|
||||
@ -1,12 +1,10 @@
|
||||
import React from 'react'
|
||||
|
||||
|
||||
interface FooterProps {
|
||||
className?: string
|
||||
}
|
||||
|
||||
const Footer = (props: FooterProps) => {
|
||||
|
||||
const { className } = props
|
||||
return (
|
||||
<footer className={`bg-gray-900 text-white text-center p-4 ${className}`}>
|
||||
@ -15,6 +13,4 @@ const Footer = (props: FooterProps) => {
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Footer
|
||||
}
|
||||
export { Footer }
|
||||
|
||||
@ -7,7 +7,9 @@ import './loader.css'
|
||||
|
||||
const Loader: React.FC = () => {
|
||||
const dispatch = useDispatch()
|
||||
const activeRequests = useSelector((state: RootState) => state.loader.activeRequests)
|
||||
const activeRequests = useSelector(
|
||||
(state: RootState) => state.loader.activeRequests
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
let timeout: NodeJS.Timeout | null = null
|
||||
@ -35,6 +37,4 @@ const Loader: React.FC = () => {
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Loader
|
||||
}
|
||||
export { Loader }
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import React, { FC } from 'react'
|
||||
|
||||
interface OffCanvasProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const OffCanvas: FC<OffCanvasProps> = ({ isOpen, onClose }) => {
|
||||
@ -29,6 +29,4 @@ const OffCanvas: FC<OffCanvasProps> = ({ isOpen, onClose }) => {
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
OffCanvas
|
||||
}
|
||||
export { OffCanvas }
|
||||
|
||||
@ -1,42 +1,50 @@
|
||||
import React, { FC } from 'react';
|
||||
import { FaHome, FaUserPlus, FaBars, FaSyncAlt } from 'react-icons/fa';
|
||||
import Link from 'next/link';
|
||||
import React, { FC } from 'react'
|
||||
import { FaHome, FaUserPlus, FaBars, FaSyncAlt } from 'react-icons/fa'
|
||||
import Link from 'next/link'
|
||||
|
||||
interface SideMenuProps {
|
||||
isCollapsed: boolean;
|
||||
toggleSidebar: () => void;
|
||||
isCollapsed: boolean
|
||||
toggleSidebar: () => void
|
||||
}
|
||||
|
||||
const menuItems = [
|
||||
{ icon: <FaSyncAlt />, label: 'Auto Renew', path: '/' },
|
||||
{ icon: <FaUserPlus />, label: 'Register', path: '/register' }
|
||||
];
|
||||
]
|
||||
|
||||
const SideMenu: FC<SideMenuProps> = ({ isCollapsed, toggleSidebar }) => {
|
||||
return (
|
||||
<div className={`flex flex-col bg-gray-800 text-white transition-all duration-300 ${isCollapsed ? 'w-16' : 'w-64'} h-full`}>
|
||||
<div
|
||||
className={`flex flex-col bg-gray-800 text-white transition-all duration-300 ${isCollapsed ? 'w-16' : 'w-64'} h-full`}
|
||||
>
|
||||
<div className="flex items-center h-16 bg-gray-900 relative">
|
||||
<button onClick={toggleSidebar} className="absolute left-4">
|
||||
<FaBars />
|
||||
</button>
|
||||
<h1 className={`${isCollapsed ? 'hidden' : 'block'} text-2xl font-bold ml-12`}>Certs UI</h1>
|
||||
<h1
|
||||
className={`${isCollapsed ? 'hidden' : 'block'} text-2xl font-bold ml-12`}
|
||||
>
|
||||
Certs UI
|
||||
</h1>
|
||||
</div>
|
||||
<nav className="flex-1">
|
||||
<ul>
|
||||
{menuItems.map((item, index) => (
|
||||
<li key={index} className="hover:bg-gray-700">
|
||||
<Link href={item.path} className="flex items-center w-full p-4">
|
||||
<span className={`${isCollapsed ? 'mr-0' : 'mr-4'}`}>{item.icon}</span>
|
||||
<span className={`${isCollapsed ? 'hidden' : 'block'}`}>{item.label}</span>
|
||||
<span className={`${isCollapsed ? 'mr-0' : 'mr-4'}`}>
|
||||
{item.icon}
|
||||
</span>
|
||||
<span className={`${isCollapsed ? 'hidden' : 'block'}`}>
|
||||
{item.label}
|
||||
</span>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
SideMenu
|
||||
};
|
||||
export { SideMenu }
|
||||
|
||||
@ -34,8 +34,8 @@ const Toast = () => {
|
||||
}, [toastState, dispatch])
|
||||
|
||||
return (
|
||||
<ToastContainer
|
||||
position="bottom-right"
|
||||
<ToastContainer
|
||||
position="bottom-right"
|
||||
theme="dark"
|
||||
autoClose={5000}
|
||||
hideProgressBar={false}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
"use client" // Add this line
|
||||
'use client' // Add this line
|
||||
|
||||
import React, { FC, useState } from 'react'
|
||||
import { FaCog, FaBars } from 'react-icons/fa'
|
||||
@ -48,12 +48,10 @@ const TopMenu: FC<TopMenuProps> = ({ onToggleOffCanvas }) => {
|
||||
<FaCog />
|
||||
</button>
|
||||
<button onClick={toggleMenu} className="md:hidden">
|
||||
<FaBars />
|
||||
<FaBars />
|
||||
</button>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
TopMenu
|
||||
}
|
||||
export { TopMenu }
|
||||
|
||||
@ -1,28 +1,33 @@
|
||||
"use client"
|
||||
'use client'
|
||||
import React, { FC } from 'react'
|
||||
|
||||
interface CustomButtonProps {
|
||||
onClick?: () => void
|
||||
className?: string
|
||||
children: React.ReactNode
|
||||
disabled?: boolean
|
||||
type?: "button" | "submit" | "reset"
|
||||
onClick?: () => void
|
||||
className?: string
|
||||
children: React.ReactNode
|
||||
disabled?: boolean
|
||||
type?: 'button' | 'submit' | 'reset'
|
||||
}
|
||||
|
||||
const CustomButton: FC<CustomButtonProps> = (props) => {
|
||||
const {
|
||||
onClick,
|
||||
className = '',
|
||||
children,
|
||||
disabled = false,
|
||||
type = 'button'
|
||||
} = props
|
||||
|
||||
const { onClick, className = '', children, disabled = false, type = 'button' } = props
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={className}
|
||||
disabled={disabled}
|
||||
type={type}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={className}
|
||||
disabled={disabled}
|
||||
type={type}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export { CustomButton }
|
||||
|
||||
@ -1,48 +1,47 @@
|
||||
// components/CustomInput.tsx
|
||||
"use client"
|
||||
'use client'
|
||||
import React from 'react'
|
||||
|
||||
interface CustomInputProps {
|
||||
value: string
|
||||
onChange?: (value: string) => void
|
||||
placeholder?: string
|
||||
type: 'text' | 'password' | 'email' | 'number' | 'tel' | 'url'
|
||||
error?: string
|
||||
title?: string
|
||||
inputClassName?: string
|
||||
errorClassName?: string
|
||||
className?: string
|
||||
value: string
|
||||
onChange?: (value: string) => void
|
||||
placeholder?: string
|
||||
type: 'text' | 'password' | 'email' | 'number' | 'tel' | 'url'
|
||||
error?: string
|
||||
title?: string
|
||||
inputClassName?: string
|
||||
errorClassName?: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
const CustomInput: React.FC<CustomInputProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
placeholder = '',
|
||||
type = 'text',
|
||||
error,
|
||||
title,
|
||||
inputClassName = '',
|
||||
errorClassName = '',
|
||||
className = ''
|
||||
value,
|
||||
onChange,
|
||||
placeholder = '',
|
||||
type = 'text',
|
||||
error,
|
||||
title,
|
||||
inputClassName = '',
|
||||
errorClassName = '',
|
||||
className = ''
|
||||
}) => {
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onChange?.(e.target.value)
|
||||
}
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onChange?.(e.target.value);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{title && <label>{title}</label>}
|
||||
<input
|
||||
type={type}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
placeholder={placeholder}
|
||||
className={inputClassName}
|
||||
/>
|
||||
{error && <p className={errorClassName}>{error}</p>}
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div className={className}>
|
||||
{title && <label>{title}</label>}
|
||||
<input
|
||||
type={type}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
placeholder={placeholder}
|
||||
className={inputClassName}
|
||||
/>
|
||||
{error && <p className={errorClassName}>{error}</p>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { CustomInput }
|
||||
|
||||
@ -1,7 +1,4 @@
|
||||
import { CustomButton } from "./customButton";
|
||||
import { CustomInput } from "./customInput";
|
||||
import { CustomButton } from './customButton'
|
||||
import { CustomInput } from './customInput'
|
||||
|
||||
export {
|
||||
CustomButton,
|
||||
CustomInput
|
||||
}
|
||||
export { CustomButton, CustomInput }
|
||||
|
||||
9
src/ClientApp/entities/CacheAccount.ts
Normal file
9
src/ClientApp/entities/CacheAccount.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { CacheAccountHostname } from './CacheAccountHostname'
|
||||
|
||||
export interface CacheAccount {
|
||||
accountId: string
|
||||
description?: string
|
||||
contacts: string[]
|
||||
hostnames: CacheAccountHostname[]
|
||||
isEditMode: boolean
|
||||
}
|
||||
5
src/ClientApp/entities/CacheAccountHostname.ts
Normal file
5
src/ClientApp/entities/CacheAccountHostname.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface CacheAccountHostname {
|
||||
hostname: string
|
||||
expires: Date
|
||||
isUpcomingExpire: boolean
|
||||
}
|
||||
@ -1,55 +1,64 @@
|
||||
import { useState, useEffect } from "react"
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
// Helper functions for validation
|
||||
const isValidEmail = (email: string) => {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
return emailRegex.test(email)
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
return emailRegex.test(email)
|
||||
}
|
||||
|
||||
const isValidPhoneNumber = (phone: string) => {
|
||||
const phoneRegex = /^\+?[1-9]\d{1,14}$/
|
||||
return phoneRegex.test(phone)
|
||||
const phoneRegex = /^\+?[1-9]\d{1,14}$/
|
||||
return phoneRegex.test(phone)
|
||||
}
|
||||
|
||||
const isValidContact = (contact: string) => {
|
||||
return isValidEmail(contact) || isValidPhoneNumber(contact)
|
||||
return isValidEmail(contact) || isValidPhoneNumber(contact)
|
||||
}
|
||||
|
||||
const isValidHostname = (hostname: string) => {
|
||||
const hostnameRegex = /^(?!:\/\/)([a-zA-Z0-9-_]{1,63}\.?)+[a-zA-Z]{2,6}$/
|
||||
return hostnameRegex.test(hostname)
|
||||
const hostnameRegex = /^(?!:\/\/)([a-zA-Z0-9-_]{1,63}\.?)+[a-zA-Z]{2,6}$/
|
||||
return hostnameRegex.test(hostname)
|
||||
}
|
||||
|
||||
// Props interface for useValidation hook
|
||||
interface UseValidationProps {
|
||||
initialValue: string
|
||||
validateFn: (value: string) => boolean
|
||||
errorMessage: string
|
||||
initialValue: string
|
||||
validateFn: (value: string) => boolean
|
||||
errorMessage: string
|
||||
}
|
||||
|
||||
// Custom hook for input validation
|
||||
const useValidation = ({ initialValue, validateFn, errorMessage }: UseValidationProps) => {
|
||||
const [value, setValue] = useState(initialValue)
|
||||
const [error, setError] = useState("")
|
||||
const useValidation = ({
|
||||
initialValue,
|
||||
validateFn,
|
||||
errorMessage
|
||||
}: UseValidationProps) => {
|
||||
const [value, setValue] = useState(initialValue)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
const handleChange = (newValue: string) => {
|
||||
|
||||
console.log(newValue)
|
||||
setValue(newValue)
|
||||
if (newValue.trim() === "") {
|
||||
setError("This field cannot be empty.")
|
||||
} else if (!validateFn(newValue.trim())) {
|
||||
setError(errorMessage)
|
||||
} else {
|
||||
setError("")
|
||||
}
|
||||
const handleChange = (newValue: string) => {
|
||||
console.log(newValue)
|
||||
setValue(newValue)
|
||||
if (newValue.trim() === '') {
|
||||
setError('This field cannot be empty.')
|
||||
} else if (!validateFn(newValue.trim())) {
|
||||
setError(errorMessage)
|
||||
} else {
|
||||
setError('')
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
handleChange(initialValue)
|
||||
}, [initialValue])
|
||||
useEffect(() => {
|
||||
handleChange(initialValue)
|
||||
}, [initialValue])
|
||||
|
||||
return { value, error, handleChange, reset: () => setValue("") }
|
||||
return { value, error, handleChange, reset: () => setValue('') }
|
||||
}
|
||||
|
||||
export { useValidation, isValidEmail, isValidPhoneNumber, isValidContact, isValidHostname }
|
||||
export {
|
||||
useValidation,
|
||||
isValidEmail,
|
||||
isValidPhoneNumber,
|
||||
isValidContact,
|
||||
isValidHostname
|
||||
}
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
import { HostnameResponse } from "./HostnameResponse";
|
||||
import { HostnameResponse } from './HostnameResponse'
|
||||
|
||||
export interface GetAccountResponse {
|
||||
accountId: string,
|
||||
contacts: string[],
|
||||
hostnames: HostnameResponse[],
|
||||
accountId: string
|
||||
contacts: string[]
|
||||
hostnames: HostnameResponse[]
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
export interface GetContactsResponse {
|
||||
contacts: string[]
|
||||
}
|
||||
contacts: string[]
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { HostnameResponse } from "./HostnameResponse";
|
||||
import { HostnameResponse } from './HostnameResponse'
|
||||
|
||||
export interface GetHostnamesResponse {
|
||||
hostnames: HostnameResponse[]
|
||||
}
|
||||
hostnames: HostnameResponse[]
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
export interface HostnameResponse {
|
||||
hostname: string
|
||||
expires: string
|
||||
isUpcomingExpire: boolean
|
||||
}
|
||||
hostname: string
|
||||
expires: string
|
||||
isUpcomingExpire: boolean
|
||||
}
|
||||
|
||||
106
src/ClientApp/package-lock.json
generated
106
src/ClientApp/package-lock.json
generated
@ -23,7 +23,10 @@
|
||||
"@types/react-dom": "^18",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.2.3",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"postcss": "^8",
|
||||
"prettier": "^3.3.2",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5"
|
||||
}
|
||||
@ -435,6 +438,18 @@
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@pkgr/core": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz",
|
||||
"integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/unts"
|
||||
}
|
||||
},
|
||||
"node_modules/@reduxjs/toolkit": {
|
||||
"version": "2.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.2.5.tgz",
|
||||
@ -1639,6 +1654,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-config-prettier": {
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz",
|
||||
"integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"eslint-config-prettier": "bin/cli.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-import-resolver-node": {
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz",
|
||||
@ -1801,6 +1828,36 @@
|
||||
"eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-prettier": {
|
||||
"version": "5.1.3",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz",
|
||||
"integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"prettier-linter-helpers": "^1.0.0",
|
||||
"synckit": "^0.8.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint-plugin-prettier"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/eslint": ">=8.0.0",
|
||||
"eslint": ">=8.0.0",
|
||||
"eslint-config-prettier": "*",
|
||||
"prettier": ">=3.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/eslint": {
|
||||
"optional": true
|
||||
},
|
||||
"eslint-config-prettier": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-react": {
|
||||
"version": "7.34.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.34.2.tgz",
|
||||
@ -1976,6 +2033,12 @@
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/fast-diff": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz",
|
||||
"integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/fast-glob": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
|
||||
@ -3717,6 +3780,33 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz",
|
||||
"integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier-linter-helpers": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
|
||||
"integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fast-diff": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
@ -4434,6 +4524,22 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/synckit": {
|
||||
"version": "0.8.8",
|
||||
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz",
|
||||
"integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@pkgr/core": "^0.1.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/unts"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "3.4.4",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.4.tgz",
|
||||
|
||||
@ -24,7 +24,10 @@
|
||||
"@types/react-dom": "^18",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.2.3",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"postcss": "^8",
|
||||
"prettier": "^3.3.2",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5"
|
||||
}
|
||||
|
||||
@ -2,29 +2,29 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||
|
||||
interface LoaderState {
|
||||
activeRequests: number
|
||||
activeRequests: number
|
||||
}
|
||||
|
||||
const initialState: LoaderState = {
|
||||
activeRequests: 0,
|
||||
activeRequests: 0
|
||||
}
|
||||
|
||||
const loaderSlice = createSlice({
|
||||
name: 'loader',
|
||||
initialState,
|
||||
reducers: {
|
||||
increment: (state) => {
|
||||
state.activeRequests += 1
|
||||
},
|
||||
decrement: (state) => {
|
||||
if (state.activeRequests > 0) {
|
||||
state.activeRequests -= 1
|
||||
}
|
||||
},
|
||||
reset: (state) => {
|
||||
state.activeRequests = 0
|
||||
},
|
||||
name: 'loader',
|
||||
initialState,
|
||||
reducers: {
|
||||
increment: (state) => {
|
||||
state.activeRequests += 1
|
||||
},
|
||||
decrement: (state) => {
|
||||
if (state.activeRequests > 0) {
|
||||
state.activeRequests -= 1
|
||||
}
|
||||
},
|
||||
reset: (state) => {
|
||||
state.activeRequests = 0
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const { increment, decrement, reset } = loaderSlice.actions
|
||||
|
||||
@ -8,25 +8,28 @@ interface ToastState {
|
||||
|
||||
const initialState: ToastState = {
|
||||
message: '',
|
||||
type: 'info',
|
||||
type: 'info'
|
||||
}
|
||||
|
||||
const toastSlice = createSlice({
|
||||
name: 'toast',
|
||||
initialState,
|
||||
reducers: {
|
||||
showToast: (state, action: PayloadAction<{
|
||||
message: string
|
||||
type: 'success' | 'error' | 'info' | 'warning'
|
||||
}>) => {
|
||||
showToast: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
message: string
|
||||
type: 'success' | 'error' | 'info' | 'warning'
|
||||
}>
|
||||
) => {
|
||||
state.message = action.payload.message
|
||||
state.type = action.payload.type
|
||||
},
|
||||
clearToast: (state) => {
|
||||
state.message = ''
|
||||
state.type = 'info'
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const { showToast, clearToast } = toastSlice.actions
|
||||
|
||||
@ -3,11 +3,11 @@ import loaderReducer from '@/redux/slices//loaderSlice'
|
||||
import toastReducer from '@/redux/slices/toastSlice'
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
loader: loaderReducer,
|
||||
toast: toastReducer,
|
||||
},
|
||||
reducer: {
|
||||
loader: loaderReducer,
|
||||
toast: toastReducer
|
||||
}
|
||||
})
|
||||
|
||||
export type RootState = ReturnType<typeof store.getState>
|
||||
export type AppDispatch = typeof store.dispatch
|
||||
export type AppDispatch = typeof store.dispatch
|
||||
|
||||
@ -1,188 +1,235 @@
|
||||
import { store } from '@/redux/store';
|
||||
import { increment, decrement } from '@/redux/slices/loaderSlice';
|
||||
import { showToast } from '@/redux/slices/toastSlice';
|
||||
import { store } from '@/redux/store'
|
||||
import { increment, decrement } from '@/redux/slices/loaderSlice'
|
||||
import { showToast } from '@/redux/slices/toastSlice'
|
||||
|
||||
interface RequestInterceptor {
|
||||
(req: XMLHttpRequest): void;
|
||||
(req: XMLHttpRequest): void
|
||||
}
|
||||
|
||||
interface ResponseInterceptor<T> {
|
||||
(response: T | null, error: ProblemDetails | null): T | void;
|
||||
(response: T | null, error: ProblemDetails | null): T | void
|
||||
}
|
||||
|
||||
interface ProblemDetails {
|
||||
Title: string;
|
||||
Detail: string | null;
|
||||
Status: number;
|
||||
Title: string
|
||||
Detail: string | null
|
||||
Status: number
|
||||
}
|
||||
|
||||
interface HttpServiceCallbacks {
|
||||
onIncrement?: () => void;
|
||||
onDecrement?: () => void;
|
||||
onShowToast?: (message: string, type: 'info' | 'error') => void;
|
||||
onIncrement?: () => void
|
||||
onDecrement?: () => void
|
||||
onShowToast?: (message: string, type: 'info' | 'error') => void
|
||||
}
|
||||
|
||||
class HttpService {
|
||||
private requestInterceptors: RequestInterceptor[] = [];
|
||||
private responseInterceptors: Array<ResponseInterceptor<any>> = [];
|
||||
private callbacks: HttpServiceCallbacks;
|
||||
private requestInterceptors: RequestInterceptor[] = []
|
||||
private responseInterceptors: Array<ResponseInterceptor<any>> = []
|
||||
private callbacks: HttpServiceCallbacks
|
||||
|
||||
constructor(callbacks: HttpServiceCallbacks) {
|
||||
this.callbacks = callbacks;
|
||||
constructor(callbacks: HttpServiceCallbacks) {
|
||||
this.callbacks = callbacks
|
||||
}
|
||||
|
||||
private invokeIncrement(): void {
|
||||
this.callbacks.onIncrement?.()
|
||||
}
|
||||
|
||||
private invokeDecrement(): void {
|
||||
this.callbacks.onDecrement?.()
|
||||
}
|
||||
|
||||
private invokeShowToast(message: string, type: 'info' | 'error'): void {
|
||||
this.callbacks.onShowToast?.(message, type)
|
||||
}
|
||||
|
||||
private async request<TResponse>(
|
||||
method: string,
|
||||
url: string,
|
||||
data?: any
|
||||
): Promise<TResponse | null> {
|
||||
const xhr = new XMLHttpRequest()
|
||||
xhr.open(method, url)
|
||||
|
||||
this.handleRequestInterceptors(xhr)
|
||||
|
||||
if (data && typeof data !== 'string') {
|
||||
xhr.setRequestHeader('Content-Type', 'application/json')
|
||||
}
|
||||
|
||||
private invokeIncrement(): void {
|
||||
this.callbacks.onIncrement?.();
|
||||
this.invokeIncrement()
|
||||
|
||||
return new Promise<TResponse | null>((resolve) => {
|
||||
xhr.onload = () => this.handleLoad<TResponse>(xhr, resolve)
|
||||
xhr.onerror = () => this.handleNetworkError(resolve)
|
||||
xhr.send(data ? JSON.stringify(data) : null)
|
||||
})
|
||||
}
|
||||
|
||||
private handleRequestInterceptors(xhr: XMLHttpRequest): void {
|
||||
this.requestInterceptors.forEach((interceptor) => {
|
||||
try {
|
||||
interceptor(xhr)
|
||||
} catch (error) {
|
||||
const problemDetails = this.createProblemDetails(
|
||||
'Request Interceptor Error',
|
||||
error,
|
||||
0
|
||||
)
|
||||
this.showProblemDetails(problemDetails)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private handleResponseInterceptors<TResponse>(
|
||||
response: TResponse | null,
|
||||
error: ProblemDetails | null
|
||||
): TResponse | null {
|
||||
this.responseInterceptors.forEach((interceptor) => {
|
||||
try {
|
||||
interceptor(response, error)
|
||||
} catch (e) {
|
||||
const problemDetails = this.createProblemDetails(
|
||||
'Response Interceptor Error',
|
||||
e,
|
||||
0
|
||||
)
|
||||
this.showProblemDetails(problemDetails)
|
||||
}
|
||||
})
|
||||
return response
|
||||
}
|
||||
|
||||
private handleLoad<TResponse>(
|
||||
xhr: XMLHttpRequest,
|
||||
resolve: (value: TResponse | null) => void
|
||||
): void {
|
||||
this.invokeDecrement()
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
this.handleSuccessfulResponse<TResponse>(xhr, resolve)
|
||||
} else {
|
||||
this.handleErrorResponse(xhr, resolve)
|
||||
}
|
||||
}
|
||||
|
||||
private invokeDecrement(): void {
|
||||
this.callbacks.onDecrement?.();
|
||||
private handleSuccessfulResponse<TResponse>(
|
||||
xhr: XMLHttpRequest,
|
||||
resolve: (value: TResponse | null) => void
|
||||
): void {
|
||||
try {
|
||||
if (xhr.response) {
|
||||
const response = JSON.parse(xhr.response)
|
||||
resolve(this.handleResponseInterceptors(response, null) as TResponse)
|
||||
} else {
|
||||
resolve(null)
|
||||
}
|
||||
} catch (error) {
|
||||
const problemDetails = this.createProblemDetails(
|
||||
'Response Parse Error',
|
||||
error,
|
||||
xhr.status
|
||||
)
|
||||
this.showProblemDetails(problemDetails)
|
||||
resolve(null)
|
||||
}
|
||||
}
|
||||
|
||||
private invokeShowToast(message: string, type: 'info' | 'error'): void {
|
||||
this.callbacks.onShowToast?.(message, type);
|
||||
private handleErrorResponse<TResponse>(
|
||||
xhr: XMLHttpRequest,
|
||||
resolve: (value: TResponse | null) => void
|
||||
): void {
|
||||
const problemDetails = this.createProblemDetails(
|
||||
xhr.statusText,
|
||||
xhr.responseText,
|
||||
xhr.status
|
||||
)
|
||||
this.showProblemDetails(problemDetails)
|
||||
resolve(this.handleResponseInterceptors(null, problemDetails))
|
||||
}
|
||||
|
||||
private handleNetworkError<TResponse>(
|
||||
resolve: (value: TResponse | null) => void
|
||||
): void {
|
||||
const problemDetails = this.createProblemDetails('Network Error', null, 0)
|
||||
this.showProblemDetails(problemDetails)
|
||||
resolve(this.handleResponseInterceptors(null, problemDetails))
|
||||
}
|
||||
|
||||
private createProblemDetails(
|
||||
title: string,
|
||||
detail: any,
|
||||
status: number
|
||||
): ProblemDetails {
|
||||
return {
|
||||
Title: title,
|
||||
Detail: detail instanceof Error ? detail.message : String(detail),
|
||||
Status: status
|
||||
}
|
||||
}
|
||||
|
||||
private async request<TResponse>(method: string, url: string, data?: any): Promise<TResponse | null> {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open(method, url);
|
||||
|
||||
this.handleRequestInterceptors(xhr);
|
||||
|
||||
if (data && typeof data !== 'string') {
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
}
|
||||
|
||||
this.invokeIncrement();
|
||||
|
||||
return new Promise<TResponse | null>((resolve) => {
|
||||
xhr.onload = () => this.handleLoad<TResponse>(xhr, resolve);
|
||||
xhr.onerror = () => this.handleNetworkError(resolve);
|
||||
xhr.send(data ? JSON.stringify(data) : null);
|
||||
});
|
||||
private showProblemDetails(problemDetails: ProblemDetails): void {
|
||||
if (problemDetails.Detail) {
|
||||
const errorMessages = problemDetails.Detail.split(',')
|
||||
errorMessages.forEach((message) => {
|
||||
this.invokeShowToast(message.trim(), 'error')
|
||||
})
|
||||
} else {
|
||||
this.invokeShowToast('Unknown error', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
private handleRequestInterceptors(xhr: XMLHttpRequest): void {
|
||||
this.requestInterceptors.forEach(interceptor => {
|
||||
try {
|
||||
interceptor(xhr);
|
||||
} catch (error) {
|
||||
const problemDetails = this.createProblemDetails('Request Interceptor Error', error, 0);
|
||||
this.showProblemDetails(problemDetails);
|
||||
}
|
||||
});
|
||||
}
|
||||
public async get<TResponse>(url: string): Promise<TResponse | null> {
|
||||
return await this.request<TResponse>('GET', url)
|
||||
}
|
||||
|
||||
private handleResponseInterceptors<TResponse>(response: TResponse | null, error: ProblemDetails | null): TResponse | null {
|
||||
this.responseInterceptors.forEach(interceptor => {
|
||||
try {
|
||||
interceptor(response, error);
|
||||
} catch (e) {
|
||||
const problemDetails = this.createProblemDetails('Response Interceptor Error', e, 0);
|
||||
this.showProblemDetails(problemDetails);
|
||||
}
|
||||
});
|
||||
return response;
|
||||
}
|
||||
public async post<TRequest, TResponse>(
|
||||
url: string,
|
||||
data: TRequest
|
||||
): Promise<TResponse | null> {
|
||||
return await this.request<TResponse>('POST', url, data)
|
||||
}
|
||||
|
||||
private handleLoad<TResponse>(xhr: XMLHttpRequest, resolve: (value: TResponse | null) => void): void {
|
||||
this.invokeDecrement();
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
this.handleSuccessfulResponse<TResponse>(xhr, resolve);
|
||||
} else {
|
||||
this.handleErrorResponse(xhr, resolve);
|
||||
}
|
||||
}
|
||||
public async put<TRequest, TResponse>(
|
||||
url: string,
|
||||
data: TRequest
|
||||
): Promise<TResponse | null> {
|
||||
return await this.request<TResponse>('PUT', url, data)
|
||||
}
|
||||
|
||||
private handleSuccessfulResponse<TResponse>(xhr: XMLHttpRequest, resolve: (value: TResponse | null) => void): void {
|
||||
try {
|
||||
if (xhr.response) {
|
||||
const response = JSON.parse(xhr.response);
|
||||
resolve(this.handleResponseInterceptors(response, null) as TResponse);
|
||||
} else {
|
||||
resolve(null);
|
||||
}
|
||||
} catch (error) {
|
||||
const problemDetails = this.createProblemDetails('Response Parse Error', error, xhr.status);
|
||||
this.showProblemDetails(problemDetails);
|
||||
resolve(null);
|
||||
}
|
||||
}
|
||||
public async delete<TResponse>(url: string): Promise<TResponse | null> {
|
||||
return await this.request<TResponse>('DELETE', url)
|
||||
}
|
||||
|
||||
private handleErrorResponse<TResponse>(xhr: XMLHttpRequest, resolve: (value: TResponse | null) => void): void {
|
||||
const problemDetails = this.createProblemDetails(xhr.statusText, xhr.responseText, xhr.status);
|
||||
this.showProblemDetails(problemDetails);
|
||||
resolve(this.handleResponseInterceptors(null, problemDetails));
|
||||
}
|
||||
public addRequestInterceptor(interceptor: RequestInterceptor): void {
|
||||
this.requestInterceptors.push(interceptor)
|
||||
}
|
||||
|
||||
private handleNetworkError<TResponse>(resolve: (value: TResponse | null) => void): void {
|
||||
const problemDetails = this.createProblemDetails('Network Error', null, 0);
|
||||
this.showProblemDetails(problemDetails);
|
||||
resolve(this.handleResponseInterceptors(null, problemDetails));
|
||||
}
|
||||
|
||||
private createProblemDetails(title: string, detail: any, status: number): ProblemDetails {
|
||||
return {
|
||||
Title: title,
|
||||
Detail: detail instanceof Error ? detail.message : String(detail),
|
||||
Status: status
|
||||
};
|
||||
}
|
||||
|
||||
private showProblemDetails(problemDetails: ProblemDetails): void {
|
||||
if (problemDetails.Detail) {
|
||||
const errorMessages = problemDetails.Detail.split(',');
|
||||
errorMessages.forEach(message => {
|
||||
this.invokeShowToast(message.trim(), 'error');
|
||||
});
|
||||
} else {
|
||||
this.invokeShowToast('Unknown error', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
public async get<TResponse>(url: string): Promise<TResponse | null> {
|
||||
return await this.request<TResponse>('GET', url);
|
||||
}
|
||||
|
||||
public async post<TRequest, TResponse>(url: string, data: TRequest): Promise<TResponse | null> {
|
||||
return await this.request<TResponse>('POST', url, data);
|
||||
}
|
||||
|
||||
public async put<TRequest, TResponse>(url: string, data: TRequest): Promise<TResponse | null> {
|
||||
return await this.request<TResponse>('PUT', url, data);
|
||||
}
|
||||
|
||||
public async delete<TResponse>(url: string): Promise<TResponse | null> {
|
||||
return await this.request<TResponse>('DELETE', url);
|
||||
}
|
||||
|
||||
public addRequestInterceptor(interceptor: RequestInterceptor): void {
|
||||
this.requestInterceptors.push(interceptor);
|
||||
}
|
||||
|
||||
public addResponseInterceptor<TResponse>(interceptor: ResponseInterceptor<TResponse>): void {
|
||||
this.responseInterceptors.push(interceptor);
|
||||
}
|
||||
public addResponseInterceptor<TResponse>(
|
||||
interceptor: ResponseInterceptor<TResponse>
|
||||
): void {
|
||||
this.responseInterceptors.push(interceptor)
|
||||
}
|
||||
}
|
||||
|
||||
// Instance of HttpService
|
||||
const httpService = new HttpService({
|
||||
onIncrement: () => store.dispatch(increment()),
|
||||
onDecrement: () => store.dispatch(decrement()),
|
||||
onShowToast: (message: string, type: 'info' | 'error') => store.dispatch(showToast({ message, type })),
|
||||
});
|
||||
onIncrement: () => store.dispatch(increment()),
|
||||
onDecrement: () => store.dispatch(decrement()),
|
||||
onShowToast: (message: string, type: 'info' | 'error') =>
|
||||
store.dispatch(showToast({ message, type }))
|
||||
})
|
||||
|
||||
// Add loader state handling via interceptors
|
||||
httpService.addRequestInterceptor((xhr) => {
|
||||
// Additional request logic can be added here
|
||||
});
|
||||
// Additional request logic can be added here
|
||||
})
|
||||
|
||||
httpService.addResponseInterceptor((response, error) => {
|
||||
// Additional response logic can be added here
|
||||
return response;
|
||||
});
|
||||
// Additional response logic can be added here
|
||||
return response
|
||||
})
|
||||
|
||||
export { httpService };
|
||||
export { httpService }
|
||||
|
||||
// Example usage of the httpService
|
||||
// async function fetchData() {
|
||||
@ -193,4 +240,3 @@ export { httpService };
|
||||
// console.error('Failed to fetch data');
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user