Refactor frontend structure and update dependencies
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:
2026-04-26 14:56:48 +07:00
parent fe7acd655e
commit b0c8a0a4bb
109 changed files with 9800 additions and 2816 deletions
@@ -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 />
}