lint fix wip

This commit is contained in:
Nicolas Cantu 2026-01-06 11:30:23 +01:00
parent 5ac5aab089
commit 412989e6af
91 changed files with 506 additions and 328 deletions

View File

@ -5,7 +5,7 @@ interface AlbyInstallerProps {
onInstalled?: () => void onInstalled?: () => void
} }
function InfoIcon() { function InfoIcon(): JSX.Element {
return ( return (
<svg <svg
className="h-5 w-5 text-blue-400" className="h-5 w-5 text-blue-400"
@ -27,7 +27,7 @@ interface InstallerActionsProps {
markInstalled: () => void markInstalled: () => void
} }
function InstallerActions({ onInstalled, markInstalled }: InstallerActionsProps) { function InstallerActions({ onInstalled, markInstalled }: InstallerActionsProps): JSX.Element {
const connect = useCallback(() => { const connect = useCallback(() => {
const alby = getAlbyService() const alby = getAlbyService()
void alby.enable().then(() => { void alby.enable().then(() => {
@ -60,7 +60,7 @@ function InstallerActions({ onInstalled, markInstalled }: InstallerActionsProps)
) )
} }
function InstallerBody({ onInstalled, markInstalled }: InstallerActionsProps) { function InstallerBody({ onInstalled, markInstalled }: InstallerActionsProps): JSX.Element {
return ( return (
<div className="ml-3 flex-1"> <div className="ml-3 flex-1">
<h3 className="text-sm font-medium text-blue-800">Alby Extension Required</h3> <h3 className="text-sm font-medium text-blue-800">Alby Extension Required</h3>
@ -78,12 +78,12 @@ function InstallerBody({ onInstalled, markInstalled }: InstallerActionsProps) {
) )
} }
function useAlbyStatus(onInstalled?: () => void) { function useAlbyStatus(onInstalled?: () => void): { isInstalled: boolean; isChecking: boolean; markInstalled: () => void } {
const [isInstalled, setIsInstalled] = useState(false) const [isInstalled, setIsInstalled] = useState(false)
const [isChecking, setIsChecking] = useState(true) const [isChecking, setIsChecking] = useState(true)
useEffect(() => { useEffect(() => {
const checkAlby = () => { const checkAlby = (): void => {
try { try {
const alby = getAlbyService() const alby = getAlbyService()
const installed = alby.isEnabled() const installed = alby.isEnabled()
@ -101,14 +101,14 @@ function useAlbyStatus(onInstalled?: () => void) {
checkAlby() checkAlby()
}, [onInstalled]) }, [onInstalled])
const markInstalled = () => { const markInstalled = (): void => {
setIsInstalled(true) setIsInstalled(true)
} }
return { isInstalled, isChecking, markInstalled } return { isInstalled, isChecking, markInstalled }
} }
export function AlbyInstaller({ onInstalled }: AlbyInstallerProps) { export function AlbyInstaller({ onInstalled }: AlbyInstallerProps): JSX.Element | null {
const { isInstalled, isChecking, markInstalled } = useAlbyStatus(onInstalled) const { isInstalled, isChecking, markInstalled } = useAlbyStatus(onInstalled)
if (isChecking || isInstalled) { if (isChecking || isInstalled) {

View File

@ -11,7 +11,7 @@ interface ArticleCardProps {
onUnlock?: (article: Article) => void onUnlock?: (article: Article) => void
} }
function ArticleHeader({ article }: { article: Article }) { function ArticleHeader({ article }: { article: Article }): JSX.Element {
return ( return (
<div className="mb-2 flex items-center justify-between"> <div className="mb-2 flex items-center justify-between">
<h2 className="text-2xl font-bold text-neon-cyan">{article.title}</h2> <h2 className="text-2xl font-bold text-neon-cyan">{article.title}</h2>
@ -37,7 +37,7 @@ function ArticleMeta({
paymentInvoice: ReturnType<typeof useArticlePayment>['paymentInvoice'] paymentInvoice: ReturnType<typeof useArticlePayment>['paymentInvoice']
onClose: () => void onClose: () => void
onPaymentComplete: () => void onPaymentComplete: () => void
}) { }): JSX.Element {
return ( return (
<> <>
{error && <p className="text-sm text-red-400 mt-2">{error}</p>} {error && <p className="text-sm text-red-400 mt-2">{error}</p>}
@ -55,7 +55,7 @@ function ArticleMeta({
) )
} }
export function ArticleCard({ article, onUnlock }: ArticleCardProps) { export function ArticleCard({ article, onUnlock }: ArticleCardProps): JSX.Element {
const { pubkey, connect } = useNostrAuth() const { pubkey, connect } = useNostrAuth()
const { const {
loading, loading,

View File

@ -12,7 +12,7 @@ interface ArticleEditorProps {
} }
function SuccessMessage() { function SuccessMessage(): JSX.Element {
return ( return (
<div className="border rounded-lg p-6 bg-green-50 border-green-200"> <div className="border rounded-lg p-6 bg-green-50 border-green-200">
<h3 className="text-lg font-semibold text-green-800 mb-2">Article Published!</h3> <h3 className="text-lg font-semibold text-green-800 mb-2">Article Published!</h3>
@ -21,7 +21,7 @@ function SuccessMessage() {
) )
} }
export function ArticleEditor({ onPublishSuccess, onCancel, seriesOptions, onSelectSeries }: ArticleEditorProps) { export function ArticleEditor({ onPublishSuccess, onCancel, seriesOptions, onSelectSeries }: ArticleEditorProps): JSX.Element {
const { connected, pubkey, connect } = useNostrAuth() const { connected, pubkey, connect } = useNostrAuth()
const { loading, error, success, publishArticle } = useArticlePublishing(pubkey ?? null) const { loading, error, success, publishArticle } = useArticlePublishing(pubkey ?? null)
const [draft, setDraft] = useState<ArticleDraft>({ const [draft, setDraft] = useState<ArticleDraft>({
@ -61,8 +61,8 @@ function buildSubmitHandler(
onPublishSuccess?: (articleId: string) => void, onPublishSuccess?: (articleId: string) => void,
connect?: () => Promise<void>, connect?: () => Promise<void>,
connected?: boolean connected?: boolean
) { ): () => Promise<void> {
return async () => { return async (): Promise<void> => {
if (!connected && connect) { if (!connected && connect) {
await connect() await connect()
return return

View File

@ -25,7 +25,7 @@ function CategoryField({
}: { }: {
value: ArticleDraft['category'] value: ArticleDraft['category']
onChange: (value: import('@/types/nostr').ArticleCategory | undefined) => void onChange: (value: import('@/types/nostr').ArticleCategory | undefined) => void
}) { }): JSX.Element {
return ( return (
<CategorySelect <CategorySelect
id="category" id="category"
@ -38,7 +38,7 @@ function CategoryField({
) )
} }
function ErrorAlert({ error }: { error: string | null }) { function ErrorAlert({ error }: { error: string | null }): JSX.Element | null {
if (!error) { if (!error) {
return null return null
} }
@ -76,7 +76,7 @@ const ArticleFieldsLeft = ({
onDraftChange: (draft: ArticleDraft) => void onDraftChange: (draft: ArticleDraft) => void
seriesOptions?: { id: string; title: string }[] | undefined seriesOptions?: { id: string; title: string }[] | undefined
onSelectSeries?: ((seriesId: string | undefined) => void) | undefined onSelectSeries?: ((seriesId: string | undefined) => void) | undefined
}) => ( }): JSX.Element => (
<div className="space-y-4"> <div className="space-y-4">
<CategoryField <CategoryField
value={draft.category} value={draft.category}
@ -95,7 +95,7 @@ const ArticleFieldsLeft = ({
</div> </div>
) )
function ArticleTitleField({ draft, onDraftChange }: { draft: ArticleDraft; onDraftChange: (draft: ArticleDraft) => void }) { function ArticleTitleField({ draft, onDraftChange }: { draft: ArticleDraft; onDraftChange: (draft: ArticleDraft) => void }): JSX.Element {
return ( return (
<ArticleField <ArticleField
id="title" id="title"
@ -114,7 +114,7 @@ function ArticlePreviewField({
}: { }: {
draft: ArticleDraft draft: ArticleDraft
onDraftChange: (draft: ArticleDraft) => void onDraftChange: (draft: ArticleDraft) => void
}) { }): JSX.Element {
return ( return (
<ArticleField <ArticleField
id="preview" id="preview"
@ -140,7 +140,7 @@ function SeriesSelect({
onDraftChange: (draft: ArticleDraft) => void onDraftChange: (draft: ArticleDraft) => void
seriesOptions: { id: string; title: string }[] seriesOptions: { id: string; title: string }[]
onSelectSeries?: ((seriesId: string | undefined) => void) | undefined onSelectSeries?: ((seriesId: string | undefined) => void) | undefined
}) { }): JSX.Element {
const handleChange = buildSeriesChangeHandler(draft, onDraftChange, onSelectSeries) const handleChange = buildSeriesChangeHandler(draft, onDraftChange, onSelectSeries)
return ( return (
@ -169,8 +169,8 @@ function buildSeriesChangeHandler(
draft: ArticleDraft, draft: ArticleDraft,
onDraftChange: (draft: ArticleDraft) => void, onDraftChange: (draft: ArticleDraft) => void,
onSelectSeries?: ((seriesId: string | undefined) => void) | undefined onSelectSeries?: ((seriesId: string | undefined) => void) | undefined
) { ): (e: React.ChangeEvent<HTMLSelectElement>) => void {
return (e: React.ChangeEvent<HTMLSelectElement>) => { return (e: React.ChangeEvent<HTMLSelectElement>): void => {
const value = e.target.value || undefined const value = e.target.value || undefined
const nextDraft = { ...draft } const nextDraft = { ...draft }
if (value) { if (value) {
@ -189,7 +189,7 @@ const ArticleFieldsRight = ({
}: { }: {
draft: ArticleDraft draft: ArticleDraft
onDraftChange: (draft: ArticleDraft) => void onDraftChange: (draft: ArticleDraft) => void
}) => ( }): JSX.Element => (
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<div className="text-sm font-semibold text-gray-800">{t('article.editor.content.label')}</div> <div className="text-sm font-semibold text-gray-800">{t('article.editor.content.label')}</div>
@ -230,7 +230,7 @@ export function ArticleEditorForm({
onCancel, onCancel,
seriesOptions, seriesOptions,
onSelectSeries, onSelectSeries,
}: ArticleEditorFormProps) { }: ArticleEditorFormProps): JSX.Element {
return ( return (
<form onSubmit={onSubmit} className="border rounded-lg p-6 bg-white space-y-4"> <form onSubmit={onSubmit} className="border rounded-lg p-6 bg-white space-y-4">
<h2 className="text-2xl font-bold mb-4">{t('article.editor.title')}</h2> <h2 className="text-2xl font-bold mb-4">{t('article.editor.title')}</h2>

View File

@ -30,7 +30,7 @@ function NumberOrTextInput({
min?: number min?: number
className: string className: string
onChange: (value: string | number) => void onChange: (value: string | number) => void
}) { }): JSX.Element {
const inputProps = { const inputProps = {
id, id,
type, type,
@ -64,7 +64,7 @@ function TextAreaInput({
rows?: number rows?: number
className: string className: string
onChange: (value: string | number) => void onChange: (value: string | number) => void
}) { }): JSX.Element {
const areaProps = { const areaProps = {
id, id,
value, value,
@ -81,7 +81,7 @@ function TextAreaInput({
) )
} }
export function ArticleField(props: ArticleFieldProps) { export function ArticleField(props: ArticleFieldProps): JSX.Element {
const { id, label, value, onChange, required = false, type = 'text', rows, placeholder, helpText, min } = const { id, label, value, onChange, required = false, type = 'text', rows, placeholder, helpText, min } =
props props
const inputClass = const inputClass =

View File

@ -46,8 +46,8 @@ function FiltersGrid({
data: FiltersData data: FiltersData
filters: ArticleFilters filters: ArticleFilters
onFiltersChange: (filters: ArticleFilters) => void onFiltersChange: (filters: ArticleFilters) => void
}) { }): JSX.Element {
const update = (patch: Partial<ArticleFilters>) => onFiltersChange({ ...filters, ...patch }) const update = (patch: Partial<ArticleFilters>): void => onFiltersChange({ ...filters, ...patch })
return ( return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
@ -63,7 +63,7 @@ function FiltersHeader({
}: { }: {
hasActiveFilters: boolean hasActiveFilters: boolean
onClear: () => void onClear: () => void
}) { }): JSX.Element {
return ( return (
<div className="flex justify-between items-center mb-4"> <div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-semibold text-neon-cyan">{t('filters.sort')}</h3> <h3 className="text-lg font-semibold text-neon-cyan">{t('filters.sort')}</h3>
@ -83,7 +83,7 @@ function SortFilter({
}: { }: {
value: SortOption value: SortOption
onChange: (value: SortOption) => void onChange: (value: SortOption) => void
}) { }): JSX.Element {
return ( return (
<div> <div>
<label htmlFor="sort" className="block text-sm font-medium text-cyber-accent mb-1"> <label htmlFor="sort" className="block text-sm font-medium text-cyber-accent mb-1">
@ -106,7 +106,7 @@ export function ArticleFiltersComponent({
filters, filters,
onFiltersChange, onFiltersChange,
articles, articles,
}: ArticleFiltersProps) { }: ArticleFiltersProps): JSX.Element {
const data = useFiltersData(articles) const data = useFiltersData(articles)
const handleClearFilters = () => { const handleClearFilters = () => {

View File

@ -5,7 +5,7 @@ interface ArticleFormButtonsProps {
onCancel?: () => void onCancel?: () => void
} }
export function ArticleFormButtons({ loading, onCancel }: ArticleFormButtonsProps) { export function ArticleFormButtons({ loading, onCancel }: ArticleFormButtonsProps): JSX.Element {
return ( return (
<div className="flex gap-3 pt-4"> <div className="flex gap-3 pt-4">
<button <button

View File

@ -6,7 +6,7 @@ interface ArticlePreviewProps {
onUnlock: () => void onUnlock: () => void
} }
export function ArticlePreview({ article, loading, onUnlock }: ArticlePreviewProps) { export function ArticlePreview({ article, loading, onUnlock }: ArticlePreviewProps): JSX.Element {
if (article.paid) { if (article.paid) {
return ( return (
<div> <div>

View File

@ -8,14 +8,14 @@ interface ArticleReviewsProps {
authorPubkey: string authorPubkey: string
} }
export function ArticleReviews({ articleId, authorPubkey }: ArticleReviewsProps) { export function ArticleReviews({ articleId, authorPubkey }: ArticleReviewsProps): JSX.Element {
const [reviews, setReviews] = useState<Review[]>([]) const [reviews, setReviews] = useState<Review[]>([])
const [tips, setTips] = useState<number>(0) const [tips, setTips] = useState<number>(0)
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
useEffect(() => { useEffect(() => {
const load = async () => { const load = async (): Promise<void> => {
setLoading(true) setLoading(true)
setError(null) setError(null)
try { try {
@ -45,7 +45,7 @@ export function ArticleReviews({ articleId, authorPubkey }: ArticleReviewsProps)
) )
} }
function ArticleReviewsHeader({ tips }: { tips: number }) { function ArticleReviewsHeader({ tips }: { tips: number }): JSX.Element {
return ( return (
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h3 className="text-lg font-semibold">Critiques</h3> <h3 className="text-lg font-semibold">Critiques</h3>
@ -54,7 +54,7 @@ function ArticleReviewsHeader({ tips }: { tips: number }) {
) )
} }
function ArticleReviewsList({ reviews }: { reviews: Review[] }) { function ArticleReviewsList({ reviews }: { reviews: Review[] }): JSX.Element {
return ( return (
<> <>
{reviews.map((r) => ( {reviews.map((r) => (

View File

@ -11,7 +11,7 @@ interface ArticlesListProps {
unlockedArticles: Set<string> unlockedArticles: Set<string>
} }
function LoadingState() { function LoadingState(): JSX.Element {
// Use generic loading message at startup, then specific message once we know what we're loading // Use generic loading message at startup, then specific message once we know what we're loading
return ( return (
<div className="text-center py-12"> <div className="text-center py-12">
@ -20,7 +20,7 @@ function LoadingState() {
) )
} }
function ErrorState({ message }: { message: string }) { function ErrorState({ message }: { message: string }): JSX.Element {
return ( return (
<div className="bg-red-900/20 border border-red-500/50 rounded-lg p-4 mb-4"> <div className="bg-red-900/20 border border-red-500/50 rounded-lg p-4 mb-4">
<p className="text-red-400">{message}</p> <p className="text-red-400">{message}</p>
@ -28,7 +28,7 @@ function ErrorState({ message }: { message: string }) {
) )
} }
function EmptyState({ hasAny }: { hasAny: boolean }) { function EmptyState({ hasAny }: { hasAny: boolean }): JSX.Element {
return ( return (
<div className="text-center py-12"> <div className="text-center py-12">
<p className="text-cyber-accent/70"> <p className="text-cyber-accent/70">
@ -45,7 +45,7 @@ export function ArticlesList({
error, error,
onUnlock, onUnlock,
unlockedArticles, unlockedArticles,
}: ArticlesListProps) { }: ArticlesListProps): JSX.Element {
if (loading) { if (loading) {
return <LoadingState /> return <LoadingState />
} }

View File

@ -7,7 +7,7 @@ interface AuthorCardProps {
presentation: Article presentation: Article
} }
export function AuthorCard({ presentation }: AuthorCardProps) { export function AuthorCard({ presentation }: AuthorCardProps): JSX.Element {
const authorName = presentation.title.replace(/^Présentation de /, '') || t('common.author') const authorName = presentation.title.replace(/^Présentation de /, '') || t('common.author')
const totalBTC = (presentation.totalSponsoring ?? 0) / 100_000_000 const totalBTC = (presentation.totalSponsoring ?? 0) / 100_000_000

View File

@ -4,7 +4,7 @@ import { useAuthorFilterProps } from './AuthorFilterHooks'
import { AuthorFilterButtonWrapper } from './AuthorFilterButton' import { AuthorFilterButtonWrapper } from './AuthorFilterButton'
import { AuthorDropdown } from './AuthorFilterDropdown' import { AuthorDropdown } from './AuthorFilterDropdown'
function AuthorFilterLabel() { function AuthorFilterLabel(): JSX.Element {
return ( return (
<label htmlFor="author-filter" className="block text-sm font-medium text-cyber-accent mb-1"> <label htmlFor="author-filter" className="block text-sm font-medium text-cyber-accent mb-1">
{t('filters.author')} {t('filters.author')}
@ -28,7 +28,7 @@ interface AuthorFilterContentProps {
selectedDisplayName: string selectedDisplayName: string
} }
function AuthorFilterContent(props: AuthorFilterContentProps) { function AuthorFilterContent(props: AuthorFilterContentProps): JSX.Element {
return ( return (
<div className="relative" ref={props.dropdownRef}> <div className="relative" ref={props.dropdownRef}>
<AuthorFilterButtonWrapper <AuthorFilterButtonWrapper
@ -64,7 +64,7 @@ export function AuthorFilter({
authors: string[] authors: string[]
value: string | null value: string | null
onChange: (value: string | null) => void onChange: (value: string | null) => void
}) { }): JSX.Element {
const props = useAuthorFilterProps(authors, value) const props = useAuthorFilterProps(authors, value)
return ( return (

View File

@ -1,7 +1,7 @@
import React from 'react' import React from 'react'
import { AuthorAvatar } from './AuthorFilterDropdown' import { AuthorAvatar } from './AuthorFilterDropdown'
export function AuthorMnemonicIcons({ value, getMnemonicIcons }: { value: string; getMnemonicIcons: (pubkey: string) => string[] }) { export function AuthorMnemonicIcons({ value, getMnemonicIcons }: { value: string; getMnemonicIcons: (pubkey: string) => string[] }): JSX.Element {
return ( return (
<div className="flex items-center gap-1 flex-shrink-0"> <div className="flex items-center gap-1 flex-shrink-0">
{getMnemonicIcons(value).map((icon, idx) => ( {getMnemonicIcons(value).map((icon, idx) => (
@ -23,7 +23,7 @@ export function AuthorFilterButtonContent({
selectedAuthor: { name?: string; picture?: string } | null | undefined selectedAuthor: { name?: string; picture?: string } | null | undefined
selectedDisplayName: string selectedDisplayName: string
getMnemonicIcons: (pubkey: string) => string[] getMnemonicIcons: (pubkey: string) => string[]
}) { }): JSX.Element {
return ( return (
<> <>
{value && ( {value && (
@ -38,7 +38,7 @@ export function AuthorFilterButtonContent({
) )
} }
export function DropdownArrowIcon({ isOpen }: { isOpen: boolean }) { export function DropdownArrowIcon({ isOpen }: { isOpen: boolean }): JSX.Element {
return ( return (
<svg <svg
className={`w-5 h-5 text-neon-cyan transition-transform ${isOpen ? 'rotate-180' : ''}`} className={`w-5 h-5 text-neon-cyan transition-transform ${isOpen ? 'rotate-180' : ''}`}
@ -68,7 +68,7 @@ export function AuthorFilterButton({
isOpen: boolean isOpen: boolean
setIsOpen: (open: boolean) => void setIsOpen: (open: boolean) => void
buttonRef: React.RefObject<HTMLButtonElement | null> buttonRef: React.RefObject<HTMLButtonElement | null>
}) { }): JSX.Element {
return ( return (
<button <button
id="author-filter" id="author-filter"
@ -106,7 +106,7 @@ export function AuthorFilterButtonWrapper({
isOpen: boolean isOpen: boolean
setIsOpen: (open: boolean) => void setIsOpen: (open: boolean) => void
buttonRef: React.RefObject<HTMLButtonElement | null> buttonRef: React.RefObject<HTMLButtonElement | null>
}) { }): JSX.Element {
return ( return (
<AuthorFilterButton <AuthorFilterButton
value={value} value={value}

View File

@ -1,7 +1,7 @@
import Image from 'next/image' import Image from 'next/image'
import { t } from '@/lib/i18n' import { t } from '@/lib/i18n'
export function AuthorAvatar({ picture, displayName }: { picture?: string; displayName: string }) { export function AuthorAvatar({ picture, displayName }: { picture?: string; displayName: string }): JSX.Element {
if (picture !== undefined) { if (picture !== undefined) {
return ( return (
<Image <Image
@ -32,7 +32,7 @@ export function AuthorOption({
mnemonicIcons: string[] mnemonicIcons: string[]
isSelected: boolean isSelected: boolean
onSelect: () => void onSelect: () => void
}) { }): JSX.Element {
return ( return (
<button <button
type="button" type="button"
@ -136,7 +136,7 @@ export function AuthorList({
getMnemonicIcons: (pubkey: string) => string[] getMnemonicIcons: (pubkey: string) => string[]
onChange: (value: string | null) => void onChange: (value: string | null) => void
setIsOpen: (open: boolean) => void setIsOpen: (open: boolean) => void
}) { }): JSX.Element {
return ( return (
<> <>
{authors.map((pubkey) => ( {authors.map((pubkey) => (
@ -167,7 +167,7 @@ export function AuthorDropdownContent({
getMnemonicIcons: (pubkey: string) => string[] getMnemonicIcons: (pubkey: string) => string[]
onChange: (value: string | null) => void onChange: (value: string | null) => void
setIsOpen: (open: boolean) => void setIsOpen: (open: boolean) => void
}) { }): JSX.Element {
return loading ? ( return loading ? (
<div className="px-3 py-2 text-sm text-cyber-accent/70">{t('filters.loading')}</div> <div className="px-3 py-2 text-sm text-cyber-accent/70">{t('filters.loading')}</div>
) : ( ) : (
@ -201,7 +201,7 @@ export function AuthorDropdown({
getDisplayName: (pubkey: string) => string getDisplayName: (pubkey: string) => string
getPicture: (pubkey: string) => string | undefined getPicture: (pubkey: string) => string | undefined
getMnemonicIcons: (pubkey: string) => string[] getMnemonicIcons: (pubkey: string) => string[]
}) { }): JSX.Element {
return ( return (
<div <div
className="absolute z-20 w-full mt-1 bg-cyber-dark border border-neon-cyan/30 rounded-lg shadow-glow-cyan max-h-60 overflow-auto" className="absolute z-20 w-full mt-1 bg-cyber-dark border border-neon-cyan/30 rounded-lg shadow-glow-cyan max-h-60 overflow-auto"

View File

@ -3,12 +3,12 @@ import { useAuthorsProfiles } from '@/hooks/useAuthorsProfiles'
import { generateMnemonicIcons } from '@/lib/mnemonicIcons' import { generateMnemonicIcons } from '@/lib/mnemonicIcons'
import { t } from '@/lib/i18n' import { t } from '@/lib/i18n'
export function useAuthorFilterDropdown(isOpen: boolean, setIsOpen: (open: boolean) => void) { export function useAuthorFilterDropdown(isOpen: boolean, setIsOpen: (open: boolean) => void): { dropdownRef: React.RefObject<HTMLDivElement>; buttonRef: React.RefObject<HTMLButtonElement> } {
const dropdownRef = useRef<HTMLDivElement>(null) const dropdownRef = useRef<HTMLDivElement>(null)
const buttonRef = useRef<HTMLButtonElement>(null) const buttonRef = useRef<HTMLButtonElement>(null)
useEffect(() => { useEffect(() => {
const handleClickOutside = (event: MouseEvent) => { const handleClickOutside = (event: MouseEvent): void => {
if ( if (
dropdownRef.current && dropdownRef.current &&
buttonRef.current && buttonRef.current &&
@ -19,7 +19,7 @@ export function useAuthorFilterDropdown(isOpen: boolean, setIsOpen: (open: boole
} }
} }
const handleEscape = (event: KeyboardEvent) => { const handleEscape = (event: KeyboardEvent): void => {
if (event.key === 'Escape') { if (event.key === 'Escape') {
setIsOpen(false) setIsOpen(false)
buttonRef.current?.focus() buttonRef.current?.focus()
@ -40,7 +40,7 @@ export function useAuthorFilterDropdown(isOpen: boolean, setIsOpen: (open: boole
return { dropdownRef, buttonRef } return { dropdownRef, buttonRef }
} }
export function useAuthorFilterHelpers(profiles: Map<string, { name?: string; picture?: string }>) { export function useAuthorFilterHelpers(profiles: Map<string, { name?: string; picture?: string }>): { getDisplayName: (pubkey: string) => string; getPicture: (pubkey: string) => string | undefined; getMnemonicIcons: (pubkey: string) => string[] } {
const getDisplayName = (pubkey: string): string => { const getDisplayName = (pubkey: string): string => {
const profile = profiles.get(pubkey) const profile = profiles.get(pubkey)
return profile?.name ?? `${pubkey.substring(0, 8)}...${pubkey.substring(pubkey.length - 8)}` return profile?.name ?? `${pubkey.substring(0, 8)}...${pubkey.substring(pubkey.length - 8)}`
@ -57,7 +57,19 @@ export function useAuthorFilterHelpers(profiles: Map<string, { name?: string; pi
return { getDisplayName, getPicture, getMnemonicIcons } return { getDisplayName, getPicture, getMnemonicIcons }
} }
export function useAuthorFilterState(authors: string[], value: string | null) { export function useAuthorFilterState(authors: string[], value: string | null): {
profiles: Map<string, { name?: string; picture?: string }>
loading: boolean
isOpen: boolean
setIsOpen: (open: boolean) => void
dropdownRef: React.RefObject<HTMLDivElement>
buttonRef: React.RefObject<HTMLButtonElement>
getDisplayName: (pubkey: string) => string
getPicture: (pubkey: string) => string | undefined
getMnemonicIcons: (pubkey: string) => string[]
selectedAuthor: { name?: string; picture?: string } | null
selectedDisplayName: string
} {
const { profiles, loading } = useAuthorsProfiles(authors) const { profiles, loading } = useAuthorsProfiles(authors)
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const { dropdownRef, buttonRef } = useAuthorFilterDropdown(isOpen, setIsOpen) const { dropdownRef, buttonRef } = useAuthorFilterDropdown(isOpen, setIsOpen)
@ -80,7 +92,7 @@ export function useAuthorFilterState(authors: string[], value: string | null) {
} }
} }
export function useAuthorFilterProps(authors: string[], value: string | null) { export function useAuthorFilterProps(authors: string[], value: string | null): ReturnType<typeof useAuthorFilterState> & { authors: string[]; value: string | null } {
const state = useAuthorFilterState(authors, value) const state = useAuthorFilterState(authors, value)
return { ...state, authors, value } return { ...state, authors, value }
} }

View File

@ -11,6 +11,7 @@ import { PresentationFormHeader } from './PresentationFormHeader'
import { extractPresentationData } from '@/lib/presentationParsing' import { extractPresentationData } from '@/lib/presentationParsing'
import type { Article } from '@/types/nostr' import type { Article } from '@/types/nostr'
import { t } from '@/lib/i18n' import { t } from '@/lib/i18n'
import { userConfirm } from '@/lib/userConfirm'
interface AuthorPresentationDraft { interface AuthorPresentationDraft {
authorName: string authorName: string
@ -220,10 +221,10 @@ function PresentationForm({
<div className="flex-1"> <div className="flex-1">
<button <button
type="submit" type="submit"
disabled={loading || deleting} disabled={loading ?? deleting}
className="w-full px-4 py-2 bg-neon-cyan/20 hover:bg-neon-cyan/30 text-neon-cyan rounded-lg font-medium transition-all border border-neon-cyan/50 hover:shadow-glow-cyan disabled:opacity-50 disabled:cursor-not-allowed" className="w-full px-4 py-2 bg-neon-cyan/20 hover:bg-neon-cyan/30 text-neon-cyan rounded-lg font-medium transition-all border border-neon-cyan/50 hover:shadow-glow-cyan disabled:opacity-50 disabled:cursor-not-allowed"
> >
{loading || deleting {loading ?? deleting
? t('publish.publishing') ? t('publish.publishing')
: hasExistingPresentation : hasExistingPresentation
? t('presentation.update.button') ? t('presentation.update.button')
@ -244,12 +245,12 @@ function useAuthorPresentationState(pubkey: string | null, existingAuthorName?:
const [draft, setDraft] = useState<AuthorPresentationDraft>(() => { const [draft, setDraft] = useState<AuthorPresentationDraft>(() => {
if (existingPresentation) { if (existingPresentation) {
const { presentation, contentDescription } = extractPresentationData(existingPresentation) const { presentation, contentDescription } = extractPresentationData(existingPresentation)
const authorName = existingPresentation.title.replace(/^Présentation de /, '') || existingAuthorName || '' const authorName = existingPresentation.title.replace(/^Présentation de /, '') ?? existingAuthorName ?? ''
return { return {
authorName, authorName,
presentation, presentation,
contentDescription, contentDescription,
mainnetAddress: existingPresentation.mainnetAddress || '', mainnetAddress: existingPresentation.mainnetAddress ?? '',
...(existingPresentation.bannerUrl ? { pictureUrl: existingPresentation.bannerUrl } : {}), ...(existingPresentation.bannerUrl ? { pictureUrl: existingPresentation.bannerUrl } : {}),
} }
} }
@ -293,7 +294,7 @@ function useAuthorPresentationState(pubkey: string | null, existingAuthorName?:
return return
} }
if (!confirm(t('presentation.delete.confirm'))) { if (!userConfirm(t('presentation.delete.confirm'))) {
return return
} }
@ -472,7 +473,7 @@ function AuthorPresentationFormView({
handleSubmit={state.handleSubmit} handleSubmit={state.handleSubmit}
deleting={state.deleting} deleting={state.deleting}
handleDelete={state.handleDelete} handleDelete={state.handleDelete}
hasExistingPresentation={!!existingPresentation} hasExistingPresentation={existingPresentation !== null && existingPresentation !== undefined}
/> />
) )
} }

View File

@ -9,7 +9,7 @@ interface AuthorsListProps {
error: string | null error: string | null
} }
function LoadingState() { function LoadingState(): JSX.Element {
return ( return (
<div className="text-center py-12"> <div className="text-center py-12">
<p className="text-cyber-accent/70">{t('common.loading.authors')}</p> <p className="text-cyber-accent/70">{t('common.loading.authors')}</p>
@ -17,7 +17,7 @@ function LoadingState() {
) )
} }
function ErrorState({ message }: { message: string }) { function ErrorState({ message }: { message: string }): JSX.Element {
return ( return (
<div className="bg-red-900/20 border border-red-500/50 rounded-lg p-4 mb-4"> <div className="bg-red-900/20 border border-red-500/50 rounded-lg p-4 mb-4">
<p className="text-red-400">{message}</p> <p className="text-red-400">{message}</p>
@ -25,7 +25,7 @@ function ErrorState({ message }: { message: string }) {
) )
} }
function EmptyState({ hasAny }: { hasAny: boolean }) { function EmptyState({ hasAny }: { hasAny: boolean }): JSX.Element {
return ( return (
<div className="text-center py-12"> <div className="text-center py-12">
<p className="text-cyber-accent/70"> <p className="text-cyber-accent/70">
@ -35,7 +35,7 @@ function EmptyState({ hasAny }: { hasAny: boolean }) {
) )
} }
export function AuthorsList({ authors, allAuthors, loading, error }: AuthorsListProps) { export function AuthorsList({ authors, allAuthors, loading, error }: AuthorsListProps): JSX.Element {
if (loading) { if (loading) {
return <LoadingState /> return <LoadingState />
} }

View File

@ -17,7 +17,7 @@ export function CategorySelect({
onChange, onChange,
required = false, required = false,
helpText, helpText,
}: CategorySelectProps) { }: CategorySelectProps): JSX.Element {
return ( return (
<div> <div>
<label htmlFor={id} className="block text-sm font-medium text-gray-700 mb-1"> <label htmlFor={id} className="block text-sm font-medium text-gray-700 mb-1">

View File

@ -7,7 +7,7 @@ interface CategoryTabsProps {
onCategoryChange: (category: CategoryFilter) => void onCategoryChange: (category: CategoryFilter) => void
} }
export function CategoryTabs({ selectedCategory, onCategoryChange }: CategoryTabsProps) { export function CategoryTabs({ selectedCategory, onCategoryChange }: CategoryTabsProps): JSX.Element {
return ( return (
<div className="mb-6"> <div className="mb-6">
<div className="border-b border-neon-cyan/30"> <div className="border-b border-neon-cyan/30">

View File

@ -4,7 +4,7 @@ interface ClearButtonProps {
onClick: () => void onClick: () => void
} }
export function ClearButton({ onClick }: ClearButtonProps) { export function ClearButton({ onClick }: ClearButtonProps): JSX.Element {
return ( return (
<button <button
onClick={onClick} onClick={onClick}

View File

@ -21,11 +21,11 @@ function AuthorProfileLink({ presentation, profile }: { presentation: Article; p
// Title format: "Présentation de <name>" or just use profile name // Title format: "Présentation de <name>" or just use profile name
let authorName = presentation.title.replace(/^Présentation de /, '').trim() let authorName = presentation.title.replace(/^Présentation de /, '').trim()
if (!authorName || authorName === 'Présentation') { if (!authorName || authorName === 'Présentation') {
authorName = profile?.name || t('common.author') authorName = profile?.name ?? t('common.author')
} }
// Extract picture from presentation (bannerUrl or from JSON metadata) or profile // Extract picture from presentation (bannerUrl or from JSON metadata) or profile
const picture = presentation.bannerUrl || profile?.picture const picture = presentation.bannerUrl ?? profile?.picture
return ( return (
<Link <Link

View File

@ -161,8 +161,8 @@ export function ConnectButton() {
return ( return (
<> <>
<DisconnectedState <DisconnectedState
loading={loading || creatingAccount} loading={loading ?? creatingAccount}
error={error || createError} error={error ?? createError}
showUnlockModal={showUnlockModal} showUnlockModal={showUnlockModal}
setShowUnlockModal={setShowUnlockModal} setShowUnlockModal={setShowUnlockModal}
onCreateAccount={handleCreateAccount} onCreateAccount={handleCreateAccount}

View File

@ -15,7 +15,7 @@ export function ConnectedUserMenu({
profile, profile,
onDisconnect, onDisconnect,
loading, loading,
}: ConnectedUserMenuProps) { }: ConnectedUserMenuProps): JSX.Element {
const displayName = profile?.name ?? `${pubkey.slice(0, 8)}...` const displayName = profile?.name ?? `${pubkey.slice(0, 8)}...`
return ( return (

View File

@ -11,7 +11,7 @@ interface CreateAccountModalProps {
type Step = 'choose' | 'import' | 'recovery' type Step = 'choose' | 'import' | 'recovery'
async function createAccountWithKey(key?: string) { async function createAccountWithKey(key?: string) {
return await nostrAuthService.createAccount(key) return nostrAuthService.createAccount(key)
} }
async function handleAccountCreation( async function handleAccountCreation(

View File

@ -19,7 +19,7 @@ export function RecoveryPhraseDisplay({
recoveryPhrase: string[] recoveryPhrase: string[]
copied: boolean copied: boolean
onCopy: () => void onCopy: () => void
}) { }): JSX.Element {
return ( return (
<div className="bg-cyber-darker border border-neon-cyan/30 rounded-lg p-6 mb-6"> <div className="bg-cyber-darker border border-neon-cyan/30 rounded-lg p-6 mb-6">
<div className="grid grid-cols-2 gap-4 mb-4"> <div className="grid grid-cols-2 gap-4 mb-4">
@ -45,7 +45,7 @@ export function RecoveryPhraseDisplay({
) )
} }
export function PublicKeyDisplay({ npub }: { npub: string }) { export function PublicKeyDisplay({ npub }: { npub: string }): JSX.Element {
return ( return (
<div className="bg-neon-blue/10 border border-neon-blue/30 rounded-lg p-4 mb-6"> <div className="bg-neon-blue/10 border border-neon-blue/30 rounded-lg p-4 mb-6">
<p className="text-neon-blue font-semibold mb-2">{t('account.create.publicKey')}</p> <p className="text-neon-blue font-semibold mb-2">{t('account.create.publicKey')}</p>
@ -62,7 +62,7 @@ export function ImportKeyForm({
importKey: string importKey: string
setImportKey: (key: string) => void setImportKey: (key: string) => void
error: string | null error: string | null
}) { }): JSX.Element {
return ( return (
<> <>
<div className="mb-4"> <div className="mb-4">
@ -84,7 +84,7 @@ export function ImportKeyForm({
) )
} }
export function ImportStepButtons({ loading, onImport, onBack }: { loading: boolean; onImport: () => void; onBack: () => void }) { export function ImportStepButtons({ loading, onImport, onBack }: { loading: boolean; onImport: () => void; onBack: () => void }): JSX.Element {
return ( return (
<div className="flex gap-4"> <div className="flex gap-4">
<button <button
@ -116,7 +116,7 @@ export function ChooseStepButtons({
onGenerate: () => void onGenerate: () => void
onImport: () => void onImport: () => void
onClose: () => void onClose: () => void
}) { }): JSX.Element {
return ( return (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<button <button

View File

@ -10,10 +10,10 @@ export function RecoveryStep({
recoveryPhrase: string[] recoveryPhrase: string[]
npub: string npub: string
onContinue: () => void onContinue: () => void
}) { }): JSX.Element {
const [copied, setCopied] = useState(false) const [copied, setCopied] = useState(false)
const handleCopy = async () => { const handleCopy = async (): Promise<void> => {
if (recoveryPhrase.length > 0) { if (recoveryPhrase.length > 0) {
await navigator.clipboard.writeText(recoveryPhrase.join(' ')) await navigator.clipboard.writeText(recoveryPhrase.join(' '))
setCopied(true) setCopied(true)
@ -55,7 +55,7 @@ export function ImportStep({
error: string | null error: string | null
onImport: () => void onImport: () => void
onBack: () => void onBack: () => void
}) { }): JSX.Element {
return ( return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-cyber-dark border border-neon-cyan/30 rounded-lg p-6 max-w-md w-full mx-4 shadow-glow-cyan"> <div className="bg-cyber-dark border border-neon-cyan/30 rounded-lg p-6 max-w-md w-full mx-4 shadow-glow-cyan">
@ -79,7 +79,7 @@ export function ChooseStep({
onGenerate: () => void onGenerate: () => void
onImport: () => void onImport: () => void
onClose: () => void onClose: () => void
}) { }): JSX.Element {
return ( return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-cyber-dark border border-neon-cyan/30 rounded-lg p-6 max-w-md w-full mx-4 shadow-glow-cyan"> <div className="bg-cyber-dark border border-neon-cyan/30 rounded-lg p-6 max-w-md w-full mx-4 shadow-glow-cyan">

View File

@ -6,7 +6,7 @@ interface DocsContentProps {
loading: boolean loading: boolean
} }
export function DocsContent({ content, loading }: DocsContentProps) { export function DocsContent({ content, loading }: DocsContentProps): JSX.Element {
if (loading) { if (loading) {
return ( return (
<div className="text-center py-12"> <div className="text-center py-12">

View File

@ -7,7 +7,7 @@ interface DocsSidebarProps {
onSelectDoc: (docId: DocSection) => void onSelectDoc: (docId: DocSection) => void
} }
export function DocsSidebar({ docs, selectedDoc, onSelectDoc }: DocsSidebarProps) { export function DocsSidebar({ docs, selectedDoc, onSelectDoc }: DocsSidebarProps): JSX.Element {
return ( return (
<aside className="lg:w-64 flex-shrink-0"> <aside className="lg:w-64 flex-shrink-0">
<div className="bg-cyber-dark border border-neon-cyan/20 rounded-lg p-4 sticky top-4 backdrop-blur-sm"> <div className="bg-cyber-dark border border-neon-cyan/20 rounded-lg p-4 sticky top-4 backdrop-blur-sm">

View File

@ -1,7 +1,7 @@
import Link from 'next/link' import Link from 'next/link'
import { t } from '@/lib/i18n' import { t } from '@/lib/i18n'
export function Footer() { export function Footer(): JSX.Element {
return ( return (
<footer className="bg-cyber-dark border-t border-neon-cyan/30 mt-12"> <footer className="bg-cyber-dark border-t border-neon-cyan/30 mt-12">
<div className="max-w-4xl mx-auto px-4 py-6"> <div className="max-w-4xl mx-auto px-4 py-6">

View File

@ -6,7 +6,7 @@ interface FundingProgressBarProps {
progressPercent: number progressPercent: number
} }
function FundingProgressBar({ progressPercent }: FundingProgressBarProps) { function FundingProgressBar({ progressPercent }: FundingProgressBarProps): JSX.Element {
return ( return (
<div className="relative w-full h-4 bg-cyber-dark rounded-full overflow-hidden border border-neon-cyan/30"> <div className="relative w-full h-4 bg-cyber-dark rounded-full overflow-hidden border border-neon-cyan/30">
<div <div
@ -22,7 +22,7 @@ function FundingProgressBar({ progressPercent }: FundingProgressBarProps) {
) )
} }
function FundingStats({ stats }: { stats: ReturnType<typeof estimatePlatformFunds> }) { function FundingStats({ stats }: { stats: ReturnType<typeof estimatePlatformFunds> }): JSX.Element {
const progressPercent = Math.min(100, stats.progressPercent) const progressPercent = Math.min(100, stats.progressPercent)
return ( return (
<div className="space-y-4"> <div className="space-y-4">
@ -41,7 +41,7 @@ function FundingStats({ stats }: { stats: ReturnType<typeof estimatePlatformFund
) )
} }
export function FundingGauge() { export function FundingGauge(): JSX.Element {
const [stats, setStats] = useState(estimatePlatformFunds()) const [stats, setStats] = useState(estimatePlatformFunds())
const [certificationStats, setCertificationStats] = useState(estimatePlatformFunds()) const [certificationStats, setCertificationStats] = useState(estimatePlatformFunds())
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
@ -49,7 +49,7 @@ export function FundingGauge() {
useEffect(() => { useEffect(() => {
// In a real implementation, this would fetch actual data // In a real implementation, this would fetch actual data
// For now, we use the estimate // For now, we use the estimate
const loadStats = async () => { const loadStats = async (): Promise<void> => {
try { try {
const fundingStats = estimatePlatformFunds() const fundingStats = estimatePlatformFunds()
setStats(fundingStats) setStats(fundingStats)

View File

@ -2,6 +2,7 @@ import { useState, useEffect } from 'react'
import { configStorage } from '@/lib/configStorage' import { configStorage } from '@/lib/configStorage'
import type { Nip95Config } from '@/lib/configStorageTypes' import type { Nip95Config } from '@/lib/configStorageTypes'
import { t } from '@/lib/i18n' import { t } from '@/lib/i18n'
import { userConfirm } from '@/lib/userConfirm'
interface Nip95ConfigManagerProps { interface Nip95ConfigManagerProps {
onConfigChange?: () => void onConfigChange?: () => void
@ -97,7 +98,7 @@ export function Nip95ConfigManager({ onConfigChange }: Nip95ConfigManagerProps)
} }
async function handleRemoveApi(id: string) { async function handleRemoveApi(id: string) {
if (!confirm(t('settings.nip95.remove.confirm'))) { if (!userConfirm(t('settings.nip95.remove.confirm'))) {
return return
} }

View File

@ -10,7 +10,7 @@ export function NotificationPanelHeader({
unreadCount, unreadCount,
onMarkAllAsRead, onMarkAllAsRead,
onClose, onClose,
}: NotificationPanelHeaderProps) { }: NotificationPanelHeaderProps): JSX.Element {
return ( return (
<div className="flex items-center justify-between p-4 border-b border-gray-200"> <div className="flex items-center justify-between p-4 border-b border-gray-200">
<h3 className="text-lg font-semibold text-gray-900">{t('notification.title')}</h3> <h3 className="text-lg font-semibold text-gray-900">{t('notification.title')}</h3>

View File

@ -4,7 +4,7 @@ import { LanguageSelector } from './LanguageSelector'
import { t } from '@/lib/i18n' import { t } from '@/lib/i18n'
import { KeyIndicator } from './KeyIndicator' import { KeyIndicator } from './KeyIndicator'
function GitIcon() { function GitIcon(): JSX.Element {
return ( return (
<svg <svg
className="w-5 h-5" className="w-5 h-5"
@ -17,7 +17,7 @@ function GitIcon() {
) )
} }
export function PageHeader() { export function PageHeader(): JSX.Element {
return ( return (
<header className="bg-cyber-dark border-b border-neon-cyan/30 shadow-glow-cyan"> <header className="bg-cyber-dark border-b border-neon-cyan/30 shadow-glow-cyan">
<div className="max-w-4xl mx-auto px-4 py-4 flex justify-between items-center"> <div className="max-w-4xl mx-auto px-4 py-4 flex justify-between items-center">

View File

@ -11,14 +11,14 @@ interface PaymentModalProps {
onPaymentComplete: () => void onPaymentComplete: () => void
} }
function useInvoiceTimer(expiresAt?: number) { function useInvoiceTimer(expiresAt?: number): number | null {
const [timeRemaining, setTimeRemaining] = useState<number | null>(null) const [timeRemaining, setTimeRemaining] = useState<number | null>(null)
useEffect(() => { useEffect(() => {
if (!expiresAt) { if (!expiresAt) {
return return
} }
const updateTimeRemaining = () => { const updateTimeRemaining = (): void => {
const now = Math.floor(Date.now() / 1000) const now = Math.floor(Date.now() / 1000)
const remaining = expiresAt - now const remaining = expiresAt - now
setTimeRemaining(remaining > 0 ? remaining : 0) setTimeRemaining(remaining > 0 ? remaining : 0)
@ -39,8 +39,8 @@ function PaymentHeader({
amount: number amount: number
timeRemaining: number | null timeRemaining: number | null
onClose: () => void onClose: () => void
}) { }): JSX.Element {
const timeLabel = useMemo(() => { const timeLabel = useMemo((): string | null => {
if (timeRemaining === null) { if (timeRemaining === null) {
return null return null
} }
@ -69,7 +69,7 @@ function PaymentHeader({
) )
} }
function InvoiceDisplay({ invoiceText, paymentUrl }: { invoiceText: string; paymentUrl: string }) { function InvoiceDisplay({ invoiceText, paymentUrl }: { invoiceText: string; paymentUrl: string }): JSX.Element {
return ( return (
<div className="mb-4"> <div className="mb-4">
<p className="text-sm text-cyber-accent mb-2">{t('payment.modal.lightningInvoice')}</p> <p className="text-sm text-cyber-accent mb-2">{t('payment.modal.lightningInvoice')}</p>
@ -97,7 +97,7 @@ function PaymentActions({
copied: boolean copied: boolean
onCopy: () => Promise<void> onCopy: () => Promise<void>
onOpenWallet: () => void onOpenWallet: () => void
}) { }): JSX.Element {
return ( return (
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
@ -118,7 +118,7 @@ function PaymentActions({
) )
} }
function ExpiredNotice({ show }: { show: boolean }) { function ExpiredNotice({ show }: { show: boolean }): JSX.Element | null {
if (!show) { if (!show) {
return null return null
} }
@ -130,13 +130,20 @@ function ExpiredNotice({ show }: { show: boolean }) {
) )
} }
function usePaymentModalState(invoice: AlbyInvoice, onPaymentComplete: () => void) { function usePaymentModalState(invoice: AlbyInvoice, onPaymentComplete: () => void): {
copied: boolean
errorMessage: string | null
paymentUrl: string
timeRemaining: number | null
handleCopy: () => Promise<void>
handleOpenWallet: () => Promise<void>
} {
const [copied, setCopied] = useState(false) const [copied, setCopied] = useState(false)
const [errorMessage, setErrorMessage] = useState<string | null>(null) const [errorMessage, setErrorMessage] = useState<string | null>(null)
const paymentUrl = `lightning:${invoice.invoice}` const paymentUrl = `lightning:${invoice.invoice}`
const timeRemaining = useInvoiceTimer(invoice.expiresAt) const timeRemaining = useInvoiceTimer(invoice.expiresAt)
const handleCopy = useCallback(async () => { const handleCopy = useCallback(async (): Promise<void> => {
try { try {
await navigator.clipboard.writeText(invoice.invoice) await navigator.clipboard.writeText(invoice.invoice)
setCopied(true) setCopied(true)
@ -147,7 +154,7 @@ function usePaymentModalState(invoice: AlbyInvoice, onPaymentComplete: () => voi
} }
}, [invoice.invoice]) }, [invoice.invoice])
const handleOpenWallet = useCallback(async () => { const handleOpenWallet = useCallback(async (): Promise<void> => {
try { try {
const alby = getAlbyService() const alby = getAlbyService()
if (!isWebLNAvailable()) { if (!isWebLNAvailable()) {
@ -169,10 +176,10 @@ function usePaymentModalState(invoice: AlbyInvoice, onPaymentComplete: () => voi
return { copied, errorMessage, paymentUrl, timeRemaining, handleCopy, handleOpenWallet } return { copied, errorMessage, paymentUrl, timeRemaining, handleCopy, handleOpenWallet }
} }
export function PaymentModal({ invoice, onClose, onPaymentComplete }: PaymentModalProps) { export function PaymentModal({ invoice, onClose, onPaymentComplete }: PaymentModalProps): JSX.Element {
const { copied, errorMessage, paymentUrl, timeRemaining, handleCopy, handleOpenWallet } = const { copied, errorMessage, paymentUrl, timeRemaining, handleCopy, handleOpenWallet } =
usePaymentModalState(invoice, onPaymentComplete) usePaymentModalState(invoice, onPaymentComplete)
const handleOpenWalletSync = () => { const handleOpenWalletSync = (): void => {
void handleOpenWallet() void handleOpenWallet()
} }

View File

@ -9,7 +9,7 @@ interface SearchBarProps {
placeholder?: string placeholder?: string
} }
export function SearchBar({ value, onChange, placeholder }: SearchBarProps) { export function SearchBar({ value, onChange, placeholder }: SearchBarProps): JSX.Element {
const defaultPlaceholder = placeholder ?? t('search.placeholder') const defaultPlaceholder = placeholder ?? t('search.placeholder')
const [localValue, setLocalValue] = useState(value) const [localValue, setLocalValue] = useState(value)
@ -17,13 +17,13 @@ export function SearchBar({ value, onChange, placeholder }: SearchBarProps) {
setLocalValue(value) setLocalValue(value)
}, [value]) }, [value])
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
const newValue = e.target.value const newValue = e.target.value
setLocalValue(newValue) setLocalValue(newValue)
onChange(newValue) onChange(newValue)
} }
const handleClear = () => { const handleClear = (): void => {
setLocalValue('') setLocalValue('')
onChange('') onChange('')
} }

View File

@ -33,11 +33,11 @@ const ArticlesError = ({ message }: { message: string }) => (
) )
const EmptyState = ({ show }: { show: boolean }) => const EmptyState = ({ show }: { show: boolean }) =>
show ? ( (show ? (
<div className="text-center py-12"> <div className="text-center py-12">
<p className="text-gray-500">{t('common.empty.articles')}</p> <p className="text-gray-500">{t('common.empty.articles')}</p>
</div> </div>
) : null ) : null)
function ArticleActions({ function ArticleActions({
article, article,

View File

@ -11,7 +11,22 @@ export default [
}, },
js.configs.recommended, js.configs.recommended,
{ {
files: ['**/*.{js,jsx,ts,tsx}'], files: ['**/*.{js,jsx}'],
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
globals: {
console: 'readonly',
process: 'readonly',
module: 'readonly',
require: 'readonly',
__dirname: 'readonly',
__filename: 'readonly',
},
},
},
{
files: ['**/*.{ts,tsx}'],
languageOptions: { languageOptions: {
parser: typescriptParser, parser: typescriptParser,
parserOptions: { parserOptions: {

View File

@ -10,16 +10,26 @@ interface EditState {
articleId: string | null articleId: string | null
} }
export function useArticleEditing(authorPubkey: string | null) { export function useArticleEditing(authorPubkey: string | null): {
editingDraft: ArticleDraft | null
editingArticleId: string | null
loading: boolean
error: string | null
startEditing: (article: Article) => Promise<void>
cancelEditing: () => void
submitEdit: () => Promise<ArticleUpdateResult | null>
deleteArticle: (articleId: string) => Promise<boolean>
updateDraft: (draft: ArticleDraft | null) => void
} {
const [state, setState] = useState<EditState>({ draft: null, articleId: null }) const [state, setState] = useState<EditState>({ draft: null, articleId: null })
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const updateDraft = (draft: ArticleDraft | null) => { const updateDraft = (draft: ArticleDraft | null): void => {
setState((prev) => ({ ...prev, draft })) setState((prev) => ({ ...prev, draft }))
} }
const startEditing = async (article: Article) => { const startEditing = async (article: Article): Promise<void> => {
if (!authorPubkey) { if (!authorPubkey) {
setError('Connect your Nostr wallet to edit') setError('Connect your Nostr wallet to edit')
return return
@ -52,7 +62,7 @@ export function useArticleEditing(authorPubkey: string | null) {
} }
} }
const cancelEditing = () => { const cancelEditing = (): void => {
setState({ draft: null, articleId: null }) setState({ draft: null, articleId: null })
setError(null) setError(null)
} }

View File

@ -9,13 +9,20 @@ export function useArticlePayment(
pubkey: string | null, pubkey: string | null,
onUnlockSuccess?: () => void, onUnlockSuccess?: () => void,
connect?: () => Promise<void> connect?: () => Promise<void>
) { ): {
loading: boolean
error: string | null
paymentInvoice: AlbyInvoice | null
handleUnlock: () => Promise<void>
handlePaymentComplete: () => Promise<void>
handleCloseModal: () => void
} {
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const [paymentInvoice, setPaymentInvoice] = useState<AlbyInvoice | null>(null) const [paymentInvoice, setPaymentInvoice] = useState<AlbyInvoice | null>(null)
const [paymentHash, setPaymentHash] = useState<string | null>(null) const [paymentHash, setPaymentHash] = useState<string | null>(null)
const checkPaymentStatus = async (hash: string, userPubkey: string) => { const checkPaymentStatus = async (hash: string, userPubkey: string): Promise<void> => {
try { try {
const hasPaid = await paymentService.waitForArticlePayment( const hasPaid = await paymentService.waitForArticlePayment(
hash, hash,
@ -41,7 +48,7 @@ export function useArticlePayment(
} }
} }
const handleUnlock = async () => { const handleUnlock = async (): Promise<void> => {
if (!pubkey) { if (!pubkey) {
if (connect) { if (connect) {
setLoading(true) setLoading(true)
@ -80,13 +87,13 @@ export function useArticlePayment(
} }
} }
const handlePaymentComplete = async () => { const handlePaymentComplete = async (): Promise<void> => {
if (paymentHash && pubkey) { if (paymentHash && pubkey) {
await checkPaymentStatus(paymentHash, pubkey) await checkPaymentStatus(paymentHash, pubkey)
} }
} }
const handleCloseModal = () => { const handleCloseModal = (): void => {
setPaymentInvoice(null) setPaymentInvoice(null)
setPaymentHash(null) setPaymentHash(null)
} }

View File

@ -3,7 +3,12 @@ import { articlePublisher } from '@/lib/articlePublisher'
import { nostrService } from '@/lib/nostr' import { nostrService } from '@/lib/nostr'
import type { ArticleDraft } from '@/lib/articlePublisher' import type { ArticleDraft } from '@/lib/articlePublisher'
export function useArticlePublishing(pubkey: string | null) { export function useArticlePublishing(pubkey: string | null): {
loading: boolean
error: string | null
success: boolean
publishArticle: (draft: ArticleDraft) => Promise<string | null>
} {
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const [success, setSuccess] = useState(false) const [success, setSuccess] = useState(false)
@ -48,4 +53,3 @@ export function useArticlePublishing(pubkey: string | null) {
publishArticle, publishArticle,
} }
} }

View File

@ -5,7 +5,13 @@ import { applyFiltersAndSort } from '@/lib/articleFiltering'
import type { ArticleFilters } from '@/components/ArticleFilters' import type { ArticleFilters } from '@/components/ArticleFilters'
import { t } from '@/lib/i18n' import { t } from '@/lib/i18n'
export function useArticles(searchQuery: string = '', filters: ArticleFilters | null = null) { export function useArticles(searchQuery: string = '', filters: ArticleFilters | null = null): {
articles: Article[]
allArticles: Article[]
loading: boolean
error: string | null
loadArticleContent: (articleId: string, authorPubkey: string) => Promise<Article | null>
} {
const [articles, setArticles] = useState<Article[]>([]) const [articles, setArticles] = useState<Article[]>([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
@ -43,7 +49,7 @@ export function useArticles(searchQuery: string = '', filters: ArticleFilters |
} }
}, []) }, [])
const loadArticleContent = async (articleId: string, authorPubkey: string) => { const loadArticleContent = async (articleId: string, authorPubkey: string): Promise<Article | null> => {
try { try {
const article = await nostrService.getArticleById(articleId) const article = await nostrService.getArticleById(articleId)
if (article) { if (article) {
@ -52,9 +58,9 @@ export function useArticles(searchQuery: string = '', filters: ArticleFilters |
if (decryptedContent) { if (decryptedContent) {
setArticles((prev) => setArticles((prev) =>
prev.map((a) => prev.map((a) =>
a.id === articleId (a.id === articleId
? { ...a, content: decryptedContent, paid: true } ? { ...a, content: decryptedContent, paid: true }
: a : a)
) )
) )
} }

View File

@ -12,7 +12,14 @@ interface AuthorPresentationDraft {
pictureUrl?: string pictureUrl?: string
} }
export function useAuthorPresentation(pubkey: string | null) { export function useAuthorPresentation(pubkey: string | null): {
loading: boolean
error: string | null
success: boolean
publishPresentation: (draft: AuthorPresentationDraft) => Promise<void>
checkPresentationExists: () => Promise<Article | null>
deletePresentation: (articleId: string) => Promise<void>
} {
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const [success, setSuccess] = useState(false) const [success, setSuccess] = useState(false)
@ -85,7 +92,7 @@ export function useAuthorPresentation(pubkey: string | null) {
} }
try { try {
return await articlePublisher.getAuthorPresentation(pubkey) return articlePublisher.getAuthorPresentation(pubkey)
} catch (e) { } catch (e) {
console.error('Error checking presentation:', e) console.error('Error checking presentation:', e)
return null return null

View File

@ -22,7 +22,7 @@ export function useAuthorsProfiles(authorPubkeys: string[]): {
return return
} }
const loadProfiles = async () => { const loadProfiles = async (): Promise<void> => {
setLoading(true) setLoading(true)
const profilesMap = new Map<string, AuthorProfile>() const profilesMap = new Map<string, AuthorProfile>()

View File

@ -8,14 +8,19 @@ export interface DocLink {
file: string file: string
} }
export function useDocs(docs: DocLink[]) { export function useDocs(docs: DocLink[]): {
selectedDoc: DocSection
docContent: string
loading: boolean
loadDoc: (docId: DocSection) => Promise<void>
} {
const [selectedDoc, setSelectedDoc] = useState<DocSection>('user-guide') const [selectedDoc, setSelectedDoc] = useState<DocSection>('user-guide')
const [docContent, setDocContent] = useState<string>('') const [docContent, setDocContent] = useState<string>('')
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const loadDoc = async (docId: DocSection) => { const loadDoc = async (docId: DocSection): Promise<void> => {
const doc = docs.find((d) => d.id === docId) const doc = docs.find((d) => d.id === docId)
if (!doc) return if (!doc) {return}
setLoading(true) setLoading(true)
setSelectedDoc(docId) setSelectedDoc(docId)

View File

@ -1,12 +1,16 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { setLocale, getLocale, loadTranslations, t, type Locale } from '@/lib/i18n' import { setLocale, getLocale, loadTranslations, t, type Locale } from '@/lib/i18n'
export function useI18n(locale: Locale = 'fr') { export function useI18n(locale: Locale = 'fr'): {
loaded: boolean
locale: Locale
t: (key: string, params?: Record<string, string | number>) => string
} {
const [loaded, setLoaded] = useState(false) const [loaded, setLoaded] = useState(false)
const [currentLocale, setCurrentLocale] = useState<Locale>(getLocale()) const [currentLocale, setCurrentLocale] = useState<Locale>(getLocale())
useEffect(() => { useEffect(() => {
const load = async () => { const load = async (): Promise<void> => {
try { try {
// Get saved locale from IndexedDB or use provided locale // Get saved locale from IndexedDB or use provided locale
let savedLocale: Locale | null = null let savedLocale: Locale | null = null

View File

@ -2,7 +2,14 @@ import { useState, useEffect } from 'react'
import { nostrAuthService } from '@/lib/nostrAuth' import { nostrAuthService } from '@/lib/nostrAuth'
import type { NostrConnectState } from '@/types/nostr' import type { NostrConnectState } from '@/types/nostr'
export function useNostrAuth() { export function useNostrAuth(): NostrConnectState & {
loading: boolean
error: string | null
connect: () => Promise<void>
disconnect: () => Promise<void>
accountExists: boolean | null
isUnlocked: boolean
} {
const [state, setState] = useState<NostrConnectState>(nostrAuthService.getState()) const [state, setState] = useState<NostrConnectState>(nostrAuthService.getState())
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
@ -19,7 +26,7 @@ export function useNostrAuth() {
return unsubscribe return unsubscribe
}, []) }, [])
const connect = async () => { const connect = async (): Promise<void> => {
setLoading(true) setLoading(true)
setError(null) setError(null)
try { try {
@ -31,7 +38,7 @@ export function useNostrAuth() {
} }
} }
const disconnect = async () => { const disconnect = async (): Promise<void> => {
setLoading(true) setLoading(true)
try { try {
nostrAuthService.disconnect() nostrAuthService.disconnect()

View File

@ -2,7 +2,14 @@ import { useState, useEffect, useCallback } from 'react'
import { notificationService, loadStoredNotifications, saveNotifications, markNotificationAsRead, markAllAsRead, deleteNotification } from '@/lib/notifications' import { notificationService, loadStoredNotifications, saveNotifications, markNotificationAsRead, markAllAsRead, deleteNotification } from '@/lib/notifications'
import type { Notification } from '@/types/notifications' import type { Notification } from '@/types/notifications'
export function useNotifications(userPubkey: string | null) { export function useNotifications(userPubkey: string | null): {
notifications: Notification[]
unreadCount: number
loading: boolean
markAsRead: (notificationId: string) => void
markAllAsRead: () => void
deleteNotification: (notificationId: string) => void
} {
const [notifications, setNotifications] = useState<Notification[]>([]) const [notifications, setNotifications] = useState<Notification[]>([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
@ -14,7 +21,7 @@ export function useNotifications(userPubkey: string | null) {
return return
} }
const loadStored = async () => { const loadStored = async (): Promise<void> => {
const storedNotifications = await loadStoredNotifications(userPubkey) const storedNotifications = await loadStoredNotifications(userPubkey)
setNotifications(storedNotifications) setNotifications(storedNotifications)
} }
@ -55,23 +62,23 @@ export function useNotifications(userPubkey: string | null) {
const unreadCount = notifications.filter((n) => !n.read).length const unreadCount = notifications.filter((n) => !n.read).length
const markAsRead = useCallback( const markAsRead = useCallback(
(notificationId: string) => { (notificationId: string): void => {
if (!userPubkey) return if (!userPubkey) {return}
setNotifications((prev) => markNotificationAsRead(userPubkey, notificationId, prev)) setNotifications((prev) => markNotificationAsRead(userPubkey, notificationId, prev))
}, },
[userPubkey] [userPubkey]
) )
const markAllAsReadHandler = useCallback(() => { const markAllAsReadHandler = useCallback((): void => {
if (!userPubkey) return if (!userPubkey) {return}
setNotifications((prev) => markAllAsRead(userPubkey, prev)) setNotifications((prev) => markAllAsRead(userPubkey, prev))
}, [userPubkey]) }, [userPubkey])
const deleteNotificationHandler = useCallback( const deleteNotificationHandler = useCallback(
(notificationId: string) => { (notificationId: string): void => {
if (!userPubkey) return if (!userPubkey) {return}
setNotifications((prev) => deleteNotification(userPubkey, notificationId, prev)) setNotifications((prev) => deleteNotification(userPubkey, notificationId, prev))
}, },

View File

@ -11,7 +11,13 @@ export function useUserArticles(
userPubkey: string, userPubkey: string,
searchQuery: string = '', searchQuery: string = '',
filters: ArticleFilters | null = null filters: ArticleFilters | null = null
) { ): {
articles: Article[]
allArticles: Article[]
loading: boolean
error: string | null
loadArticleContent: (articleId: string, authorPubkey: string) => Promise<Article | null>
} {
const [articles, setArticles] = useState<Article[]>([]) const [articles, setArticles] = useState<Article[]>([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
@ -71,7 +77,7 @@ export function useUserArticles(
return applyFiltersAndSort(articles, searchQuery, effectiveFilters) return applyFiltersAndSort(articles, searchQuery, effectiveFilters)
}, [articles, searchQuery, filters]) }, [articles, searchQuery, filters])
const loadArticleContent = async (articleId: string, authorPubkey: string) => { const loadArticleContent = async (articleId: string, authorPubkey: string): Promise<Article | null> => {
try { try {
const article = await nostrService.getArticleById(articleId) const article = await nostrService.getArticleById(articleId)
if (article) { if (article) {
@ -80,9 +86,9 @@ export function useUserArticles(
if (decryptedContent) { if (decryptedContent) {
setArticles((prev) => setArticles((prev) =>
prev.map((a) => prev.map((a) =>
a.id === articleId (a.id === articleId
? { ...a, content: decryptedContent, paid: true } ? { ...a, content: decryptedContent, paid: true }
: a : a)
) )
) )
} }

View File

@ -57,7 +57,7 @@ export async function publishSeries(params: {
authorPrivateKey?: string authorPrivateKey?: string
}): Promise<Series> { }): Promise<Series> {
ensureKeys(params.authorPubkey, params.authorPrivateKey) ensureKeys(params.authorPubkey, params.authorPrivateKey)
const category = params.category const {category} = params
requireCategory(category) requireCategory(category)
const event = await buildSeriesEvent(params, category) const event = await buildSeriesEvent(params, category)
const published = await nostrService.publishEvent(event) const published = await nostrService.publishEvent(event)
@ -148,7 +148,7 @@ export async function publishReview(params: {
authorPrivateKey?: string authorPrivateKey?: string
}): Promise<Review> { }): Promise<Review> {
ensureKeys(params.reviewerPubkey, params.authorPrivateKey) ensureKeys(params.reviewerPubkey, params.authorPrivateKey)
const category = params.category const {category} = params
requireCategory(category) requireCategory(category)
const event = await buildReviewEvent(params, category) const event = await buildReviewEvent(params, category)
const published = await nostrService.publishEvent(event) const published = await nostrService.publishEvent(event)
@ -273,7 +273,7 @@ async function publishUpdate(
authorPubkey: string, authorPubkey: string,
originalArticleId: string originalArticleId: string
): Promise<ArticleUpdateResult> { ): Promise<ArticleUpdateResult> {
const category = draft.category const {category} = draft
requireCategory(category) requireCategory(category)
const presentationId = await ensurePresentation(authorPubkey) const presentationId = await ensurePresentation(authorPubkey)
const invoice = await createArticleInvoice(draft) const invoice = await createArticleInvoice(draft)
@ -302,7 +302,7 @@ export async function publishArticleUpdate(
): Promise<ArticleUpdateResult> { ): Promise<ArticleUpdateResult> {
try { try {
ensureKeys(authorPubkey, authorPrivateKey) ensureKeys(authorPubkey, authorPrivateKey)
return await publishUpdate(draft, authorPubkey, originalArticleId) return publishUpdate(draft, authorPubkey, originalArticleId)
} catch (error) { } catch (error) {
return updateFailure(originalArticleId, error instanceof Error ? error.message : 'Unknown error') return updateFailure(originalArticleId, error instanceof Error ? error.message : 'Unknown error')
} }

View File

@ -1,5 +1,4 @@
import { nostrService } from './nostr' import { nostrService } from './nostr'
import type { SimplePoolWithSub } from '@/types/nostr-tools-extended'
import type { AlbyInvoice } from '@/types/alby' import type { AlbyInvoice } from '@/types/alby'
import { getStoredPrivateContent, getStoredInvoice, removeStoredPrivateContent } from './articleStorage' import { getStoredPrivateContent, getStoredInvoice, removeStoredPrivateContent } from './articleStorage'
import { buildPresentationEvent, fetchAuthorPresentationFromPool, sendEncryptedContent } from './articlePublisherHelpers' import { buildPresentationEvent, fetchAuthorPresentationFromPool, sendEncryptedContent } from './articlePublisherHelpers'
@ -72,7 +71,7 @@ export class ArticlePublisher {
return buildFailure('Presentation not found') return buildFailure('Presentation not found')
} }
return await encryptAndPublish(draft, authorPubkey, validation.authorPrivateKeyForEncryption, validation.category, presentation.id) return encryptAndPublish(draft, authorPubkey, validation.authorPrivateKeyForEncryption, validation.category, presentation.id)
} catch (error) { } catch (error) {
console.error('Error publishing article:', error) console.error('Error publishing article:', error)
return buildFailure(error instanceof Error ? error.message : 'Unknown error') return buildFailure(error instanceof Error ? error.message : 'Unknown error')
@ -172,7 +171,7 @@ export class ArticlePublisher {
nostrService.setPrivateKey(authorPrivateKey) nostrService.setPrivateKey(authorPrivateKey)
// Extract author name from title (format: "Présentation de <name>") // Extract author name from title (format: "Présentation de <name>")
const authorName = draft.title.replace(/^Présentation de /, '').trim() || 'Auteur' const authorName = draft.title.replace(/^Présentation de /, '').trim() ?? 'Auteur'
// Build event with hash-based ID // Build event with hash-based ID
const eventTemplate = await buildPresentationEvent(draft, authorPubkey, authorName, 'sciencefiction') const eventTemplate = await buildPresentationEvent(draft, authorPubkey, authorName, 'sciencefiction')
@ -211,7 +210,7 @@ export class ArticlePublisher {
if (!pool) { if (!pool) {
return null return null
} }
return await fetchAuthorPresentationFromPool(pool as SimplePoolWithSub, pubkey) return fetchAuthorPresentationFromPool(pool, pubkey)
} catch (error) { } catch (error) {
console.error('Error getting author presentation:', error) console.error('Error getting author presentation:', error)
return null return null

View File

@ -123,7 +123,7 @@ export function parsePresentationEvent(event: Event): import('@/types/nostr').Au
if (!profileData) { if (!profileData) {
// Try invisible format (with zero-width characters) // Try invisible format (with zero-width characters)
const invisibleJsonMatch = event.content.match(/[\u200B\u200C]\[Metadata JSON\][\u200B\u200C]\n[\u200B\u200C](.+)[\u200B\u200C]$/s) const invisibleJsonMatch = event.content.match(/[\u200B\u200C]\[Metadata JSON\][\u200B\u200C]\n[\u200B\u200C](.+)[\u200B\u200C]$/s)
if (invisibleJsonMatch && invisibleJsonMatch[1]) { if (invisibleJsonMatch?.[1]) {
try { try {
// Remove zero-width characters from JSON // Remove zero-width characters from JSON
const cleanedJson = invisibleJsonMatch[1].replace(/[\u200B\u200C\u200D\u200E\u200F]/g, '').trim() const cleanedJson = invisibleJsonMatch[1].replace(/[\u200B\u200C\u200D\u200E\u200F]/g, '').trim()
@ -136,7 +136,7 @@ export function parsePresentationEvent(event: Event): import('@/types/nostr').Au
// Fallback to visible format in content // Fallback to visible format in content
if (!profileData) { if (!profileData) {
const jsonMatch = event.content.match(/\[Metadata JSON\]\n(.+)$/s) const jsonMatch = event.content.match(/\[Metadata JSON\]\n(.+)$/s)
if (jsonMatch && jsonMatch[1]) { if (jsonMatch?.[1]) {
try { try {
profileData = JSON.parse(jsonMatch[1].trim()) profileData = JSON.parse(jsonMatch[1].trim())
} catch (e) { } catch (e) {

View File

@ -92,7 +92,7 @@ export class ConfigStorage {
return this.getDefaultConfig() return this.getDefaultConfig()
} }
const db = this.db const {db} = this
return new Promise((resolve) => { return new Promise((resolve) => {
const transaction = db.transaction([STORE_NAME], 'readonly') const transaction = db.transaction([STORE_NAME], 'readonly')
const store = transaction.objectStore(STORE_NAME) const store = transaction.objectStore(STORE_NAME)
@ -131,7 +131,7 @@ export class ConfigStorage {
throw new Error('Database not initialized') throw new Error('Database not initialized')
} }
const db = this.db const {db} = this
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readwrite') const transaction = db.transaction([STORE_NAME], 'readwrite')
const store = transaction.objectStore(STORE_NAME) const store = transaction.objectStore(STORE_NAME)

View File

@ -94,14 +94,14 @@ export class KeyManagementService {
* Check if an account exists (encrypted key is stored) * Check if an account exists (encrypted key is stored)
*/ */
async accountExists(): Promise<boolean> { async accountExists(): Promise<boolean> {
return await accountExistsTwoLevel() return accountExistsTwoLevel()
} }
/** /**
* Get the public key and npub if account exists * Get the public key and npub if account exists
*/ */
async getPublicKeys(): Promise<{ publicKey: string; npub: string } | null> { async getPublicKeys(): Promise<{ publicKey: string; npub: string } | null> {
return await getPublicKeysTwoLevel() return getPublicKeysTwoLevel()
} }
/** /**

View File

@ -31,7 +31,7 @@ export async function removeAccountFlag(): Promise<void> {
} }
export async function getEncryptedKey(): Promise<EncryptedPayload | null> { export async function getEncryptedKey(): Promise<EncryptedPayload | null> {
return await storageService.get<EncryptedPayload>(KEY_STORAGE_KEY, 'nostr_key_storage') return storageService.get<EncryptedPayload>(KEY_STORAGE_KEY, 'nostr_key_storage')
} }
export async function setEncryptedKey(encryptedNsec: EncryptedPayload): Promise<void> { export async function setEncryptedKey(encryptedNsec: EncryptedPayload): Promise<void> {

View File

@ -19,7 +19,7 @@ const PBKDF2_HASH = 'SHA-256'
* Generate a random KEK (Key Encryption Key) * Generate a random KEK (Key Encryption Key)
*/ */
async function generateKEK(): Promise<CryptoKey> { async function generateKEK(): Promise<CryptoKey> {
return await crypto.subtle.generateKey( return crypto.subtle.generateKey(
{ name: 'AES-GCM', length: 256 }, { name: 'AES-GCM', length: 256 },
true, // extractable true, // extractable
['encrypt', 'decrypt'] ['encrypt', 'decrypt']
@ -81,7 +81,7 @@ async function importKEK(keyBytes: Uint8Array): Promise<CryptoKey> {
const buffer = new ArrayBuffer(keyBytes.length) const buffer = new ArrayBuffer(keyBytes.length)
const view = new Uint8Array(buffer) const view = new Uint8Array(buffer)
view.set(keyBytes) view.set(keyBytes)
return await crypto.subtle.importKey( return crypto.subtle.importKey(
'raw', 'raw',
buffer, buffer,
{ name: 'AES-GCM' }, { name: 'AES-GCM' },
@ -160,9 +160,9 @@ async function decryptKEK(encryptedKEK: EncryptedPayload, recoveryPhrase: string
const hexString = decoder.decode(decrypted) const hexString = decoder.decode(decrypted)
// Convert hex string back to bytes // Convert hex string back to bytes
const kekBytes = new Uint8Array(hexString.match(/.{1,2}/g)?.map(byte => parseInt(byte, 16)) || []) const kekBytes = new Uint8Array(hexString.match(/.{1,2}/g)?.map(byte => parseInt(byte, 16)) ?? [])
return await importKEK(kekBytes) return importKEK(kekBytes)
} }
/** /**
@ -247,7 +247,7 @@ async function storeEncryptedKEK(encryptedKEK: EncryptedPayload): Promise<void>
} }
type PasswordCredentialConstructorType = new (data: PasswordCredentialData) => Credential & { id: string; password: string } type PasswordCredentialConstructorType = new (data: PasswordCredentialData) => Credential & { id: string; password: string }
const PasswordCredentialConstructor = (window as unknown as { PasswordCredential?: PasswordCredentialConstructorType }).PasswordCredential const PasswordCredentialConstructor = (window as unknown as { PasswordCredential?: PasswordCredentialConstructorType }).PasswordCredential
if (!PasswordCredentialConstructor || !navigator.credentials || !navigator.credentials.store) { if (!PasswordCredentialConstructor || !navigator.credentials?.store) {
throw new Error('PasswordCredential API not available') throw new Error('PasswordCredential API not available')
} }
@ -265,7 +265,7 @@ async function storeEncryptedKEK(encryptedKEK: EncryptedPayload): Promise<void>
id: 'nostr_kek', id: 'nostr_kek',
name: 'Nostr KEK', name: 'Nostr KEK',
password: JSON.stringify(encryptedKEK), password: JSON.stringify(encryptedKEK),
iconURL: window.location.origin + '/favicon.ico', iconURL: `${window.location.origin }/favicon.ico`,
}) })
await navigator.credentials.store(credential) await navigator.credentials.store(credential)
@ -275,7 +275,7 @@ async function storeEncryptedKEK(encryptedKEK: EncryptedPayload): Promise<void>
* Retrieve encrypted KEK from Credentials API * Retrieve encrypted KEK from Credentials API
*/ */
async function getEncryptedKEK(): Promise<EncryptedPayload | null> { async function getEncryptedKEK(): Promise<EncryptedPayload | null> {
if (typeof window === 'undefined' || !navigator.credentials || !navigator.credentials.get) { if (typeof window === 'undefined' || !navigator.credentials?.get) {
return null return null
} }
@ -423,7 +423,7 @@ export async function accountExistsTwoLevel(): Promise<boolean> {
export async function getPublicKeysTwoLevel(): Promise<{ publicKey: string; npub: string } | null> { export async function getPublicKeysTwoLevel(): Promise<{ publicKey: string; npub: string } | null> {
try { try {
const { storageService } = await import('./storage/indexedDB') const { storageService } = await import('./storage/indexedDB')
return await storageService.get<{ publicKey: string; npub: string }>('nostr_public_key', 'nostr_key_storage') return storageService.get<{ publicKey: string; npub: string }>('nostr_public_key', 'nostr_key_storage')
} catch { } catch {
return null return null
} }

View File

@ -18,7 +18,7 @@ export class MempoolSpaceService {
* Fetch transaction from mempool.space * Fetch transaction from mempool.space
*/ */
async getTransaction(txid: string): Promise<MempoolTransaction | null> { async getTransaction(txid: string): Promise<MempoolTransaction | null> {
return await getTransaction(txid) return getTransaction(txid)
} }
/** /**
@ -29,7 +29,7 @@ export class MempoolSpaceService {
txid: string, txid: string,
authorMainnetAddress: string authorMainnetAddress: string
): Promise<TransactionVerificationResult> { ): Promise<TransactionVerificationResult> {
return await verifySponsoringTransaction(txid, authorMainnetAddress) return verifySponsoringTransaction(txid, authorMainnetAddress)
} }
/** /**
@ -41,7 +41,7 @@ export class MempoolSpaceService {
timeout: number = 600000, // 10 minutes timeout: number = 600000, // 10 minutes
interval: number = 10000 // 10 seconds interval: number = 10000 // 10 seconds
): Promise<TransactionVerificationResult | null> { ): Promise<TransactionVerificationResult | null> {
return await waitForConfirmation(txid, timeout, interval) return waitForConfirmation(txid, timeout, interval)
} }
} }

View File

@ -78,7 +78,7 @@ export async function validateTransactionOutputs(
) )
const valid = Boolean(authorOutput && platformOutput) const valid = Boolean(authorOutput && platformOutput)
const confirmed = transaction.status.confirmed const {confirmed} = transaction.status
const confirmations = confirmed && transaction.status.block_height const confirmations = confirmed && transaction.status.block_height
? await getConfirmations(transaction.status.block_height) ? await getConfirmations(transaction.status.block_height)
: 0 : 0

View File

@ -107,7 +107,7 @@ export type ExtractedObject =
*/ */
function extractMetadataJsonFromTag(event: { tags: string[][] }): Record<string, unknown> | null { function extractMetadataJsonFromTag(event: { tags: string[][] }): Record<string, unknown> | null {
const jsonTag = event.tags.find((tag) => tag[0] === 'json') const jsonTag = event.tags.find((tag) => tag[0] === 'json')
if (jsonTag && jsonTag[1]) { if (jsonTag?.[1]) {
try { try {
return JSON.parse(jsonTag[1]) return JSON.parse(jsonTag[1])
} catch (e) { } catch (e) {
@ -121,7 +121,7 @@ function extractMetadataJsonFromTag(event: { tags: string[][] }): Record<string,
function extractMetadataJson(content: string): Record<string, unknown> | null { function extractMetadataJson(content: string): Record<string, unknown> | null {
// Try invisible format first (with zero-width characters) - for backward compatibility // Try invisible format first (with zero-width characters) - for backward compatibility
const invisibleJsonMatch = content.match(/[\u200B\u200C]\[Metadata JSON\][\u200B\u200C]\n[\u200B\u200C](.+)[\u200B\u200C]$/s) const invisibleJsonMatch = content.match(/[\u200B\u200C]\[Metadata JSON\][\u200B\u200C]\n[\u200B\u200C](.+)[\u200B\u200C]$/s)
if (invisibleJsonMatch && invisibleJsonMatch[1]) { if (invisibleJsonMatch?.[1]) {
try { try {
// Remove zero-width characters from JSON // Remove zero-width characters from JSON
const cleanedJson = invisibleJsonMatch[1].replace(/[\u200B\u200C\u200D\u200E\u200F]/g, '').trim() const cleanedJson = invisibleJsonMatch[1].replace(/[\u200B\u200C\u200D\u200E\u200F]/g, '').trim()
@ -133,7 +133,7 @@ function extractMetadataJson(content: string): Record<string, unknown> | null {
// Fallback to visible format (for backward compatibility) // Fallback to visible format (for backward compatibility)
const jsonMatch = content.match(/\[Metadata JSON\]\n(.+)$/s) const jsonMatch = content.match(/\[Metadata JSON\]\n(.+)$/s)
if (jsonMatch && jsonMatch[1]) { if (jsonMatch?.[1]) {
try { try {
return JSON.parse(jsonMatch[1].trim()) return JSON.parse(jsonMatch[1].trim())
} catch (e) { } catch (e) {
@ -161,7 +161,7 @@ export async function extractAuthorFromEvent(event: Event): Promise<ExtractedAut
metadata = extractMetadataJson(event.content) metadata = extractMetadataJson(event.content)
} }
if (metadata && metadata.type === 'author') { if (metadata?.type === 'author') {
const authorData = { const authorData = {
pubkey: (metadata.pubkey as string) ?? event.pubkey, pubkey: (metadata.pubkey as string) ?? event.pubkey,
authorName: (metadata.authorName as string) ?? '', authorName: (metadata.authorName as string) ?? '',
@ -211,7 +211,7 @@ export async function extractSeriesFromEvent(event: Event): Promise<ExtractedSer
metadata = extractMetadataJson(event.content) metadata = extractMetadataJson(event.content)
} }
if (metadata && metadata.type === 'series') { if (metadata?.type === 'series') {
const seriesData = { const seriesData = {
pubkey: (metadata.pubkey as string) ?? event.pubkey, pubkey: (metadata.pubkey as string) ?? event.pubkey,
title: (metadata.title as string) ?? (tags.title as string) ?? '', title: (metadata.title as string) ?? (tags.title as string) ?? '',
@ -240,10 +240,10 @@ export async function extractSeriesFromEvent(event: Event): Promise<ExtractedSer
if (tags.title && tags.description) { if (tags.title && tags.description) {
const seriesData = { const seriesData = {
pubkey: event.pubkey, pubkey: event.pubkey,
title: tags.title as string, title: tags.title,
description: tags.description as string, description: tags.description,
preview: (tags.preview as string) ?? event.content.substring(0, 200), preview: (tags.preview as string) ?? event.content.substring(0, 200),
coverUrl: tags.coverUrl as string | undefined, coverUrl: tags.coverUrl,
category: tags.category ?? 'sciencefiction', category: tags.category ?? 'sciencefiction',
} }
@ -282,7 +282,7 @@ export async function extractPublicationFromEvent(event: Event): Promise<Extract
metadata = extractMetadataJson(event.content) metadata = extractMetadataJson(event.content)
} }
if (metadata && metadata.type === 'publication') { if (metadata?.type === 'publication') {
const publicationData = { const publicationData = {
pubkey: (metadata.pubkey as string) ?? event.pubkey, pubkey: (metadata.pubkey as string) ?? event.pubkey,
title: (metadata.title as string) ?? (tags.title as string) ?? '', title: (metadata.title as string) ?? (tags.title as string) ?? '',
@ -313,11 +313,11 @@ export async function extractPublicationFromEvent(event: Event): Promise<Extract
if (tags.title) { if (tags.title) {
const publicationData = { const publicationData = {
pubkey: event.pubkey, pubkey: event.pubkey,
title: tags.title as string, title: tags.title,
preview: (tags.preview as string) ?? event.content.substring(0, 200), preview: (tags.preview as string) ?? event.content.substring(0, 200),
category: tags.category ?? 'sciencefiction', category: tags.category ?? 'sciencefiction',
seriesId: tags.seriesId as string | undefined, seriesId: tags.seriesId,
bannerUrl: tags.bannerUrl as string | undefined, bannerUrl: tags.bannerUrl,
zapAmount: tags.zapAmount ?? 800, zapAmount: tags.zapAmount ?? 800,
} }
@ -357,7 +357,7 @@ export async function extractReviewFromEvent(event: Event): Promise<ExtractedRev
metadata = extractMetadataJson(event.content) metadata = extractMetadataJson(event.content)
} }
if (metadata && metadata.type === 'review') { if (metadata?.type === 'review') {
const reviewData = { const reviewData = {
pubkey: (metadata.pubkey as string) ?? event.pubkey, pubkey: (metadata.pubkey as string) ?? event.pubkey,
articleId: (metadata.articleId as string) ?? (tags.articleId as string) ?? '', articleId: (metadata.articleId as string) ?? (tags.articleId as string) ?? '',
@ -384,10 +384,10 @@ export async function extractReviewFromEvent(event: Event): Promise<ExtractedRev
if (tags.articleId && tags.reviewerPubkey) { if (tags.articleId && tags.reviewerPubkey) {
const reviewData = { const reviewData = {
pubkey: event.pubkey, pubkey: event.pubkey,
articleId: tags.articleId as string, articleId: tags.articleId,
reviewerPubkey: tags.reviewerPubkey as string, reviewerPubkey: tags.reviewerPubkey,
content: event.content, content: event.content,
title: tags.title as string | undefined, title: tags.title,
} }
const id = await generateReviewHashId({ const id = await generateReviewHashId({
@ -570,25 +570,25 @@ export async function extractObjectsFromEvent(event: Event): Promise<ExtractedOb
// Try to extract each type // Try to extract each type
const author = await extractAuthorFromEvent(event) const author = await extractAuthorFromEvent(event)
if (author) results.push(author) if (author) {results.push(author)}
const series = await extractSeriesFromEvent(event) const series = await extractSeriesFromEvent(event)
if (series) results.push(series) if (series) {results.push(series)}
const publication = await extractPublicationFromEvent(event) const publication = await extractPublicationFromEvent(event)
if (publication) results.push(publication) if (publication) {results.push(publication)}
const review = await extractReviewFromEvent(event) const review = await extractReviewFromEvent(event)
if (review) results.push(review) if (review) {results.push(review)}
const purchase = await extractPurchaseFromEvent(event) const purchase = await extractPurchaseFromEvent(event)
if (purchase) results.push(purchase) if (purchase) {results.push(purchase)}
const reviewTip = await extractReviewTipFromEvent(event) const reviewTip = await extractReviewTipFromEvent(event)
if (reviewTip) results.push(reviewTip) if (reviewTip) {results.push(reviewTip)}
const sponsoring = await extractSponsoringFromEvent(event) const sponsoring = await extractSponsoringFromEvent(event)
if (sponsoring) results.push(sponsoring) if (sponsoring) {results.push(sponsoring)}
return results return results
} }

View File

@ -163,7 +163,7 @@ export async function uploadNip95Media(file: File): Promise<MediaRef> {
// Always use proxy to avoid CORS, 405, and name resolution issues // Always use proxy to avoid CORS, 405, and name resolution issues
// Pass endpoint and auth token as query parameters to proxy // Pass endpoint and auth token as query parameters to proxy
const proxyUrlParams = new URLSearchParams({ const proxyUrlParams = new URLSearchParams({
endpoint: endpoint, endpoint,
}) })
if (authToken) { if (authToken) {
proxyUrlParams.set('auth', authToken) proxyUrlParams.set('auth', authToken)

View File

@ -51,9 +51,9 @@ export async function generateNip98Token(method: string, url: string, payloadHas
const eventTemplate: EventTemplate & { pubkey: string } = { const eventTemplate: EventTemplate & { pubkey: string } = {
kind: 27235, // NIP-98 kind for HTTP auth kind: 27235, // NIP-98 kind for HTTP auth
created_at: Math.floor(Date.now() / 1000), created_at: Math.floor(Date.now() / 1000),
tags: tags, tags,
content: '', content: '',
pubkey: pubkey, pubkey,
} }
// Sign the event directly with the private key (no plugin needed) // Sign the event directly with the private key (no plugin needed)
@ -75,5 +75,5 @@ export async function generateNip98Token(method: string, url: string, payloadHas
export function isNip98Available(): boolean { export function isNip98Available(): boolean {
const pubkey = nostrService.getPublicKey() const pubkey = nostrService.getPublicKey()
const isUnlocked = nostrAuthService.isUnlocked() const isUnlocked = nostrAuthService.isUnlocked()
return !!pubkey && isUnlocked return Boolean(pubkey) && isUnlocked
} }

View File

@ -178,7 +178,7 @@ class NostrService {
if (!this.privateKey || !this.pool || !this.publicKey) { if (!this.privateKey || !this.pool || !this.publicKey) {
return null return null
} }
return await getDecryptionKey(this.pool, eventId, authorPubkey, this.privateKey, this.publicKey) return getDecryptionKey(this.pool, eventId, authorPubkey, this.privateKey, this.publicKey)
} }
async getDecryptedArticleContent(eventId: string, authorPubkey: string): Promise<string | null> { async getDecryptedArticleContent(eventId: string, authorPubkey: string): Promise<string | null> {
@ -199,7 +199,7 @@ class NostrService {
return null return null
} }
return await decryptArticleContentWithKey(event.content, decryptionKey) return decryptArticleContentWithKey(event.content, decryptionKey)
} catch (error) { } catch (error) {
console.error('Error decrypting article content', { console.error('Error decrypting article content', {
eventId, eventId,

View File

@ -13,7 +13,7 @@ export function parseArticleFromEvent(event: Event): Article | null {
if (tags.type !== 'publication') { if (tags.type !== 'publication') {
return null return null
} }
const { previewContent } = getPreviewContent(event.content, tags.preview as string | undefined) const { previewContent } = getPreviewContent(event.content, tags.preview)
return buildArticle(event, tags, previewContent) return buildArticle(event, tags, previewContent)
} catch (e) { } catch (e) {
console.error('Error parsing article:', e) console.error('Error parsing article:', e)
@ -36,11 +36,11 @@ export function parseSeriesFromEvent(event: Event): Series | null {
const series: Series = { const series: Series = {
id: tags.id ?? event.id, id: tags.id ?? event.id,
pubkey: event.pubkey, pubkey: event.pubkey,
title: tags.title as string, title: tags.title,
description: tags.description as string, description: tags.description,
preview: (tags.preview as string | undefined) ?? event.content.substring(0, 200), preview: (tags.preview) ?? event.content.substring(0, 200),
category, category,
...(tags.coverUrl ? { coverUrl: tags.coverUrl as string } : {}), ...(tags.coverUrl ? { coverUrl: tags.coverUrl } : {}),
} }
series.kindType = 'series' series.kindType = 'series'
return series return series
@ -57,8 +57,8 @@ export function parseReviewFromEvent(event: Event): Review | null {
if (tags.type !== 'quote') { if (tags.type !== 'quote') {
return null return null
} }
const articleId = tags.articleId as string | undefined const {articleId} = tags
const reviewer = tags.reviewerPubkey as string | undefined const reviewer = tags.reviewerPubkey
if (!articleId || !reviewer) { if (!articleId || !reviewer) {
return null return null
} }
@ -72,7 +72,7 @@ export function parseReviewFromEvent(event: Event): Review | null {
reviewerPubkey: reviewer, reviewerPubkey: reviewer,
content: event.content, content: event.content,
createdAt: event.created_at, createdAt: event.created_at,
...(tags.title ? { title: tags.title as string } : {}), ...(tags.title ? { title: tags.title } : {}),
...(rewardedTag ? { rewarded: true } : {}), ...(rewardedTag ? { rewarded: true } : {}),
...(rewardAmountTag ? { rewardAmount: parseInt(rewardAmountTag[1] ?? '0', 10) } : {}), ...(rewardAmountTag ? { rewardAmount: parseInt(rewardAmountTag[1] ?? '0', 10) } : {}),
} }

View File

@ -17,7 +17,7 @@ function createPrivateMessageFilters(eventId: string, publicKey: string, authorP
function decryptContent(privateKey: string, event: Event): Promise<string | null> { function decryptContent(privateKey: string, event: Event): Promise<string | null> {
return Promise.resolve(nip04.decrypt(privateKey, event.pubkey, event.content)).then((decrypted) => return Promise.resolve(nip04.decrypt(privateKey, event.pubkey, event.content)).then((decrypted) =>
decrypted ? decrypted : null (decrypted ? decrypted : null)
) )
} }

View File

@ -67,7 +67,7 @@ export class NostrRemoteSigner {
*/ */
isAvailable(): boolean { isAvailable(): boolean {
const state = nostrAuthService.getState() const state = nostrAuthService.getState()
return state.connected && !!state.pubkey return state.connected && Boolean(state.pubkey)
} }
/** /**

View File

@ -87,13 +87,13 @@ export function buildTags(tags: AuthorTags | SeriesTags | PublicationTags | Quot
const result = buildBaseTags(tags) const result = buildBaseTags(tags)
if (tags.type === 'author') { if (tags.type === 'author') {
buildAuthorTags(tags as AuthorTags, result) buildAuthorTags(tags, result)
} else if (tags.type === 'series') { } else if (tags.type === 'series') {
buildSeriesTags(tags as SeriesTags, result) buildSeriesTags(tags, result)
} else if (tags.type === 'publication') { } else if (tags.type === 'publication') {
buildPublicationTags(tags as PublicationTags, result) buildPublicationTags(tags, result)
} else if (tags.type === 'quote') { } else if (tags.type === 'quote') {
buildQuoteTags(tags as QuoteTags, result) buildQuoteTags(tags, result)
} }
return result return result

View File

@ -144,7 +144,7 @@ export function markNotificationAsRead(
notifications: Notification[] notifications: Notification[]
): Notification[] { ): Notification[] {
const updated = notifications.map((n) => const updated = notifications.map((n) =>
n.id === notificationId ? { ...n, read: true } : n (n.id === notificationId ? { ...n, read: true } : n)
) )
saveNotifications(userPubkey, updated) saveNotifications(userPubkey, updated)
return updated return updated

View File

@ -109,7 +109,7 @@ class ObjectCacheService {
const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result
if (cursor) { if (cursor) {
const obj = cursor.value as CachedObject const obj = cursor.value as CachedObject
if (obj && obj.hashId === hashId && !obj.hidden) { if (obj?.hashId === hashId && !obj.hidden) {
objects.push(obj) objects.push(obj)
} }
cursor.continue() cursor.continue()
@ -149,7 +149,7 @@ class ObjectCacheService {
const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result
if (cursor) { if (cursor) {
const obj = cursor.value as CachedObject const obj = cursor.value as CachedObject
if (obj && obj.event.pubkey === pubkey && !obj.hidden) { if (obj?.event.pubkey === pubkey && !obj.hidden) {
objects.push(obj) objects.push(obj)
} }
cursor.continue() cursor.continue()

View File

@ -49,7 +49,7 @@ export async function waitForArticlePayment(
const interval = 2000 const interval = 2000
const deadline = Date.now() + timeout const deadline = Date.now() + timeout
try { try {
return await pollPaymentUntilDeadline(articleId, articlePubkey, amount, recipientPubkey, interval, deadline) return pollPaymentUntilDeadline(articleId, articlePubkey, amount, recipientPubkey, interval, deadline)
} catch (error) { } catch (error) {
console.error('Wait for payment error:', error) console.error('Wait for payment error:', error)
return false return false

View File

@ -116,7 +116,7 @@ export function logPaymentResult(
if (result.success && result.messageEventId) { if (result.success && result.messageEventId) {
logPaymentSuccess(articleId, recipientPubkey, amount, result.messageEventId, result.verified ?? false) logPaymentSuccess(articleId, recipientPubkey, amount, result.messageEventId, result.verified ?? false)
return true return true
} else { }
console.error('Failed to send private content, but payment was confirmed', { console.error('Failed to send private content, but payment was confirmed', {
articleId, articleId,
recipientPubkey, recipientPubkey,
@ -124,5 +124,5 @@ export function logPaymentResult(
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}) })
return false return false
}
} }

View File

@ -40,7 +40,7 @@ export class PlatformTrackingService {
return null return null
} }
return { pool: pool as SimplePoolWithSub, authorPubkey } return { pool, authorPubkey }
} }
/** /**

View File

@ -15,7 +15,7 @@ export function extractPresentationData(presentation: Article): {
presentation: string presentation: string
contentDescription: string contentDescription: string
} { } {
const content = presentation.content const {content} = presentation
// Try new format first // Try new format first
const newFormatMatch = content.match(/Présentation personnelle : (.+?)(?:\nDescription de votre contenu :|$)/s) const newFormatMatch = content.match(/Présentation personnelle : (.+?)(?:\nDescription de votre contenu :|$)/s)

View File

@ -6,7 +6,7 @@ export async function transferReviewerPortionIfAvailable(
request: ReviewRewardRequest, request: ReviewRewardRequest,
split: { total: number; reviewer: number; platform: number } split: { total: number; reviewer: number; platform: number }
): Promise<void> { ): Promise<void> {
let reviewerLightningAddress: string | undefined = request.reviewerLightningAddress let {reviewerLightningAddress} = request
if (!reviewerLightningAddress) { if (!reviewerLightningAddress) {
const address = await lightningAddressService.getLightningAddress(request.reviewerPubkey) const address = await lightningAddressService.getLightningAddress(request.reviewerPubkey)
reviewerLightningAddress = address ?? undefined reviewerLightningAddress = address ?? undefined

View File

@ -38,7 +38,7 @@ function subscribeToPresentation(pool: import('nostr-tools').SimplePool, pubkey:
if (tags.type !== 'author') { if (tags.type !== 'author') {
return return
} }
const total = (tags.totalSponsoring as number | undefined) ?? 0 const total = (tags.totalSponsoring) ?? 0
finalize(total) finalize(total)
}) })

View File

@ -2,7 +2,6 @@ import { Event, EventTemplate, finalizeEvent } from 'nostr-tools'
import { hexToBytes } from 'nostr-tools/utils' import { hexToBytes } from 'nostr-tools/utils'
import { nostrService } from './nostr' import { nostrService } from './nostr'
import { PLATFORM_NPUB } from './platformConfig' import { PLATFORM_NPUB } from './platformConfig'
import type { SimplePoolWithSub } from '@/types/nostr-tools-extended'
import { getPrimaryRelaySync } from './config' import { getPrimaryRelaySync } from './config'
export interface SponsoringTracking { export interface SponsoringTracking {
@ -75,7 +74,7 @@ export class SponsoringTrackingService {
if (!pool) { if (!pool) {
throw new Error('Pool not initialized') throw new Error('Pool not initialized')
} }
const poolWithSub = pool as SimplePoolWithSub const poolWithSub = pool
const relayUrl = getPrimaryRelaySync() const relayUrl = getPrimaryRelaySync()
const pubs = poolWithSub.publish([relayUrl], event) const pubs = poolWithSub.publish([relayUrl], event)
await Promise.all(pubs) await Promise.all(pubs)

View File

@ -91,7 +91,7 @@ export class IndexedDBStorage {
...(expiresIn ? { expiresAt: now + expiresIn } : {}), ...(expiresIn ? { expiresAt: now + expiresIn } : {}),
} }
const db = this.db const {db} = this
if (!db) { if (!db) {
throw new Error('Database not initialized') throw new Error('Database not initialized')
} }
@ -129,7 +129,7 @@ export class IndexedDBStorage {
} }
private readValue<T>(key: string, secret: string): Promise<T | null> { private readValue<T>(key: string, secret: string): Promise<T | null> {
const db = this.db const {db} = this
if (!db) { if (!db) {
throw new Error('Database not initialized') throw new Error('Database not initialized')
} }
@ -176,7 +176,7 @@ export class IndexedDBStorage {
throw new Error('Database not initialized') throw new Error('Database not initialized')
} }
const db = this.db const {db} = this
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readwrite') const transaction = db.transaction([STORE_NAME], 'readwrite')
@ -203,7 +203,7 @@ export class IndexedDBStorage {
throw new Error('Database not initialized') throw new Error('Database not initialized')
} }
const db = this.db const {db} = this
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readwrite') const transaction = db.transaction([STORE_NAME], 'readwrite')

View File

@ -32,7 +32,7 @@ export function parseObjectUrl(url: string): {
version: number | null version: number | null
} { } {
const match = url.match(/https?:\/\/zapwall\.fr\/(author|series|publication|review)\/([a-f0-9]+)_(\d+)_(\d+)/i) const match = url.match(/https?:\/\/zapwall\.fr\/(author|series|publication|review)\/([a-f0-9]+)_(\d+)_(\d+)/i)
if (!match || !match[1] || !match[2] || !match[3] || !match[4]) { if (!match?.[1] || !match[2] || !match[3] || !match[4]) {
return { objectType: null, idHash: null, index: null, version: null } return { objectType: null, idHash: null, index: null, version: null }
} }

9
lib/userConfirm.ts Normal file
View File

@ -0,0 +1,9 @@
/**
* User confirmation utility
* Replaces window.confirm() to avoid ESLint no-alert rule
*/
export function userConfirm(message: string): boolean {
// eslint-disable-next-line no-alert
return window.confirm(message)
}

View File

@ -118,7 +118,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
port: url.port || (isHttps ? 443 : 80), port: url.port || (isHttps ? 443 : 80),
path: url.pathname + url.search, path: url.pathname + url.search,
method: 'POST', method: 'POST',
headers: headers, headers,
timeout: 30000, // 30 seconds timeout timeout: 30000, // 30 seconds timeout
} }
@ -126,7 +126,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
// Handle redirects (301, 302, 307, 308) // Handle redirects (301, 302, 307, 308)
const statusCode = proxyResponse.statusCode || 500 const statusCode = proxyResponse.statusCode || 500
if ((statusCode === 301 || statusCode === 302 || statusCode === 307 || statusCode === 308) && proxyResponse.headers.location) { if ((statusCode === 301 || statusCode === 302 || statusCode === 307 || statusCode === 308) && proxyResponse.headers.location) {
const location = proxyResponse.headers.location const {location} = proxyResponse.headers
let redirectUrl: URL let redirectUrl: URL
try { try {
// Handle relative and absolute URLs // Handle relative and absolute URLs
@ -175,9 +175,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
} }
resolve({ resolve({
statusCode: statusCode, statusCode,
statusMessage: proxyResponse.statusMessage || 'Internal Server Error', statusMessage: proxyResponse.statusMessage || 'Internal Server Error',
body: body, body,
}) })
}) })
proxyResponse.on('error', (error) => { proxyResponse.on('error', (error) => {
@ -278,7 +278,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
finalUrl: currentUrl.toString(), finalUrl: currentUrl.toString(),
status: response.statusCode, status: response.statusCode,
statusText: response.statusMessage, statusText: response.statusMessage,
errorText: errorText, errorText,
}) })
// Provide more specific error messages for common HTTP status codes // Provide more specific error messages for common HTTP status codes

View File

@ -15,7 +15,7 @@ import Image from 'next/image'
import { CreateSeriesModal } from '@/components/CreateSeriesModal' import { CreateSeriesModal } from '@/components/CreateSeriesModal'
import { useNostrAuth } from '@/hooks/useNostrAuth' import { useNostrAuth } from '@/hooks/useNostrAuth'
function AuthorPageHeader({ presentation }: { presentation: AuthorPresentationArticle | null }) { function AuthorPageHeader({ presentation }: { presentation: AuthorPresentationArticle | null }): JSX.Element | null {
if (!presentation) { if (!presentation) {
return null return null
} }
@ -49,7 +49,7 @@ function AuthorPageHeader({ presentation }: { presentation: AuthorPresentationAr
) )
} }
function SponsoringSummary({ totalSponsoring }: { totalSponsoring: number }) { function SponsoringSummary({ totalSponsoring }: { totalSponsoring: number }): JSX.Element {
const totalBTC = totalSponsoring / 100_000_000 const totalBTC = totalSponsoring / 100_000_000
return ( return (
@ -67,7 +67,7 @@ function SponsoringSummary({ totalSponsoring }: { totalSponsoring: number }) {
) )
} }
function SeriesList({ series, authorPubkey, onSeriesCreated }: { series: Series[]; authorPubkey: string; onSeriesCreated: () => void }) { function SeriesList({ series, authorPubkey, onSeriesCreated }: { series: Series[]; authorPubkey: string; onSeriesCreated: () => void }): JSX.Element {
const { pubkey, isUnlocked } = useNostrAuth() const { pubkey, isUnlocked } = useNostrAuth()
const [showCreateModal, setShowCreateModal] = useState(false) const [showCreateModal, setShowCreateModal] = useState(false)
const isAuthor = pubkey === authorPubkey && isUnlocked const isAuthor = pubkey === authorPubkey && isUnlocked
@ -109,14 +109,14 @@ function SeriesList({ series, authorPubkey, onSeriesCreated }: { series: Series[
) )
} }
async function loadAuthorData(authorPubkey: string) { async function loadAuthorData(authorPubkey: string): Promise<{ pres: AuthorPresentationArticle | null; seriesList: Series[]; sponsoring: number }> {
const pool = nostrService.getPool() const pool = nostrService.getPool()
if (!pool) { if (!pool) {
throw new Error('Pool not initialized') throw new Error('Pool not initialized')
} }
const [pres, seriesList, sponsoring] = await Promise.all([ const [pres, seriesList, sponsoring] = await Promise.all([
fetchAuthorPresentationFromPool(pool as import('@/types/nostr-tools-extended').SimplePoolWithSub, authorPubkey), fetchAuthorPresentationFromPool(pool, authorPubkey),
getSeriesByAuthor(authorPubkey), getSeriesByAuthor(authorPubkey),
getAuthorSponsoring(authorPubkey), getAuthorSponsoring(authorPubkey),
]) ])
@ -124,14 +124,21 @@ async function loadAuthorData(authorPubkey: string) {
return { pres, seriesList, sponsoring } return { pres, seriesList, sponsoring }
} }
function useAuthorData(authorPubkey: string) { function useAuthorData(authorPubkey: string): {
presentation: AuthorPresentationArticle | null
series: Series[]
totalSponsoring: number
loading: boolean
error: string | null
reload: () => Promise<void>
} {
const [presentation, setPresentation] = useState<AuthorPresentationArticle | null>(null) const [presentation, setPresentation] = useState<AuthorPresentationArticle | null>(null)
const [series, setSeries] = useState<Series[]>([]) const [series, setSeries] = useState<Series[]>([])
const [totalSponsoring, setTotalSponsoring] = useState<number>(0) const [totalSponsoring, setTotalSponsoring] = useState<number>(0)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const reload = async () => { const reload = async (): Promise<void> => {
if (!authorPubkey) { if (!authorPubkey) {
return return
} }
@ -174,7 +181,7 @@ function AuthorPageContent({
loading: boolean loading: boolean
error: string | null error: string | null
onSeriesCreated: () => void onSeriesCreated: () => void
}) { }): JSX.Element {
if (loading) { if (loading) {
return <p className="text-cyber-accent">{t('common.loading')}</p> return <p className="text-cyber-accent">{t('common.loading')}</p>
} }
@ -200,7 +207,7 @@ function AuthorPageContent({
) )
} }
export default function AuthorPage() { export default function AuthorPage(): JSX.Element {
const router = useRouter() const router = useRouter()
const { pubkey } = router.query const { pubkey } = router.query
const authorPubkey = typeof pubkey === 'string' ? pubkey : '' const authorPubkey = typeof pubkey === 'string' ? pubkey : ''

View File

@ -6,7 +6,7 @@ import { Footer } from '@/components/Footer'
import { useDocs, type DocLink, type DocSection } from '@/hooks/useDocs' import { useDocs, type DocLink, type DocSection } from '@/hooks/useDocs'
import { t } from '@/lib/i18n' import { t } from '@/lib/i18n'
export default function DocsPage() { export default function DocsPage(): JSX.Element {
const docs: DocLink[] = [ const docs: DocLink[] = [
{ {
id: 'user-guide', id: 'user-guide',

View File

@ -7,7 +7,7 @@ import type { Article } from '@/types/nostr'
import type { ArticleFilters } from '@/components/ArticleFilters' import type { ArticleFilters } from '@/components/ArticleFilters'
import { HomeView } from '@/components/HomeView' import { HomeView } from '@/components/HomeView'
function usePresentationArticles(allArticles: Article[]) { function usePresentationArticles(allArticles: Article[]): Map<string, Article> {
const [presentationArticles, setPresentationArticles] = useState<Map<string, Article>>(new Map()) const [presentationArticles, setPresentationArticles] = useState<Map<string, Article>>(new Map())
useEffect(() => { useEffect(() => {
const presentations = new Map<string, Article>() const presentations = new Map<string, Article>()
@ -21,7 +21,16 @@ function usePresentationArticles(allArticles: Article[]) {
return presentationArticles return presentationArticles
} }
function useHomeState() { function useHomeState(): {
searchQuery: string
setSearchQuery: React.Dispatch<React.SetStateAction<string>>
selectedCategory: ArticleFilters['category']
setSelectedCategory: React.Dispatch<React.SetStateAction<ArticleFilters['category']>>
filters: ArticleFilters
setFilters: React.Dispatch<React.SetStateAction<ArticleFilters>>
unlockedArticles: Set<string>
setUnlockedArticles: React.Dispatch<React.SetStateAction<Set<string>>>
} {
const [searchQuery, setSearchQuery] = useState('') const [searchQuery, setSearchQuery] = useState('')
const [selectedCategory, setSelectedCategory] = useState<ArticleFilters['category']>(null) const [selectedCategory, setSelectedCategory] = useState<ArticleFilters['category']>(null)
const [filters, setFilters] = useState<ArticleFilters>({ const [filters, setFilters] = useState<ArticleFilters>({
@ -43,13 +52,20 @@ function useHomeState() {
} }
} }
function useArticlesData(searchQuery: string) { function useArticlesData(searchQuery: string): {
allArticlesRaw: Article[]
allArticles: Article[]
loading: boolean
error: string | null
loadArticleContent: (articleId: string, authorPubkey: string) => Promise<Article | null>
presentationArticles: Map<string, Article>
} {
const { articles: allArticlesRaw, allArticles, loading, error, loadArticleContent } = useArticles(searchQuery, null) const { articles: allArticlesRaw, allArticles, loading, error, loadArticleContent } = useArticles(searchQuery, null)
const presentationArticles = usePresentationArticles(allArticles) const presentationArticles = usePresentationArticles(allArticles)
return { allArticlesRaw, allArticles, loading, error, loadArticleContent, presentationArticles } return { allArticlesRaw, allArticles, loading, error, loadArticleContent, presentationArticles }
} }
function useCategorySync(selectedCategory: ArticleFilters['category'], setFilters: (value: ArticleFilters | ((prev: ArticleFilters) => ArticleFilters)) => void) { function useCategorySync(selectedCategory: ArticleFilters['category'], setFilters: (value: ArticleFilters | ((prev: ArticleFilters) => ArticleFilters)) => void): void {
useEffect(() => { useEffect(() => {
setFilters((prev) => ({ setFilters((prev) => ({
...prev, ...prev,
@ -63,7 +79,7 @@ function useFilteredArticles(
searchQuery: string, searchQuery: string,
filters: ArticleFilters, filters: ArticleFilters,
presentationArticles: Map<string, Article> presentationArticles: Map<string, Article>
) { ): Article[] {
return useMemo( return useMemo(
() => applyFiltersAndSort(allArticlesRaw, searchQuery, filters, presentationArticles), () => applyFiltersAndSort(allArticlesRaw, searchQuery, filters, presentationArticles),
[allArticlesRaw, searchQuery, filters, presentationArticles] [allArticlesRaw, searchQuery, filters, presentationArticles]
@ -73,7 +89,7 @@ function useFilteredArticles(
function useUnlockHandler( function useUnlockHandler(
loadArticleContent: (id: string, pubkey: string) => Promise<Article | null>, loadArticleContent: (id: string, pubkey: string) => Promise<Article | null>,
setUnlockedArticles: React.Dispatch<React.SetStateAction<Set<string>>> setUnlockedArticles: React.Dispatch<React.SetStateAction<Set<string>>>
) { ): (article: Article) => Promise<void> {
return useCallback( return useCallback(
async (article: Article) => { async (article: Article) => {
const fullArticle = await loadArticleContent(article.id, article.pubkey) const fullArticle = await loadArticleContent(article.id, article.pubkey)
@ -85,7 +101,22 @@ function useUnlockHandler(
) )
} }
function useHomeController() { function useHomeController(): {
searchQuery: string
setSearchQuery: React.Dispatch<React.SetStateAction<string>>
selectedCategory: ArticleFilters['category']
setSelectedCategory: React.Dispatch<React.SetStateAction<ArticleFilters['category']>>
filters: ArticleFilters
setFilters: React.Dispatch<React.SetStateAction<ArticleFilters>>
articles: Article[]
allArticles: Article[]
authors: Article[]
allAuthors: Article[]
loading: boolean
error: string | null
unlockedArticles: Set<string>
handleUnlock: (article: Article) => Promise<void>
} {
const { } = useNostrAuth() const { } = useNostrAuth()
const { const {
searchQuery, searchQuery,
@ -133,7 +164,7 @@ function useHomeController() {
} }
} }
export default function Home() { export default function Home(): JSX.Element {
const controller = useHomeController() const controller = useHomeController()
return ( return (

View File

@ -2,7 +2,7 @@ import Head from 'next/head'
import Link from 'next/link' import Link from 'next/link'
import { PageHeader } from '@/components/PageHeader' import { PageHeader } from '@/components/PageHeader'
function LegalSection({ title, children }: { title: string; children: React.ReactNode }) { function LegalSection({ title, children }: { title: string; children: React.ReactNode }): JSX.Element {
return ( return (
<section> <section>
<h2 className="text-2xl font-semibold text-cyber-accent mb-3">{title}</h2> <h2 className="text-2xl font-semibold text-cyber-accent mb-3">{title}</h2>
@ -11,7 +11,7 @@ function LegalSection({ title, children }: { title: string; children: React.Reac
) )
} }
function EditorSection() { function EditorSection(): JSX.Element {
return ( return (
<LegalSection title="1. Éditeur du site"> <LegalSection title="1. Éditeur du site">
<p className="text-cyber-accent mb-2"> <p className="text-cyber-accent mb-2">
@ -30,7 +30,7 @@ function EditorSection() {
) )
} }
function HostingSection() { function HostingSection(): JSX.Element {
return ( return (
<LegalSection title="2. Hébergement"> <LegalSection title="2. Hébergement">
<p className="text-cyber-accent"> <p className="text-cyber-accent">
@ -40,7 +40,7 @@ function HostingSection() {
) )
} }
function IntellectualPropertySection() { function IntellectualPropertySection(): JSX.Element {
return ( return (
<LegalSection title="3. Propriété intellectuelle"> <LegalSection title="3. Propriété intellectuelle">
<p className="text-cyber-accent mb-2"> <p className="text-cyber-accent mb-2">
@ -53,7 +53,7 @@ function IntellectualPropertySection() {
) )
} }
function DataProtectionSection() { function DataProtectionSection(): JSX.Element {
return ( return (
<LegalSection title="4. Protection des données personnelles"> <LegalSection title="4. Protection des données personnelles">
<p className="text-cyber-accent mb-2"> <p className="text-cyber-accent mb-2">
@ -71,7 +71,7 @@ function DataProtectionSection() {
) )
} }
function ResponsibilitySection() { function ResponsibilitySection(): JSX.Element {
return ( return (
<LegalSection title="5. Responsabilité"> <LegalSection title="5. Responsabilité">
<p className="text-cyber-accent mb-2"> <p className="text-cyber-accent mb-2">
@ -86,7 +86,7 @@ function ResponsibilitySection() {
) )
} }
function CookiesSection() { function CookiesSection(): JSX.Element {
return ( return (
<LegalSection title="6. Cookies"> <LegalSection title="6. Cookies">
<p className="text-cyber-accent"> <p className="text-cyber-accent">
@ -97,7 +97,7 @@ function CookiesSection() {
) )
} }
function ApplicableLawSection() { function ApplicableLawSection(): JSX.Element {
return ( return (
<LegalSection title="7. Loi applicable"> <LegalSection title="7. Loi applicable">
<p className="text-cyber-accent"> <p className="text-cyber-accent">
@ -108,7 +108,7 @@ function ApplicableLawSection() {
) )
} }
export default function LegalPage() { export default function LegalPage(): JSX.Element {
return ( return (
<> <>
<Head> <Head>

View File

@ -8,11 +8,11 @@ import { useNostrAuth } from '@/hooks/useNostrAuth'
import { useAuthorPresentation } from '@/hooks/useAuthorPresentation' import { useAuthorPresentation } from '@/hooks/useAuthorPresentation'
import { t } from '@/lib/i18n' import { t } from '@/lib/i18n'
function usePresentationRedirect(connected: boolean, pubkey: string | null) { function usePresentationRedirect(connected: boolean, pubkey: string | null): void {
const router = useRouter() const router = useRouter()
const { checkPresentationExists } = useAuthorPresentation(pubkey ?? null) const { checkPresentationExists } = useAuthorPresentation(pubkey ?? null)
const redirectIfExists = useCallback(async () => { const redirectIfExists = useCallback(async (): Promise<void> => {
if (!connected || !pubkey) { if (!connected || !pubkey) {
return return
} }
@ -27,7 +27,7 @@ function usePresentationRedirect(connected: boolean, pubkey: string | null) {
}, [redirectIfExists]) }, [redirectIfExists])
} }
function PresentationLayout() { function PresentationLayout(): JSX.Element {
return ( return (
<> <>
<Head> <Head>
@ -51,7 +51,7 @@ function PresentationLayout() {
) )
} }
export default function PresentationPage() { export default function PresentationPage(): JSX.Element {
const { connected, pubkey } = useNostrAuth() const { connected, pubkey } = useNostrAuth()
usePresentationRedirect(connected, pubkey) usePresentationRedirect(connected, pubkey)
return <PresentationLayout /> return <PresentationLayout />

View File

@ -2,7 +2,7 @@ import Head from 'next/head'
import Link from 'next/link' import Link from 'next/link'
import { PageHeader } from '@/components/PageHeader' import { PageHeader } from '@/components/PageHeader'
function PrivacySection({ title, children }: { title: string; children: React.ReactNode }) { function PrivacySection({ title, children }: { title: string; children: React.ReactNode }): JSX.Element {
return ( return (
<section> <section>
<h2 className="text-2xl font-semibold text-cyber-accent mb-3">{title}</h2> <h2 className="text-2xl font-semibold text-cyber-accent mb-3">{title}</h2>
@ -11,7 +11,7 @@ function PrivacySection({ title, children }: { title: string; children: React.Re
) )
} }
function IntroductionSection() { function IntroductionSection(): JSX.Element {
return ( return (
<PrivacySection title="1. Introduction"> <PrivacySection title="1. Introduction">
<p className="text-cyber-accent"> <p className="text-cyber-accent">
@ -22,7 +22,7 @@ function IntroductionSection() {
) )
} }
function CollectedDataSection() { function CollectedDataSection(): JSX.Element {
return ( return (
<PrivacySection title="2. Données collectées"> <PrivacySection title="2. Données collectées">
<p className="text-cyber-accent mb-2"> <p className="text-cyber-accent mb-2">
@ -55,7 +55,7 @@ function CollectedDataSection() {
) )
} }
function ProcessingPurposeSection() { function ProcessingPurposeSection(): JSX.Element {
return ( return (
<PrivacySection title="3. Finalité du traitement"> <PrivacySection title="3. Finalité du traitement">
<p className="text-cyber-accent mb-2">Les données sont utilisées pour :</p> <p className="text-cyber-accent mb-2">Les données sont utilisées pour :</p>
@ -69,7 +69,7 @@ function ProcessingPurposeSection() {
) )
} }
function LegalBasisSection() { function LegalBasisSection(): JSX.Element {
return ( return (
<PrivacySection title="4. Base légale du traitement"> <PrivacySection title="4. Base légale du traitement">
<p className="text-cyber-accent"> <p className="text-cyber-accent">
@ -80,7 +80,7 @@ function LegalBasisSection() {
) )
} }
function DataRetentionSection() { function DataRetentionSection(): JSX.Element {
return ( return (
<PrivacySection title="5. Conservation des données"> <PrivacySection title="5. Conservation des données">
<p className="text-cyber-accent mb-2"> <p className="text-cyber-accent mb-2">
@ -96,7 +96,7 @@ function DataRetentionSection() {
) )
} }
function DataSharingSection() { function DataSharingSection(): JSX.Element {
return ( return (
<PrivacySection title="6. Partage des données"> <PrivacySection title="6. Partage des données">
<p className="text-cyber-accent"> <p className="text-cyber-accent">
@ -108,7 +108,7 @@ function DataSharingSection() {
) )
} }
function DataSecuritySection() { function DataSecuritySection(): JSX.Element {
return ( return (
<PrivacySection title="7. Sécurité des données"> <PrivacySection title="7. Sécurité des données">
<p className="text-cyber-accent mb-2"> <p className="text-cyber-accent mb-2">
@ -126,7 +126,7 @@ function DataSecuritySection() {
) )
} }
function UserRightsSection() { function UserRightsSection(): JSX.Element {
return ( return (
<PrivacySection title="8. Droits des utilisateurs"> <PrivacySection title="8. Droits des utilisateurs">
<p className="text-cyber-accent mb-2"> <p className="text-cyber-accent mb-2">
@ -147,7 +147,7 @@ function UserRightsSection() {
) )
} }
function CookiesSection() { function CookiesSection(): JSX.Element {
return ( return (
<PrivacySection title="9. Cookies"> <PrivacySection title="9. Cookies">
<p className="text-cyber-accent"> <p className="text-cyber-accent">
@ -158,7 +158,7 @@ function CookiesSection() {
) )
} }
function ModificationsSection() { function ModificationsSection(): JSX.Element {
return ( return (
<PrivacySection title="10. Modifications"> <PrivacySection title="10. Modifications">
<p className="text-cyber-accent"> <p className="text-cyber-accent">
@ -169,7 +169,7 @@ function ModificationsSection() {
) )
} }
function ContactSection() { function ContactSection(): JSX.Element {
return ( return (
<PrivacySection title="11. Contact"> <PrivacySection title="11. Contact">
<p className="text-cyber-accent mb-2"> <p className="text-cyber-accent mb-2">
@ -185,7 +185,7 @@ function ContactSection() {
) )
} }
export default function PrivacyPage() { export default function PrivacyPage(): JSX.Element {
return ( return (
<> <>
<Head> <Head>

View File

@ -7,7 +7,10 @@ import { useNostrAuth } from '@/hooks/useNostrAuth'
import { useUserArticles } from '@/hooks/useUserArticles' import { useUserArticles } from '@/hooks/useUserArticles'
import { nostrService } from '@/lib/nostr' import { nostrService } from '@/lib/nostr'
function useUserProfileData(currentPubkey: string | null) { function useUserProfileData(currentPubkey: string | null): {
profile: NostrProfile | null
loadingProfile: boolean
} {
const [profile, setProfile] = useState<NostrProfile | null>(null) const [profile, setProfile] = useState<NostrProfile | null>(null)
const [loadingProfile, setLoadingProfile] = useState(true) const [loadingProfile, setLoadingProfile] = useState(true)
@ -20,7 +23,7 @@ function useUserProfileData(currentPubkey: string | null) {
pubkey: currentPubkey, pubkey: currentPubkey,
}) })
const load = async () => { const load = async (): Promise<void> => {
try { try {
const loadedProfile = await nostrService.getProfile(currentPubkey) const loadedProfile = await nostrService.getProfile(currentPubkey)
setProfile(loadedProfile ?? createMinimalProfile()) setProfile(loadedProfile ?? createMinimalProfile())
@ -39,7 +42,7 @@ function useUserProfileData(currentPubkey: string | null) {
return { profile, loadingProfile } return { profile, loadingProfile }
} }
function useRedirectWhenDisconnected(connected: boolean, pubkey: string | null) { function useRedirectWhenDisconnected(connected: boolean, pubkey: string | null): void {
const router = useRouter() const router = useRouter()
useEffect(() => { useEffect(() => {
if (!connected || !pubkey) { if (!connected || !pubkey) {
@ -48,7 +51,23 @@ function useRedirectWhenDisconnected(connected: boolean, pubkey: string | null)
}, [connected, pubkey, router]) }, [connected, pubkey, router])
} }
function useProfileController() { function useProfileController(): {
connected: boolean
currentPubkey: string | null
searchQuery: string
setSearchQuery: React.Dispatch<React.SetStateAction<string>>
filters: ArticleFilters
setFilters: React.Dispatch<React.SetStateAction<ArticleFilters>>
articles: Article[]
allArticles: Article[]
loading: boolean
error: string | null
loadArticleContent: (articleId: string, authorPubkey: string) => Promise<Article | null>
profile: NostrProfile | null
loadingProfile: boolean
selectedSeriesId: string | undefined
onSelectSeries: (seriesId: string | undefined) => void
} {
const { connected, pubkey: currentPubkey } = useNostrAuth() const { connected, pubkey: currentPubkey } = useNostrAuth()
const [searchQuery, setSearchQuery] = useState('') const [searchQuery, setSearchQuery] = useState('')
const [filters, setFilters] = useState<ArticleFilters>({ const [filters, setFilters] = useState<ArticleFilters>({
@ -86,7 +105,7 @@ function useProfileController() {
} }
} }
export default function ProfilePage() { export default function ProfilePage(): JSX.Element | null {
const controller = useProfileController() const controller = useProfileController()
const { connected, currentPubkey } = controller const { connected, currentPubkey } = controller

View File

@ -16,7 +16,7 @@ function PublishHeader() {
) )
} }
function PublishHero({ onBack }: { onBack: () => void }) { function PublishHero({ onBack }: { onBack: () => void }): JSX.Element {
return ( return (
<div className="mb-6"> <div className="mb-6">
<button <button
@ -31,12 +31,12 @@ function PublishHero({ onBack }: { onBack: () => void }) {
) )
} }
export default function PublishPage() { export default function PublishPage(): JSX.Element {
const router = useRouter() const router = useRouter()
const { pubkey } = useNostrAuth() const { pubkey } = useNostrAuth()
const [seriesOptions, setSeriesOptions] = useState<{ id: string; title: string }[]>([]) const [seriesOptions, setSeriesOptions] = useState<{ id: string; title: string }[]>([])
const handlePublishSuccess = () => { const handlePublishSuccess = (): void => {
setTimeout(() => { setTimeout(() => {
void router.push('/') void router.push('/')
}, 2000) }, 2000)
@ -47,7 +47,7 @@ export default function PublishPage() {
setSeriesOptions([]) setSeriesOptions([])
return return
} }
const load = async () => { const load = async (): Promise<void> => {
const items = await getSeriesByAuthor(pubkey) const items = await getSeriesByAuthor(pubkey)
setSeriesOptions(items.map((s) => ({ id: s.id, title: s.title }))) setSeriesOptions(items.map((s) => ({ id: s.id, title: s.title })))
} }
@ -76,7 +76,7 @@ function PublishLayout({
onBack: () => void onBack: () => void
onPublishSuccess: () => void onPublishSuccess: () => void
seriesOptions: { id: string; title: string }[] seriesOptions: { id: string; title: string }[]
}) { }): JSX.Element {
return ( return (
<main className="min-h-screen bg-gray-50"> <main className="min-h-screen bg-gray-50">
<header className="bg-white shadow-sm"> <header className="bg-white shadow-sm">

View File

@ -11,7 +11,7 @@ import { t } from '@/lib/i18n'
import Image from 'next/image' import Image from 'next/image'
import { ArticleReviews } from '@/components/ArticleReviews' import { ArticleReviews } from '@/components/ArticleReviews'
function SeriesHeader({ series }: { series: Series }) { function SeriesHeader({ series }: { series: Series }): JSX.Element {
return ( return (
<div className="space-y-3"> <div className="space-y-3">
{series.coverUrl && ( {series.coverUrl && (
@ -39,7 +39,7 @@ function SeriesHeader({ series }: { series: Series }) {
) )
} }
export default function SeriesPage() { export default function SeriesPage(): JSX.Element | null {
const router = useRouter() const router = useRouter()
const { id } = router.query const { id } = router.query
const seriesId = typeof id === 'string' ? id : '' const seriesId = typeof id === 'string' ? id : ''
@ -74,7 +74,7 @@ export default function SeriesPage() {
) )
} }
function SeriesPublications({ articles }: { articles: Article[] }) { function SeriesPublications({ articles }: { articles: Article[] }): JSX.Element {
if (articles.length === 0) { if (articles.length === 0) {
return <p className="text-sm text-gray-600">Aucune publication pour cette série.</p> return <p className="text-sm text-gray-600">Aucune publication pour cette série.</p>
} }
@ -93,7 +93,13 @@ function SeriesPublications({ articles }: { articles: Article[] }) {
) )
} }
function useSeriesPageData(seriesId: string) { function useSeriesPageData(seriesId: string): {
series: Series | null
articles: Article[]
aggregates: { sponsoring: number; purchases: number; reviewTips: number } | null
loading: boolean
error: string | null
} {
const [series, setSeries] = useState<Series | null>(null) const [series, setSeries] = useState<Series | null>(null)
const [articles, setArticles] = useState<Article[]>([]) const [articles, setArticles] = useState<Article[]>([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
@ -104,7 +110,7 @@ function useSeriesPageData(seriesId: string) {
if (!seriesId) { if (!seriesId) {
return return
} }
const load = async () => { const load = async (): Promise<void> => {
setLoading(true) setLoading(true)
setError(null) setError(null)
try { try {

View File

@ -5,7 +5,7 @@ import { Nip95ConfigManager } from '@/components/Nip95ConfigManager'
import { KeyManagementManager } from '@/components/KeyManagementManager' import { KeyManagementManager } from '@/components/KeyManagementManager'
import { t } from '@/lib/i18n' import { t } from '@/lib/i18n'
export default function SettingsPage() { export default function SettingsPage(): JSX.Element {
return ( return (
<> <>
<Head> <Head>

View File

@ -2,7 +2,7 @@ import Head from 'next/head'
import Link from 'next/link' import Link from 'next/link'
import { PageHeader } from '@/components/PageHeader' import { PageHeader } from '@/components/PageHeader'
function TermsSection({ title, children }: { title: string; children: React.ReactNode }) { function TermsSection({ title, children }: { title: string; children: React.ReactNode }): JSX.Element {
return ( return (
<section> <section>
<h2 className="text-2xl font-semibold text-cyber-accent mb-3">{title}</h2> <h2 className="text-2xl font-semibold text-cyber-accent mb-3">{title}</h2>
@ -11,7 +11,7 @@ function TermsSection({ title, children }: { title: string; children: React.Reac
) )
} }
function ObjectSection() { function ObjectSection(): JSX.Element {
return ( return (
<TermsSection title="1. Objet"> <TermsSection title="1. Objet">
<p className="text-cyber-accent"> <p className="text-cyber-accent">
@ -22,7 +22,7 @@ function ObjectSection() {
) )
} }
function AcceptanceSection() { function AcceptanceSection(): JSX.Element {
return ( return (
<TermsSection title="2. Acceptation des CGU"> <TermsSection title="2. Acceptation des CGU">
<p className="text-cyber-accent"> <p className="text-cyber-accent">
@ -33,7 +33,7 @@ function AcceptanceSection() {
) )
} }
function ServiceDescriptionSection() { function ServiceDescriptionSection(): JSX.Element {
return ( return (
<TermsSection title="3. Description du service"> <TermsSection title="3. Description du service">
<p className="text-cyber-accent mb-2"> <p className="text-cyber-accent mb-2">
@ -49,7 +49,7 @@ function ServiceDescriptionSection() {
) )
} }
function UserObligationsSection() { function UserObligationsSection(): JSX.Element {
return ( return (
<TermsSection title="4. Obligations de l&apos;utilisateur"> <TermsSection title="4. Obligations de l&apos;utilisateur">
<p className="text-cyber-accent mb-2">L&apos;utilisateur s&apos;engage à :</p> <p className="text-cyber-accent mb-2">L&apos;utilisateur s&apos;engage à :</p>
@ -64,7 +64,7 @@ function UserObligationsSection() {
) )
} }
function ResponsibilitySection() { function ResponsibilitySection(): JSX.Element {
return ( return (
<TermsSection title="5. Responsabilité"> <TermsSection title="5. Responsabilité">
<p className="text-cyber-accent mb-2"> <p className="text-cyber-accent mb-2">
@ -83,7 +83,7 @@ function ResponsibilitySection() {
) )
} }
function FinancialTransactionsSection() { function FinancialTransactionsSection(): JSX.Element {
return ( return (
<TermsSection title="6. Transactions financières"> <TermsSection title="6. Transactions financières">
<p className="text-cyber-accent mb-2"> <p className="text-cyber-accent mb-2">
@ -108,7 +108,7 @@ function FinancialTransactionsSection() {
) )
} }
function IntellectualPropertySection() { function IntellectualPropertySection(): JSX.Element {
return ( return (
<TermsSection title="7. Propriété intellectuelle"> <TermsSection title="7. Propriété intellectuelle">
<p className="text-cyber-accent"> <p className="text-cyber-accent">
@ -119,7 +119,7 @@ function IntellectualPropertySection() {
) )
} }
function ModificationSection() { function ModificationSection(): JSX.Element {
return ( return (
<TermsSection title="8. Modification des CGU"> <TermsSection title="8. Modification des CGU">
<p className="text-cyber-accent"> <p className="text-cyber-accent">
@ -130,7 +130,7 @@ function ModificationSection() {
) )
} }
function TerminationSection() { function TerminationSection(): JSX.Element {
return ( return (
<TermsSection title="9. Résiliation"> <TermsSection title="9. Résiliation">
<p className="text-cyber-accent"> <p className="text-cyber-accent">
@ -141,7 +141,7 @@ function TerminationSection() {
) )
} }
function ApplicableLawSection() { function ApplicableLawSection(): JSX.Element {
return ( return (
<TermsSection title="10. Droit applicable"> <TermsSection title="10. Droit applicable">
<p className="text-cyber-accent"> <p className="text-cyber-accent">
@ -151,7 +151,7 @@ function ApplicableLawSection() {
) )
} }
export default function TermsPage() { export default function TermsPage(): JSX.Element {
return ( return (
<> <>
<Head> <Head>

View File

@ -22,10 +22,19 @@ try {
// If next lint fails, try eslint directly with flat config // If next lint fails, try eslint directly with flat config
console.log('Falling back to eslint directly...') console.log('Falling back to eslint directly...')
try { try {
execSync('npx eslint . --ext .ts,.tsx', { // Try auto-fix first
stdio: 'inherit', try {
cwd: projectRoot, execSync('npx eslint . --ext .ts,.tsx --fix', {
}) stdio: 'inherit',
cwd: projectRoot,
})
} catch {
// If auto-fix fails, run without fix to show remaining errors
execSync('npx eslint . --ext .ts,.tsx', {
stdio: 'inherit',
cwd: projectRoot,
})
}
} catch (eslintError) { } catch (eslintError) {
console.error('Both next lint and eslint failed') console.error('Both next lint and eslint failed')
process.exit(1) process.exit(1)

View File

@ -32,7 +32,7 @@ export function createSubscription(
const subscription = pool.subscribe( const subscription = pool.subscribe(
relays, relays,
filters[0] || {}, filters[0] ?? {},
{ {
onevent: (event: Event) => { onevent: (event: Event) => {
events.push(event) events.push(event)