mirror of
https://github.com/walkxcode/dashboard-icons.git
synced 2025-11-09 21:18:58 +01:00
feat(website): enhance website
This commit is contained in:
committed by
Thomas Camlong
parent
6e3a39a4cf
commit
63349f7490
@@ -1,50 +1,94 @@
|
||||
import { METADATA_URL } from "@/constants"
|
||||
import type { IconFile, IconWithName } from "@/types/icons"
|
||||
|
||||
/**
|
||||
* Custom error class for API errors
|
||||
*/
|
||||
export class ApiError extends Error {
|
||||
status: number
|
||||
|
||||
constructor(message: string, status = 500) {
|
||||
super(message)
|
||||
this.name = "ApiError"
|
||||
this.status = status
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches all icon data from the metadata.json file
|
||||
*/
|
||||
|
||||
export async function getAllIcons(): Promise<IconFile> {
|
||||
const file = await fetch(METADATA_URL)
|
||||
return (await file.json()) as IconFile
|
||||
try {
|
||||
const response = await fetch(METADATA_URL)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new ApiError(`Failed to fetch icons: ${response.statusText}`, response.status)
|
||||
}
|
||||
|
||||
return (await response.json()) as IconFile
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError) {
|
||||
throw error
|
||||
}
|
||||
console.error("Error fetching icons:", error)
|
||||
throw new ApiError("Failed to fetch icons data. Please try again later.")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of all icon names.
|
||||
*/
|
||||
export const getIconNames = async (): Promise<string[]> => {
|
||||
const iconsData = await getAllIcons()
|
||||
return Object.keys(iconsData)
|
||||
try {
|
||||
const iconsData = await getAllIcons()
|
||||
return Object.keys(iconsData)
|
||||
} catch (error) {
|
||||
console.error("Error getting icon names:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts icon data to an array format for easier rendering
|
||||
*/
|
||||
export async function getIconsArray(): Promise<IconWithName[]> {
|
||||
const iconsData = await getAllIcons()
|
||||
try {
|
||||
const iconsData = await getAllIcons()
|
||||
|
||||
return Object.entries(iconsData)
|
||||
.map(([name, data]) => ({
|
||||
name,
|
||||
data,
|
||||
}))
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
return Object.entries(iconsData)
|
||||
.map(([name, data]) => ({
|
||||
name,
|
||||
data,
|
||||
}))
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
} catch (error) {
|
||||
console.error("Error getting icons array:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches data for a specific icon
|
||||
*/
|
||||
export async function getIconData(iconName: string): Promise<IconWithName | null> {
|
||||
const iconsData = await getAllIcons()
|
||||
const iconData = iconsData[iconName]
|
||||
try {
|
||||
const iconsData = await getAllIcons()
|
||||
const iconData = iconsData[iconName]
|
||||
|
||||
if (!iconData) {
|
||||
return null
|
||||
}
|
||||
if (!iconData) {
|
||||
throw new ApiError(`Icon '${iconName}' not found`, 404)
|
||||
}
|
||||
|
||||
return {
|
||||
name: iconName,
|
||||
data: iconData,
|
||||
return {
|
||||
name: iconName,
|
||||
data: iconData,
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError && error.status === 404) {
|
||||
return null
|
||||
}
|
||||
console.error("Error getting icon data:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,24 +96,57 @@ export async function getIconData(iconName: string): Promise<IconWithName | null
|
||||
* Fetches author data from GitHub API
|
||||
*/
|
||||
export async function getAuthorData(authorId: number) {
|
||||
const response = await fetch(`https://api.github.com/user/${authorId}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
|
||||
"Cache-Control": "public, max-age=86400",
|
||||
},
|
||||
next: { revalidate: 86400 }, // Revalidate cache once a day
|
||||
})
|
||||
return response.json()
|
||||
try {
|
||||
const response = await fetch(`https://api.github.com/user/${authorId}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
|
||||
"Cache-Control": "public, max-age=86400",
|
||||
},
|
||||
next: { revalidate: 86400 }, // Revalidate cache once a day
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
// If unauthorized or other error, return a default user object
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
console.warn(`GitHub API rate limit or authorization issue: ${response.statusText}`)
|
||||
return {
|
||||
login: "unknown",
|
||||
avatar_url: "https://avatars.githubusercontent.com/u/0",
|
||||
html_url: "https://github.com",
|
||||
name: "Unknown User",
|
||||
bio: null,
|
||||
}
|
||||
}
|
||||
throw new ApiError(`Failed to fetch author data: ${response.statusText}`, response.status)
|
||||
}
|
||||
|
||||
return response.json()
|
||||
} catch (error) {
|
||||
console.error("Error fetching author data:", error)
|
||||
// Even for unexpected errors, return a default user to prevent page failures
|
||||
return {
|
||||
login: "unknown",
|
||||
avatar_url: "https://avatars.githubusercontent.com/u/0",
|
||||
html_url: "https://github.com",
|
||||
name: "Unknown User",
|
||||
bio: null,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches featured icons for the homepage
|
||||
* Fetches total icon count
|
||||
*/
|
||||
export async function getTotalIcons() {
|
||||
const iconsData = await getAllIcons()
|
||||
try {
|
||||
const iconsData = await getAllIcons()
|
||||
|
||||
return {
|
||||
totalIcons: Object.keys(iconsData).length,
|
||||
return {
|
||||
totalIcons: Object.keys(iconsData).length,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error getting total icons:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,12 +154,17 @@ export async function getTotalIcons() {
|
||||
* Fetches recently added icons sorted by timestamp
|
||||
*/
|
||||
export async function getRecentlyAddedIcons(limit = 8): Promise<IconWithName[]> {
|
||||
const icons = await getIconsArray()
|
||||
try {
|
||||
const icons = await getIconsArray()
|
||||
|
||||
return icons
|
||||
.sort((a, b) => {
|
||||
// Sort by timestamp in descending order (newest first)
|
||||
return new Date(b.data.update.timestamp).getTime() - new Date(a.data.update.timestamp).getTime()
|
||||
})
|
||||
.slice(0, limit)
|
||||
return icons
|
||||
.sort((a, b) => {
|
||||
// Sort by timestamp in descending order (newest first)
|
||||
return new Date(b.data.update.timestamp).getTime() - new Date(a.data.update.timestamp).getTime()
|
||||
})
|
||||
.slice(0, limit)
|
||||
} catch (error) {
|
||||
console.error("Error getting recently added icons:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,3 +4,122 @@ import { twMerge } from "tailwind-merge"
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Levenshtein distance between two strings
|
||||
*/
|
||||
export function levenshteinDistance(a: string, b: string): number {
|
||||
const matrix: number[][] = []
|
||||
|
||||
// Initialize the matrix
|
||||
for (let i = 0; i <= b.length; i++) {
|
||||
matrix[i] = [i]
|
||||
}
|
||||
for (let j = 0; j <= a.length; j++) {
|
||||
matrix[0][j] = j
|
||||
}
|
||||
|
||||
// Fill the matrix
|
||||
for (let i = 1; i <= b.length; i++) {
|
||||
for (let j = 1; j <= a.length; j++) {
|
||||
const cost = a[j - 1] === b[i - 1] ? 0 : 1
|
||||
matrix[i][j] = Math.min(
|
||||
matrix[i - 1][j] + 1, // deletion
|
||||
matrix[i][j - 1] + 1, // insertion
|
||||
matrix[i - 1][j - 1] + cost, // substitution
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return matrix[b.length][a.length]
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate similarity score between two strings (0-1)
|
||||
* Higher score means more similar
|
||||
*/
|
||||
export function calculateStringSimilarity(str1: string, str2: string): number {
|
||||
if (!str1.length || !str2.length) return 0
|
||||
if (str1 === str2) return 1
|
||||
|
||||
const distance = levenshteinDistance(str1.toLowerCase(), str2.toLowerCase())
|
||||
const maxLength = Math.max(str1.length, str2.length)
|
||||
return 1 - distance / maxLength
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if string contains all characters from query in order
|
||||
* Returns match score (0 if no match)
|
||||
*/
|
||||
export function containsCharsInOrder(str: string, query: string): number {
|
||||
if (!query) return 1
|
||||
if (!str) return 0
|
||||
|
||||
const normalizedStr = str.toLowerCase()
|
||||
const normalizedQuery = query.toLowerCase()
|
||||
|
||||
let strIndex = 0
|
||||
let queryIndex = 0
|
||||
|
||||
while (strIndex < normalizedStr.length && queryIndex < normalizedQuery.length) {
|
||||
if (normalizedStr[strIndex] === normalizedQuery[queryIndex]) {
|
||||
queryIndex++
|
||||
}
|
||||
strIndex++
|
||||
}
|
||||
|
||||
// If we matched all characters in the query
|
||||
if (queryIndex === normalizedQuery.length) {
|
||||
// Calculate a score based on closeness of matches
|
||||
// Higher score if characters are close together
|
||||
const matchRatio = normalizedStr.length / (strIndex + 1)
|
||||
return matchRatio
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Advanced fuzzy search with multiple scoring methods
|
||||
* Returns a score from 0-1, where 1 is a perfect match
|
||||
*/
|
||||
export function fuzzySearch(text: string, query: string): number {
|
||||
if (!query) return 1
|
||||
if (!text) return 0
|
||||
|
||||
// Direct inclusion check (highest priority)
|
||||
const normalizedText = text.toLowerCase()
|
||||
const normalizedQuery = query.toLowerCase()
|
||||
|
||||
if (normalizedText === normalizedQuery) return 1
|
||||
if (normalizedText.includes(normalizedQuery)) return 0.9
|
||||
|
||||
// Check for character sequence matches
|
||||
const sequenceScore = containsCharsInOrder(normalizedText, normalizedQuery)
|
||||
|
||||
// Calculate string similarity
|
||||
const similarityScore = calculateStringSimilarity(normalizedText, normalizedQuery)
|
||||
|
||||
// Word-by-word matching for multi-word queries
|
||||
const textWords = normalizedText.split(/\s+/)
|
||||
const queryWords = normalizedQuery.split(/\s+/)
|
||||
|
||||
let wordMatchCount = 0
|
||||
for (const queryWord of queryWords) {
|
||||
for (const textWord of textWords) {
|
||||
if (
|
||||
textWord.includes(queryWord) ||
|
||||
calculateStringSimilarity(textWord, queryWord) > 0.7 ||
|
||||
containsCharsInOrder(textWord, queryWord) > 0
|
||||
) {
|
||||
wordMatchCount++
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const wordMatchScore = queryWords.length > 0 ? wordMatchCount / queryWords.length : 0
|
||||
|
||||
// Combine scores with weights
|
||||
return Math.max(sequenceScore * 0.3, similarityScore * 0.3, wordMatchScore * 0.4)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user