chore: update CHANGELOG.md for 0.3.0-alpha release and implement favicon support in Top Referrers component
This commit is contained in:
@@ -4,6 +4,12 @@ All notable changes to Pulse (frontend and product) are documented in this file.
|
|||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and Pulse uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html) with a **0.x.y** version scheme while in initial development. The leading `0` indicates that the public API and behaviour may change until we release **1.0.0**.
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and Pulse uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html) with a **0.x.y** version scheme while in initial development. The leading `0` indicates that the public API and behaviour may change until we release **1.0.0**.
|
||||||
|
|
||||||
|
## [0.3.0-alpha] - 2026-02-11
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **Top Referrers favicons (PULSE-52).** The Top Referrers card now shows real site favicons (e.g. Google, ChatGPT, Instagram) when the referrer is a domain or URL. “Direct” and “Unknown” keep the globe icon; if a favicon fails to load, the previous icon is shown as fallback.
|
||||||
|
|
||||||
## [0.2.0-alpha] - 2026-02-11
|
## [0.2.0-alpha] - 2026-02-11
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -21,6 +27,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
[Unreleased]: https://github.com/ciphera-net/pulse/compare/v0.2.0-alpha...HEAD
|
[Unreleased]: https://github.com/ciphera-net/pulse/compare/v0.3.0-alpha...HEAD
|
||||||
|
[0.3.0-alpha]: https://github.com/ciphera-net/pulse/compare/v0.2.0-alpha...v0.3.0-alpha
|
||||||
[0.2.0-alpha]: https://github.com/ciphera-net/pulse/compare/v0.1.0-alpha...v0.2.0-alpha
|
[0.2.0-alpha]: https://github.com/ciphera-net/pulse/compare/v0.1.0-alpha...v0.2.0-alpha
|
||||||
[0.1.0-alpha]: https://github.com/ciphera-net/pulse/releases/tag/v0.1.0-alpha
|
[0.1.0-alpha]: https://github.com/ciphera-net/pulse/releases/tag/v0.1.0-alpha
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { formatNumber } from '@/lib/utils/format'
|
import { formatNumber } from '@/lib/utils/format'
|
||||||
import { getReferrerIcon } from '@/lib/utils/icons'
|
import { getReferrerFavicon, getReferrerIcon } from '@/lib/utils/icons'
|
||||||
import { Modal, GlobeIcon } from '@ciphera-net/ui'
|
import { Modal, GlobeIcon } from '@ciphera-net/ui'
|
||||||
import { getTopReferrers, TopReferrer } from '@/lib/api/stats'
|
import { getTopReferrers, TopReferrer } from '@/lib/api/stats'
|
||||||
|
|
||||||
@@ -19,6 +19,7 @@ export default function TopReferrers({ referrers, collectReferrers = true, siteI
|
|||||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||||
const [fullData, setFullData] = useState<TopReferrer[]>([])
|
const [fullData, setFullData] = useState<TopReferrer[]>([])
|
||||||
const [isLoadingFull, setIsLoadingFull] = useState(false)
|
const [isLoadingFull, setIsLoadingFull] = useState(false)
|
||||||
|
const [faviconFailed, setFaviconFailed] = useState<Set<string>>(new Set())
|
||||||
|
|
||||||
// Filter out empty/unknown referrers
|
// Filter out empty/unknown referrers
|
||||||
const filteredReferrers = (referrers || []).filter(
|
const filteredReferrers = (referrers || []).filter(
|
||||||
@@ -30,6 +31,22 @@ export default function TopReferrers({ referrers, collectReferrers = true, siteI
|
|||||||
const emptySlots = Math.max(0, LIMIT - displayedReferrers.length)
|
const emptySlots = Math.max(0, LIMIT - displayedReferrers.length)
|
||||||
const showViewAll = hasData && filteredReferrers.length > LIMIT
|
const showViewAll = hasData && filteredReferrers.length > LIMIT
|
||||||
|
|
||||||
|
function renderReferrerIcon(referrer: string) {
|
||||||
|
const faviconUrl = getReferrerFavicon(referrer)
|
||||||
|
const useFavicon = faviconUrl && !faviconFailed.has(referrer)
|
||||||
|
if (useFavicon) {
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
src={faviconUrl}
|
||||||
|
alt=""
|
||||||
|
className="w-5 h-5 flex-shrink-0 rounded object-contain"
|
||||||
|
onError={() => setFaviconFailed((prev) => new Set(prev).add(referrer))}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return <span className="text-lg flex-shrink-0">{getReferrerIcon(referrer)}</span>
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isModalOpen) {
|
if (isModalOpen) {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
@@ -80,7 +97,7 @@ export default function TopReferrers({ referrers, collectReferrers = true, siteI
|
|||||||
{displayedReferrers.map((ref, index) => (
|
{displayedReferrers.map((ref, index) => (
|
||||||
<div key={index} className="flex items-center justify-between h-9 group hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded-lg px-2 -mx-2 transition-colors">
|
<div key={index} className="flex items-center justify-between h-9 group hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded-lg px-2 -mx-2 transition-colors">
|
||||||
<div className="flex-1 truncate text-neutral-900 dark:text-white flex items-center gap-3">
|
<div className="flex-1 truncate text-neutral-900 dark:text-white flex items-center gap-3">
|
||||||
<span className="text-lg flex-shrink-0">{getReferrerIcon(ref.referrer)}</span>
|
{renderReferrerIcon(ref.referrer)}
|
||||||
<span className="truncate" title={ref.referrer}>{ref.referrer}</span>
|
<span className="truncate" title={ref.referrer}>{ref.referrer}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm font-semibold text-neutral-600 dark:text-neutral-400 ml-4">
|
<div className="text-sm font-semibold text-neutral-600 dark:text-neutral-400 ml-4">
|
||||||
@@ -123,7 +140,7 @@ export default function TopReferrers({ referrers, collectReferrers = true, siteI
|
|||||||
(fullData.length > 0 ? fullData : filteredReferrers).map((ref, index) => (
|
(fullData.length > 0 ? fullData : filteredReferrers).map((ref, index) => (
|
||||||
<div key={index} className="flex items-center justify-between py-2 group hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded-lg px-2 -mx-2 transition-colors">
|
<div key={index} className="flex items-center justify-between py-2 group hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded-lg px-2 -mx-2 transition-colors">
|
||||||
<div className="flex-1 truncate text-neutral-900 dark:text-white flex items-center gap-3">
|
<div className="flex-1 truncate text-neutral-900 dark:text-white flex items-center gap-3">
|
||||||
<span className="text-lg flex-shrink-0">{getReferrerIcon(ref.referrer)}</span>
|
{renderReferrerIcon(ref.referrer)}
|
||||||
<span className="truncate" title={ref.referrer}>{ref.referrer}</span>
|
<span className="truncate" title={ref.referrer}>{ref.referrer}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm font-semibold text-neutral-600 dark:text-neutral-400 ml-4">
|
<div className="text-sm font-semibold text-neutral-600 dark:text-neutral-400 ml-4">
|
||||||
|
|||||||
@@ -78,11 +78,20 @@ export function getReferrerIcon(referrerName: string) {
|
|||||||
return <FaGlobe className="text-neutral-400" />
|
return <FaGlobe className="text-neutral-400" />
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getReferrerFavicon(referrer: string) {
|
const REFERRER_NO_FAVICON = ['direct', 'unknown', '']
|
||||||
try {
|
|
||||||
const url = new URL(referrer.startsWith('http') ? referrer : `https://${referrer}`);
|
/**
|
||||||
return `https://www.google.com/s2/favicons?domain=${url.hostname}&sz=32`;
|
* Returns a favicon URL for the referrer's domain, or null for non-URL referrers
|
||||||
} catch (e) {
|
* (e.g. "Direct", "Unknown") so callers can show an icon fallback instead.
|
||||||
return null;
|
*/
|
||||||
}
|
export function getReferrerFavicon(referrer: string): string | null {
|
||||||
|
if (!referrer || typeof referrer !== 'string') return null
|
||||||
|
const normalized = referrer.trim().toLowerCase()
|
||||||
|
if (REFERRER_NO_FAVICON.includes(normalized)) return null
|
||||||
|
try {
|
||||||
|
const url = new URL(referrer.startsWith('http') ? referrer : `https://${referrer}`)
|
||||||
|
return `https://www.google.com/s2/favicons?domain=${url.hostname}&sz=32`
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pulse-frontend",
|
"name": "pulse-frontend",
|
||||||
"version": "0.1.0",
|
"version": "0.3.0-alpha",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
|
|||||||
Reference in New Issue
Block a user