Refactor frontend structure and update dependencies
Publish Docker image / build-and-push (push) Successful in 2m6s
Publish Docker image / build-and-push (push) Successful in 2m6s
- Changed the main entry point from main.jsx to main.tsx for TypeScript support. - Removed Tabler JS import and integrated Tailwind CSS for styling. - Updated package.json and package-lock.json to include new dependencies such as @fontsource-variable/geist and tailwindcss, while removing unused ones. - Enhanced Vite configuration with path aliasing for improved import management. - Deleted unused App.css and App.jsx files to streamline the project structure. Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
type NotifyCallbacks = {
|
||||
onSuccess: () => void
|
||||
onError: () => void
|
||||
}
|
||||
|
||||
function fallbackCopy(text: string, callbacks: NotifyCallbacks) {
|
||||
const textArea = document.createElement("textarea")
|
||||
textArea.value = text
|
||||
document.body.appendChild(textArea)
|
||||
textArea.focus()
|
||||
textArea.select()
|
||||
|
||||
try {
|
||||
document.execCommand("copy")
|
||||
callbacks.onSuccess()
|
||||
} catch {
|
||||
callbacks.onError()
|
||||
} finally {
|
||||
document.body.removeChild(textArea)
|
||||
}
|
||||
}
|
||||
|
||||
export async function copyTextToClipboard(text: string, callbacks: NotifyCallbacks) {
|
||||
if (navigator.clipboard) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
callbacks.onSuccess()
|
||||
return
|
||||
} catch {
|
||||
// fallback below
|
||||
}
|
||||
}
|
||||
|
||||
fallbackCopy(text, callbacks)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export function countryToFlag(isoCode?: string | null): string {
|
||||
if (!isoCode) return ""
|
||||
|
||||
return isoCode
|
||||
.toUpperCase()
|
||||
.replace(/./g, (char) => String.fromCodePoint(127397 + char.charCodeAt(0)))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
type UrlSettings = {
|
||||
baseUrl: string
|
||||
[key: string]: string
|
||||
}
|
||||
|
||||
type ServerLike = {
|
||||
id?: string
|
||||
ip?: string
|
||||
extIp?: string
|
||||
internalIp?: string
|
||||
dns?: string
|
||||
country?: string
|
||||
provider?: string
|
||||
type?: string
|
||||
tunnel?: string
|
||||
}
|
||||
|
||||
export function generateServerUrl(urlSettings: UrlSettings, server: ServerLike): string {
|
||||
const params = new URLSearchParams()
|
||||
|
||||
Object.entries(urlSettings).forEach(([key, value]) => {
|
||||
if (key !== "baseUrl" && value) {
|
||||
params.append(key, value)
|
||||
}
|
||||
})
|
||||
|
||||
if (server.ip) {
|
||||
params.append("server", server.ip)
|
||||
}
|
||||
|
||||
return `${urlSettings.baseUrl}?${params.toString()}`
|
||||
}
|
||||
|
||||
export function buildServersCsv(servers: ServerLike[], urlSettings: UrlSettings): string {
|
||||
return [
|
||||
["ID", "IP", "Ext IP", "Internal IP", "DNS", "Country", "Provider", "Type", "Tunnel", "Generated URL"],
|
||||
...servers.map((server) => [
|
||||
server.id || "",
|
||||
server.ip || "",
|
||||
server.extIp || "",
|
||||
server.internalIp || "",
|
||||
server.dns || "",
|
||||
server.country || "",
|
||||
server.provider || "",
|
||||
server.type || "",
|
||||
server.tunnel || "",
|
||||
generateServerUrl(urlSettings, server),
|
||||
]),
|
||||
]
|
||||
.map((row) => row.map((cell) => `"${cell}"`).join(","))
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
export type ServerRecord = {
|
||||
id?: string
|
||||
ip?: string
|
||||
dns?: string
|
||||
country?: string
|
||||
provider?: string
|
||||
type?: string
|
||||
tunnel?: string
|
||||
groupKey?: string | null
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export type ServerFilters = {
|
||||
country: string
|
||||
provider: string
|
||||
type: string
|
||||
tunnel: string
|
||||
}
|
||||
|
||||
export type ServerDisplayItem =
|
||||
| { type: "server"; server: ServerRecord }
|
||||
| { type: "group"; groupKey: string; servers: ServerRecord[] }
|
||||
|
||||
function normalizeText(value: unknown): string {
|
||||
return String(value ?? "").toLowerCase()
|
||||
}
|
||||
|
||||
function compareValues(a: unknown, b: unknown): number {
|
||||
const aText = normalizeText(a)
|
||||
const bText = normalizeText(b)
|
||||
|
||||
if (aText < bText) return -1
|
||||
if (aText > bText) return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
export function filterAndSortServers(
|
||||
servers: ServerRecord[],
|
||||
filters: ServerFilters,
|
||||
searchTerm: string,
|
||||
sortField: string,
|
||||
sortOrder: "asc" | "desc",
|
||||
): ServerRecord[] {
|
||||
let result = [...servers]
|
||||
|
||||
if (filters.country) {
|
||||
result = result.filter((server) => server.country === filters.country)
|
||||
}
|
||||
if (filters.provider) {
|
||||
result = result.filter((server) => server.provider === filters.provider)
|
||||
}
|
||||
if (filters.type) {
|
||||
result = result.filter((server) => server.type === filters.type)
|
||||
}
|
||||
if (filters.tunnel) {
|
||||
result = result.filter((server) => server.tunnel === filters.tunnel)
|
||||
}
|
||||
|
||||
if (searchTerm) {
|
||||
const term = searchTerm.toLowerCase()
|
||||
result = result.filter((server) => {
|
||||
return (
|
||||
normalizeText(server.ip).includes(term) ||
|
||||
normalizeText(server.dns).includes(term) ||
|
||||
normalizeText(server.country).includes(term) ||
|
||||
normalizeText(server.provider).includes(term) ||
|
||||
normalizeText(server.type).includes(term)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
result.sort((a, b) => {
|
||||
const comparison = compareValues(a[sortField], b[sortField])
|
||||
return sortOrder === "asc" ? comparison : comparison * -1
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export function buildDisplayItems(filteredServers: ServerRecord[]): ServerDisplayItem[] {
|
||||
const keyToServers: Record<string, ServerRecord[]> = {}
|
||||
|
||||
filteredServers.forEach((server) => {
|
||||
const key = (server.groupKey && String(server.groupKey).trim()) || null
|
||||
if (!key) return
|
||||
|
||||
if (!keyToServers[key]) keyToServers[key] = []
|
||||
keyToServers[key].push(server)
|
||||
})
|
||||
|
||||
const seenKeys = new Set<string>()
|
||||
const items: ServerDisplayItem[] = []
|
||||
|
||||
for (const server of filteredServers) {
|
||||
const key = (server.groupKey && String(server.groupKey).trim()) || null
|
||||
if (!key) {
|
||||
items.push({ type: "server", server })
|
||||
continue
|
||||
}
|
||||
|
||||
if (seenKeys.has(key)) continue
|
||||
seenKeys.add(key)
|
||||
|
||||
const groupServers = keyToServers[key] || []
|
||||
if (groupServers.length >= 2) {
|
||||
items.push({ type: "group", groupKey: key, servers: groupServers })
|
||||
} else if (groupServers.length === 1) {
|
||||
items.push({ type: "server", server: groupServers[0] })
|
||||
}
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import type { ServerDisplayItem, ServerRecord } from "@/features/servers/lib/server-list"
|
||||
|
||||
type PaginationInput = {
|
||||
viewMode: "cards" | "table"
|
||||
currentPage: number
|
||||
pageSize: number
|
||||
displayItems: ServerDisplayItem[]
|
||||
filteredServers: ServerRecord[]
|
||||
}
|
||||
|
||||
export type ServerPaginationResult = {
|
||||
totalPagesCards: number
|
||||
totalPagesTable: number
|
||||
totalPages: number
|
||||
paginatedDisplayItems: ServerDisplayItem[]
|
||||
paginatedServers: ServerRecord[]
|
||||
totalItemsCount: number
|
||||
}
|
||||
|
||||
export function getServerPagination({
|
||||
viewMode,
|
||||
currentPage,
|
||||
pageSize,
|
||||
displayItems,
|
||||
filteredServers,
|
||||
}: PaginationInput): ServerPaginationResult {
|
||||
const totalPagesCards = Math.ceil(displayItems.length / pageSize)
|
||||
const totalPagesTable = Math.ceil(filteredServers.length / pageSize)
|
||||
const totalPages = viewMode === "cards" ? totalPagesCards : totalPagesTable
|
||||
|
||||
const from = (currentPage - 1) * pageSize
|
||||
const to = currentPage * pageSize
|
||||
|
||||
const paginatedDisplayItems = displayItems.slice(from, to)
|
||||
const paginatedServers = filteredServers.slice(from, to)
|
||||
const totalItemsCount = viewMode === "cards" ? displayItems.length : filteredServers.length
|
||||
|
||||
return {
|
||||
totalPagesCards,
|
||||
totalPagesTable,
|
||||
totalPages,
|
||||
paginatedDisplayItems,
|
||||
paginatedServers,
|
||||
totalItemsCount,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
export const defaultServerUrlSettings = {
|
||||
baseUrl: "https://functions.yandexcloud.net/d4eno3im0qgsr4tj5hdo",
|
||||
cloudflare_gateway: "SWE-IHOR",
|
||||
bunny_gateway: "SWE-IHOR",
|
||||
fastly_gateway: "SWE-IHOR",
|
||||
telegram_gateway: "94.142.140.1",
|
||||
hetzner_gateway: "94.142.140.1",
|
||||
type: "routes",
|
||||
version: "v4.rsc",
|
||||
}
|
||||
|
||||
export const serverUrlPresets = {
|
||||
default: defaultServerUrlSettings,
|
||||
cloudflareSWE: {
|
||||
baseUrl: "https://cf.example/api",
|
||||
cloudflare_gateway: "SWE-IHOR",
|
||||
bunny_gateway: "",
|
||||
fastly_gateway: "",
|
||||
telegram_gateway: "",
|
||||
hetzner_gateway: "",
|
||||
type: "routes",
|
||||
version: "v4.rsc",
|
||||
},
|
||||
hetznerDE: {
|
||||
baseUrl: "https://hetzner.example/api",
|
||||
cloudflare_gateway: "",
|
||||
bunny_gateway: "",
|
||||
fastly_gateway: "",
|
||||
telegram_gateway: "",
|
||||
hetzner_gateway: "94.142.140.1",
|
||||
type: "routes",
|
||||
version: "v4.rsc",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { useState } from "react"
|
||||
|
||||
type ServerLike = {
|
||||
ip?: string
|
||||
}
|
||||
|
||||
export function useServerSelection(filteredServers: ServerLike[]) {
|
||||
const [selectedServers, setSelectedServers] = useState<Set<string>>(new Set())
|
||||
|
||||
const toggleServerSelection = (server: ServerLike) => {
|
||||
const serverIp = server.ip
|
||||
if (!serverIp) return
|
||||
|
||||
const next = new Set(selectedServers)
|
||||
if (next.has(serverIp)) {
|
||||
next.delete(serverIp)
|
||||
} else {
|
||||
next.add(serverIp)
|
||||
}
|
||||
setSelectedServers(next)
|
||||
}
|
||||
|
||||
const toggleGroupSelection = (servers: ServerLike[], selectAll: boolean) => {
|
||||
setSelectedServers((prev) => {
|
||||
const next = new Set(prev)
|
||||
servers.forEach((server) => {
|
||||
const serverIp = server.ip
|
||||
if (!serverIp) return
|
||||
if (selectAll) next.add(serverIp)
|
||||
else next.delete(serverIp)
|
||||
})
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
const selectAllServers = () => {
|
||||
if (selectedServers.size === filteredServers.length) {
|
||||
setSelectedServers(new Set())
|
||||
return
|
||||
}
|
||||
|
||||
const allServerIps = filteredServers.map((server) => server.ip).filter(Boolean) as string[]
|
||||
setSelectedServers(new Set(allServerIps))
|
||||
}
|
||||
|
||||
const clearSelection = () => {
|
||||
setSelectedServers(new Set())
|
||||
}
|
||||
|
||||
return {
|
||||
selectedServers,
|
||||
setSelectedServers,
|
||||
toggleServerSelection,
|
||||
toggleGroupSelection,
|
||||
selectAllServers,
|
||||
clearSelection,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import LegacyServerManager from "@/ServerManager"
|
||||
|
||||
export function ServerManager() {
|
||||
return <LegacyServerManager />
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { ServerManager } from "@/features/servers/ui/server-manager"
|
||||
|
||||
export function ServersPage() {
|
||||
return <ServerManager />
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user