(feature): formating rules

This commit is contained in:
Maksym Sadovnychyy 2024-06-20 12:22:40 +02:00
parent e321a3237f
commit 7d7ebd298a
33 changed files with 1236 additions and 842 deletions

View File

@ -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"
}
}

View 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

View File

@ -0,0 +1,7 @@
{
"singleQuote": true,
"semi": false,
"tabWidth": 2,
"endOfLine": "lf",
"trailingComma": "none"
}

23
src/ClientApp/.vscode/settings.json vendored Normal file
View 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"
]
}

View File

@ -1,12 +1,9 @@
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}`,
@ -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 }

View File

@ -1,5 +1,6 @@
const ContactPage = () => {
return (<>
return (
<>
<h1 className="text-2xl font-bold">Contact Us</h1>
<p>This is the contact page content.</p>
</>

View 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 }

View File

@ -0,0 +1,3 @@
import { deepCopy } from './deepCopy'
export { deepCopy }

View File

@ -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} />

View File

@ -1,26 +1,18 @@
"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[]>([])
@ -31,46 +23,48 @@ export default function Page() {
error: contactError,
handleChange: handleContactChange
} = useValidation({
initialValue:"",
initialValue: '',
validateFn: isValidEmail,
errorMessage: "Invalid email format."
errorMessage: 'Invalid email format.'
})
const {
value: newHostname,
error: hostnameError,
handleChange: handleHostnameChange
} = useValidation({
initialValue: "",
initialValue: '',
validateFn: isValidHostname,
errorMessage: "Invalid hostname format."})
errorMessage: 'Invalid hostname format.'
})
const init = useRef(false)
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 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 => ({
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
setInitialAccounts(deepCopy(newAccounts)) // Clone initial state
}
fetchAccounts()
@ -78,131 +72,197 @@ export default function Page() {
}, [])
const toggleEditMode = (accountId: string) => {
setAccounts(accounts.map(account =>
account.accountId === accountId ? { ...account, isEditMode: !account.isEditMode } : account
))
setAccounts(
accounts.map((account) =>
account.accountId === accountId
? { ...account, isEditMode: !account.isEditMode }
: account
)
)
}
const deleteAccount = (accountId: string) => {
setAccounts(accounts.filter(account => account.accountId !== accountId))
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)
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))
httpService.delete(
GetApiRoute(ApiRoutes.CACHE_ACCOUNT_CONTACT, accountId, contact)
)
setAccounts(accounts.map(account =>
setAccounts(
accounts.map((account) =>
account.accountId === accountId
? { ...account, contacts: account.contacts.filter(c => c !== contact) }
? {
...account,
contacts: account.contacts.filter((c) => c !== contact)
}
: account
))
)
)
}
const addContact = (accountId: string) => {
if (newContact.trim() === "" || contactError) {
if (newContact.trim() === '' || contactError) {
return
}
if (accounts.find(account => account.accountId === accountId)?.contacts.includes(newContact.trim()))
if (
accounts
.find((account) => account.accountId === accountId)
?.contacts.includes(newContact.trim())
)
return
setAccounts(accounts.map(account =>
setAccounts(
accounts.map((account) =>
account.accountId === accountId
? { ...account, contacts: [...account.contacts, newContact.trim()] }
: account
))
handleContactChange("")
)
)
handleContactChange('')
}
const deleteHostname = (accountId: string, hostname: string) => {
const account = accounts.find(account => account.accountId === accountId)
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 =>
setAccounts(
accounts.map((account) =>
account.accountId === accountId
? { ...account, hostnames: account.hostnames.filter(h => h.hostname !== hostname) }
? {
...account,
hostnames: account.hostnames.filter(
(h) => h.hostname !== hostname
)
}
: account
))
)
)
}
const addHostname = (accountId: string) => {
if (newHostname.trim() === "" || hostnameError) {
if (newHostname.trim() === '' || hostnameError) {
return
}
if (accounts.find(account => account.accountId === accountId)?.hostnames.some(h => h.hostname === newHostname.trim()))
if (
accounts
.find((account) => account.accountId === accountId)
?.hostnames.some((h) => h.hostname === newHostname.trim())
)
return
setAccounts(accounts.map(account =>
setAccounts(
accounts.map((account) =>
account.accountId === accountId
? { ...account, hostnames: [...account.hostnames, { hostname: newHostname.trim(), expires: new Date(), isUpcomingExpire: false }] }
? {
...account,
hostnames: [
...account.hostnames,
{
hostname: newHostname.trim(),
expires: new Date(),
isUpcomingExpire: false
}
]
}
: account
))
handleHostnameChange("")
)
)
handleHostnameChange('')
}
const handleSubmit = async (e: FormEvent<HTMLFormElement>, accountId: string) => {
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)
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))
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))
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)
console.log('Added contacts:', contactChanges.added)
}
if (contactChanges.removed.length > 0) {
// TODO: DELETE removed contacts
console.log("Removed contacts:", contactChanges.removed)
console.log('Removed contacts:', contactChanges.removed)
}
// Handle hostname changes
if (hostnameChanges.added.length > 0) {
// TODO: POST new hostnames
console.log("Added hostnames:", hostnameChanges.added)
console.log('Added hostnames:', hostnameChanges.added)
}
if (hostnameChanges.removed.length > 0) {
// TODO: DELETE removed hostnames
console.log("Removed hostnames:", hostnameChanges.removed)
console.log('Removed hostnames:', hostnameChanges.removed)
}
// Save current state as initial state
setInitialAccounts(JSON.parse(JSON.stringify(accounts)))
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">
<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"}
<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 ? (
@ -213,16 +273,22 @@ export default function Page() {
<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">
{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">
<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
@ -236,7 +302,10 @@ export default function Page() {
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">
<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>
@ -244,21 +313,32 @@ export default function Page() {
<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">
{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'}
{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">
<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
@ -272,16 +352,25 @@ export default function Page() {
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">
<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">
<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">
<CustomButton
type="submit"
className="bg-green-500 text-white px-3 py-1 rounded"
>
Submit
</CustomButton>
</div>
@ -294,35 +383,34 @@ export default function Page() {
<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 => (
{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 => (
{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
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>
)
}

View File

@ -1,33 +1,26 @@
"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
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,
@ -35,9 +28,9 @@ const RegisterPage = () => {
handleChange: handleContactChange,
reset: resetContact
} = useValidation({
initialValue: "",
initialValue: '',
validateFn: isValidContact,
errorMessage: "Invalid contact. Must be a valid email or phone number."
errorMessage: 'Invalid contact. Must be a valid email or phone number.'
})
const {
@ -46,9 +39,9 @@ const RegisterPage = () => {
handleChange: handleHostnameChange,
reset: resetHostname
} = useValidation({
initialValue: "",
initialValue: '',
validateFn: isValidHostname,
errorMessage: "Invalid hostname format."
errorMessage: 'Invalid hostname format.'
})
const init = useRef(false)
@ -56,84 +49,90 @@ const RegisterPage = () => {
useEffect(() => {
if (init.current) return
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
})
})
setAccounts(newAccounts)
setInitialAccounts(JSON.parse(JSON.stringify(newAccounts))) // Clone initial state
}
fetchAccounts()
init.current = true
}, [])
const handleDescription = (description: string) => {}
const handleAddContact = () => {
if (newContact.trim() !== "" && !contactError) {
setContacts([...contacts, newContact.trim()])
resetContact()
if (newContact !== '' || contactError) return
setAccount((prev) => {
const newAccount: CacheAccount =
prev !== null
? deepCopy(prev)
: {
contacts: [],
hostnames: []
}
newAccount.contacts.push(newContact)
return newAccount
})
resetContact()
}
const handleAddHostname = () => {
if (newHostname.trim() !== "" && !hostnameError) {
setHostnames([...hostnames, newHostname.trim()])
resetHostname()
if (newHostname !== '' || hostnameError) return
setAccount((prev) => {
const newAccount: CacheAccount =
prev !== null
? deepCopy(prev)
: {
contacts: [],
hostnames: []
}
newAccount.hostnames.push(newHostname)
return newAccount
})
resetHostname()
}
const handleDeleteContact = (contact: string) => {
setContacts(contacts.filter(c => c !== contact))
setAccount((prev) => {
if (prev === null) return null
const newAccount = deepCopy(prev)
newAccount.contacts = newAccount.contacts.filter((c) => c !== contact)
return newAccount
})
}
const handleDeleteHostname = (hostname: string) => {
setHostnames(hostnames.filter(h => h !== hostname))
setAccount((prev) => {
if (prev === null) return null
const newAccount = deepCopy(prev)
newAccount.hostnames = newAccount.hostnames.filter((h) => h !== hostname)
return newAccount
})
}
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([])
console.log(account)
}
return (
<div className="container mx-auto p-4">
<h1 className="text-4xl font-bold text-center mb-8">Register LetsEncrypt Account</h1>
<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)}
value={account?.description ?? ''}
onChange={handleDescription}
placeholder="Account Description"
title="Description"
inputClassName="border p-2 rounded w-full"
@ -143,10 +142,17 @@ const RegisterPage = () => {
<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">
{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">
<button
type="button"
onClick={() => handleDeleteContact(contact)}
className="bg-red-500 text-white px-2 py-1 rounded ml-4"
>
<FaTrash />
</button>
</li>
@ -164,7 +170,11 @@ const RegisterPage = () => {
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">
<button
type="button"
onClick={handleAddContact}
className="bg-green-500 text-white p-2 rounded ml-2 h-10 flex items-center"
>
<FaPlus />
</button>
</div>
@ -172,10 +182,17 @@ const RegisterPage = () => {
<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">
{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">
<button
type="button"
onClick={() => handleDeleteHostname(hostname)}
className="bg-red-500 text-white px-2 py-1 rounded ml-4"
>
<FaTrash />
</button>
</li>
@ -193,12 +210,19 @@ const RegisterPage = () => {
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">
<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">
<CustomButton
type="submit"
className="bg-green-500 text-white px-3 py-1 rounded"
>
Create Account
</CustomButton>
</form>

View File

@ -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 }

View File

@ -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 }

View File

@ -29,6 +29,4 @@ const OffCanvas: FC<OffCanvasProps> = ({ isOpen, onClose }) => {
)
}
export {
OffCanvas
}
export { OffCanvas }

View File

@ -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 }

View File

@ -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'
@ -54,6 +54,4 @@ const TopMenu: FC<TopMenuProps> = ({ onToggleOffCanvas }) => {
)
}
export {
TopMenu
}
export { TopMenu }

View File

@ -1,4 +1,4 @@
"use client"
'use client'
import React, { FC } from 'react'
interface CustomButtonProps {
@ -6,12 +6,17 @@ interface CustomButtonProps {
className?: string
children: React.ReactNode
disabled?: boolean
type?: "button" | "submit" | "reset"
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

View File

@ -1,5 +1,5 @@
// components/CustomInput.tsx
"use client"
'use client'
import React from 'react'
interface CustomInputProps {
@ -25,9 +25,8 @@ const CustomInput: React.FC<CustomInputProps> = ({
errorClassName = '',
className = ''
}) => {
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
onChange?.(e.target.value);
onChange?.(e.target.value)
}
return (

View File

@ -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 }

View File

@ -0,0 +1,9 @@
import { CacheAccountHostname } from './CacheAccountHostname'
export interface CacheAccount {
accountId: string
description?: string
contacts: string[]
hostnames: CacheAccountHostname[]
isEditMode: boolean
}

View File

@ -0,0 +1,5 @@
export interface CacheAccountHostname {
hostname: string
expires: Date
isUpcomingExpire: boolean
}

View File

@ -1,4 +1,4 @@
import { useState, useEffect } from "react"
import { useState, useEffect } from 'react'
// Helper functions for validation
const isValidEmail = (email: string) => {
@ -28,20 +28,23 @@ interface UseValidationProps {
}
// Custom hook for input validation
const useValidation = ({ initialValue, validateFn, errorMessage }: UseValidationProps) => {
const useValidation = ({
initialValue,
validateFn,
errorMessage
}: UseValidationProps) => {
const [value, setValue] = useState(initialValue)
const [error, setError] = useState("")
const [error, setError] = useState('')
const handleChange = (newValue: string) => {
console.log(newValue)
setValue(newValue)
if (newValue.trim() === "") {
setError("This field cannot be empty.")
if (newValue.trim() === '') {
setError('This field cannot be empty.')
} else if (!validateFn(newValue.trim())) {
setError(errorMessage)
} else {
setError("")
setError('')
}
}
@ -49,7 +52,13 @@ const useValidation = ({ initialValue, validateFn, errorMessage }: UseValidation
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
}

View File

@ -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[]
}

View File

@ -1,4 +1,4 @@
import { HostnameResponse } from "./HostnameResponse";
import { HostnameResponse } from './HostnameResponse'
export interface GetHostnamesResponse {
hostnames: HostnameResponse[]

View File

@ -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",

View File

@ -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"
}

View File

@ -6,7 +6,7 @@ interface LoaderState {
}
const initialState: LoaderState = {
activeRequests: 0,
activeRequests: 0
}
const loaderSlice = createSlice({
@ -23,8 +23,8 @@ const loaderSlice = createSlice({
},
reset: (state) => {
state.activeRequests = 0
},
},
}
}
})
export const { increment, decrement, reset } = loaderSlice.actions

View File

@ -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<{
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

View File

@ -5,8 +5,8 @@ import toastReducer from '@/redux/slices/toastSlice'
export const store = configureStore({
reducer: {
loader: loaderReducer,
toast: toastReducer,
},
toast: toastReducer
}
})
export type RootState = ReturnType<typeof store.getState>

View File

@ -1,167 +1,213 @@
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;
this.callbacks = callbacks
}
private invokeIncrement(): void {
this.callbacks.onIncrement?.();
this.callbacks.onIncrement?.()
}
private invokeDecrement(): void {
this.callbacks.onDecrement?.();
this.callbacks.onDecrement?.()
}
private invokeShowToast(message: string, type: 'info' | 'error'): void {
this.callbacks.onShowToast?.(message, type);
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);
private async request<TResponse>(
method: string,
url: string,
data?: any
): Promise<TResponse | null> {
const xhr = new XMLHttpRequest()
xhr.open(method, url)
this.handleRequestInterceptors(xhr);
this.handleRequestInterceptors(xhr)
if (data && typeof data !== 'string') {
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('Content-Type', 'application/json')
}
this.invokeIncrement();
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);
});
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 => {
this.requestInterceptors.forEach((interceptor) => {
try {
interceptor(xhr);
interceptor(xhr)
} catch (error) {
const problemDetails = this.createProblemDetails('Request Interceptor Error', error, 0);
this.showProblemDetails(problemDetails);
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 => {
private handleResponseInterceptors<TResponse>(
response: TResponse | null,
error: ProblemDetails | null
): TResponse | null {
this.responseInterceptors.forEach((interceptor) => {
try {
interceptor(response, error);
interceptor(response, error)
} catch (e) {
const problemDetails = this.createProblemDetails('Response Interceptor Error', e, 0);
this.showProblemDetails(problemDetails);
const problemDetails = this.createProblemDetails(
'Response Interceptor Error',
e,
0
)
this.showProblemDetails(problemDetails)
}
});
return response;
})
return response
}
private handleLoad<TResponse>(xhr: XMLHttpRequest, resolve: (value: TResponse | null) => void): void {
this.invokeDecrement();
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);
this.handleSuccessfulResponse<TResponse>(xhr, resolve)
} else {
this.handleErrorResponse(xhr, resolve);
this.handleErrorResponse(xhr, resolve)
}
}
private handleSuccessfulResponse<TResponse>(xhr: XMLHttpRequest, resolve: (value: TResponse | null) => void): void {
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);
const response = JSON.parse(xhr.response)
resolve(this.handleResponseInterceptors(response, null) as TResponse)
} else {
resolve(null);
resolve(null)
}
} catch (error) {
const problemDetails = this.createProblemDetails('Response Parse Error', error, xhr.status);
this.showProblemDetails(problemDetails);
resolve(null);
const problemDetails = this.createProblemDetails(
'Response Parse Error',
error,
xhr.status
)
this.showProblemDetails(problemDetails)
resolve(null)
}
}
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 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 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 {
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');
});
const errorMessages = problemDetails.Detail.split(',')
errorMessages.forEach((message) => {
this.invokeShowToast(message.trim(), 'error')
})
} else {
this.invokeShowToast('Unknown error', 'error');
this.invokeShowToast('Unknown error', 'error')
}
}
public async get<TResponse>(url: string): Promise<TResponse | null> {
return await this.request<TResponse>('GET', url);
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 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 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);
return await this.request<TResponse>('DELETE', url)
}
public addRequestInterceptor(interceptor: RequestInterceptor): void {
this.requestInterceptors.push(interceptor);
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)
}
}
@ -169,20 +215,21 @@ class HttpService {
const httpService = new HttpService({
onIncrement: () => store.dispatch(increment()),
onDecrement: () => store.dispatch(decrement()),
onShowToast: (message: string, type: 'info' | 'error') => store.dispatch(showToast({ message, type })),
});
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
});
})
httpService.addResponseInterceptor((response, error) => {
// Additional response logic can be added here
return response;
});
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');
// }
// }