lint fix wip
This commit is contained in:
parent
5ac5aab089
commit
412989e6af
@ -5,7 +5,7 @@ interface AlbyInstallerProps {
|
||||
onInstalled?: () => void
|
||||
}
|
||||
|
||||
function InfoIcon() {
|
||||
function InfoIcon(): JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
className="h-5 w-5 text-blue-400"
|
||||
@ -27,7 +27,7 @@ interface InstallerActionsProps {
|
||||
markInstalled: () => void
|
||||
}
|
||||
|
||||
function InstallerActions({ onInstalled, markInstalled }: InstallerActionsProps) {
|
||||
function InstallerActions({ onInstalled, markInstalled }: InstallerActionsProps): JSX.Element {
|
||||
const connect = useCallback(() => {
|
||||
const alby = getAlbyService()
|
||||
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 (
|
||||
<div className="ml-3 flex-1">
|
||||
<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 [isChecking, setIsChecking] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
const checkAlby = () => {
|
||||
const checkAlby = (): void => {
|
||||
try {
|
||||
const alby = getAlbyService()
|
||||
const installed = alby.isEnabled()
|
||||
@ -101,14 +101,14 @@ function useAlbyStatus(onInstalled?: () => void) {
|
||||
checkAlby()
|
||||
}, [onInstalled])
|
||||
|
||||
const markInstalled = () => {
|
||||
const markInstalled = (): void => {
|
||||
setIsInstalled(true)
|
||||
}
|
||||
|
||||
return { isInstalled, isChecking, markInstalled }
|
||||
}
|
||||
|
||||
export function AlbyInstaller({ onInstalled }: AlbyInstallerProps) {
|
||||
export function AlbyInstaller({ onInstalled }: AlbyInstallerProps): JSX.Element | null {
|
||||
const { isInstalled, isChecking, markInstalled } = useAlbyStatus(onInstalled)
|
||||
|
||||
if (isChecking || isInstalled) {
|
||||
|
||||
@ -11,7 +11,7 @@ interface ArticleCardProps {
|
||||
onUnlock?: (article: Article) => void
|
||||
}
|
||||
|
||||
function ArticleHeader({ article }: { article: Article }) {
|
||||
function ArticleHeader({ article }: { article: Article }): JSX.Element {
|
||||
return (
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<h2 className="text-2xl font-bold text-neon-cyan">{article.title}</h2>
|
||||
@ -37,7 +37,7 @@ function ArticleMeta({
|
||||
paymentInvoice: ReturnType<typeof useArticlePayment>['paymentInvoice']
|
||||
onClose: () => void
|
||||
onPaymentComplete: () => void
|
||||
}) {
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
{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 {
|
||||
loading,
|
||||
|
||||
@ -12,7 +12,7 @@ interface ArticleEditorProps {
|
||||
}
|
||||
|
||||
|
||||
function SuccessMessage() {
|
||||
function SuccessMessage(): JSX.Element {
|
||||
return (
|
||||
<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>
|
||||
@ -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 { loading, error, success, publishArticle } = useArticlePublishing(pubkey ?? null)
|
||||
const [draft, setDraft] = useState<ArticleDraft>({
|
||||
@ -61,8 +61,8 @@ function buildSubmitHandler(
|
||||
onPublishSuccess?: (articleId: string) => void,
|
||||
connect?: () => Promise<void>,
|
||||
connected?: boolean
|
||||
) {
|
||||
return async () => {
|
||||
): () => Promise<void> {
|
||||
return async (): Promise<void> => {
|
||||
if (!connected && connect) {
|
||||
await connect()
|
||||
return
|
||||
|
||||
@ -25,7 +25,7 @@ function CategoryField({
|
||||
}: {
|
||||
value: ArticleDraft['category']
|
||||
onChange: (value: import('@/types/nostr').ArticleCategory | undefined) => void
|
||||
}) {
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<CategorySelect
|
||||
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) {
|
||||
return null
|
||||
}
|
||||
@ -76,7 +76,7 @@ const ArticleFieldsLeft = ({
|
||||
onDraftChange: (draft: ArticleDraft) => void
|
||||
seriesOptions?: { id: string; title: string }[] | undefined
|
||||
onSelectSeries?: ((seriesId: string | undefined) => void) | undefined
|
||||
}) => (
|
||||
}): JSX.Element => (
|
||||
<div className="space-y-4">
|
||||
<CategoryField
|
||||
value={draft.category}
|
||||
@ -95,7 +95,7 @@ const ArticleFieldsLeft = ({
|
||||
</div>
|
||||
)
|
||||
|
||||
function ArticleTitleField({ draft, onDraftChange }: { draft: ArticleDraft; onDraftChange: (draft: ArticleDraft) => void }) {
|
||||
function ArticleTitleField({ draft, onDraftChange }: { draft: ArticleDraft; onDraftChange: (draft: ArticleDraft) => void }): JSX.Element {
|
||||
return (
|
||||
<ArticleField
|
||||
id="title"
|
||||
@ -114,7 +114,7 @@ function ArticlePreviewField({
|
||||
}: {
|
||||
draft: ArticleDraft
|
||||
onDraftChange: (draft: ArticleDraft) => void
|
||||
}) {
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<ArticleField
|
||||
id="preview"
|
||||
@ -140,7 +140,7 @@ function SeriesSelect({
|
||||
onDraftChange: (draft: ArticleDraft) => void
|
||||
seriesOptions: { id: string; title: string }[]
|
||||
onSelectSeries?: ((seriesId: string | undefined) => void) | undefined
|
||||
}) {
|
||||
}): JSX.Element {
|
||||
const handleChange = buildSeriesChangeHandler(draft, onDraftChange, onSelectSeries)
|
||||
|
||||
return (
|
||||
@ -169,8 +169,8 @@ function buildSeriesChangeHandler(
|
||||
draft: ArticleDraft,
|
||||
onDraftChange: (draft: ArticleDraft) => void,
|
||||
onSelectSeries?: ((seriesId: string | undefined) => void) | undefined
|
||||
) {
|
||||
return (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
): (e: React.ChangeEvent<HTMLSelectElement>) => void {
|
||||
return (e: React.ChangeEvent<HTMLSelectElement>): void => {
|
||||
const value = e.target.value || undefined
|
||||
const nextDraft = { ...draft }
|
||||
if (value) {
|
||||
@ -189,7 +189,7 @@ const ArticleFieldsRight = ({
|
||||
}: {
|
||||
draft: ArticleDraft
|
||||
onDraftChange: (draft: ArticleDraft) => void
|
||||
}) => (
|
||||
}): JSX.Element => (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-semibold text-gray-800">{t('article.editor.content.label')}</div>
|
||||
@ -230,7 +230,7 @@ export function ArticleEditorForm({
|
||||
onCancel,
|
||||
seriesOptions,
|
||||
onSelectSeries,
|
||||
}: ArticleEditorFormProps) {
|
||||
}: ArticleEditorFormProps): JSX.Element {
|
||||
return (
|
||||
<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>
|
||||
|
||||
@ -30,7 +30,7 @@ function NumberOrTextInput({
|
||||
min?: number
|
||||
className: string
|
||||
onChange: (value: string | number) => void
|
||||
}) {
|
||||
}): JSX.Element {
|
||||
const inputProps = {
|
||||
id,
|
||||
type,
|
||||
@ -64,7 +64,7 @@ function TextAreaInput({
|
||||
rows?: number
|
||||
className: string
|
||||
onChange: (value: string | number) => void
|
||||
}) {
|
||||
}): JSX.Element {
|
||||
const areaProps = {
|
||||
id,
|
||||
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 } =
|
||||
props
|
||||
const inputClass =
|
||||
|
||||
@ -46,8 +46,8 @@ function FiltersGrid({
|
||||
data: FiltersData
|
||||
filters: ArticleFilters
|
||||
onFiltersChange: (filters: ArticleFilters) => void
|
||||
}) {
|
||||
const update = (patch: Partial<ArticleFilters>) => onFiltersChange({ ...filters, ...patch })
|
||||
}): JSX.Element {
|
||||
const update = (patch: Partial<ArticleFilters>): void => onFiltersChange({ ...filters, ...patch })
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
@ -63,7 +63,7 @@ function FiltersHeader({
|
||||
}: {
|
||||
hasActiveFilters: boolean
|
||||
onClear: () => void
|
||||
}) {
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-lg font-semibold text-neon-cyan">{t('filters.sort')}</h3>
|
||||
@ -83,7 +83,7 @@ function SortFilter({
|
||||
}: {
|
||||
value: SortOption
|
||||
onChange: (value: SortOption) => void
|
||||
}) {
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<div>
|
||||
<label htmlFor="sort" className="block text-sm font-medium text-cyber-accent mb-1">
|
||||
@ -106,7 +106,7 @@ export function ArticleFiltersComponent({
|
||||
filters,
|
||||
onFiltersChange,
|
||||
articles,
|
||||
}: ArticleFiltersProps) {
|
||||
}: ArticleFiltersProps): JSX.Element {
|
||||
const data = useFiltersData(articles)
|
||||
|
||||
const handleClearFilters = () => {
|
||||
|
||||
@ -5,7 +5,7 @@ interface ArticleFormButtonsProps {
|
||||
onCancel?: () => void
|
||||
}
|
||||
|
||||
export function ArticleFormButtons({ loading, onCancel }: ArticleFormButtonsProps) {
|
||||
export function ArticleFormButtons({ loading, onCancel }: ArticleFormButtonsProps): JSX.Element {
|
||||
return (
|
||||
<div className="flex gap-3 pt-4">
|
||||
<button
|
||||
|
||||
@ -6,7 +6,7 @@ interface ArticlePreviewProps {
|
||||
onUnlock: () => void
|
||||
}
|
||||
|
||||
export function ArticlePreview({ article, loading, onUnlock }: ArticlePreviewProps) {
|
||||
export function ArticlePreview({ article, loading, onUnlock }: ArticlePreviewProps): JSX.Element {
|
||||
if (article.paid) {
|
||||
return (
|
||||
<div>
|
||||
|
||||
@ -8,14 +8,14 @@ interface ArticleReviewsProps {
|
||||
authorPubkey: string
|
||||
}
|
||||
|
||||
export function ArticleReviews({ articleId, authorPubkey }: ArticleReviewsProps) {
|
||||
export function ArticleReviews({ articleId, authorPubkey }: ArticleReviewsProps): JSX.Element {
|
||||
const [reviews, setReviews] = useState<Review[]>([])
|
||||
const [tips, setTips] = useState<number>(0)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
const load = async (): Promise<void> => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
@ -45,7 +45,7 @@ export function ArticleReviews({ articleId, authorPubkey }: ArticleReviewsProps)
|
||||
)
|
||||
}
|
||||
|
||||
function ArticleReviewsHeader({ tips }: { tips: number }) {
|
||||
function ArticleReviewsHeader({ tips }: { tips: number }): JSX.Element {
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<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 (
|
||||
<>
|
||||
{reviews.map((r) => (
|
||||
|
||||
@ -11,7 +11,7 @@ interface ArticlesListProps {
|
||||
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
|
||||
return (
|
||||
<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 (
|
||||
<div className="bg-red-900/20 border border-red-500/50 rounded-lg p-4 mb-4">
|
||||
<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 (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-cyber-accent/70">
|
||||
@ -45,7 +45,7 @@ export function ArticlesList({
|
||||
error,
|
||||
onUnlock,
|
||||
unlockedArticles,
|
||||
}: ArticlesListProps) {
|
||||
}: ArticlesListProps): JSX.Element {
|
||||
if (loading) {
|
||||
return <LoadingState />
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ interface AuthorCardProps {
|
||||
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 totalBTC = (presentation.totalSponsoring ?? 0) / 100_000_000
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ import { useAuthorFilterProps } from './AuthorFilterHooks'
|
||||
import { AuthorFilterButtonWrapper } from './AuthorFilterButton'
|
||||
import { AuthorDropdown } from './AuthorFilterDropdown'
|
||||
|
||||
function AuthorFilterLabel() {
|
||||
function AuthorFilterLabel(): JSX.Element {
|
||||
return (
|
||||
<label htmlFor="author-filter" className="block text-sm font-medium text-cyber-accent mb-1">
|
||||
{t('filters.author')}
|
||||
@ -28,7 +28,7 @@ interface AuthorFilterContentProps {
|
||||
selectedDisplayName: string
|
||||
}
|
||||
|
||||
function AuthorFilterContent(props: AuthorFilterContentProps) {
|
||||
function AuthorFilterContent(props: AuthorFilterContentProps): JSX.Element {
|
||||
return (
|
||||
<div className="relative" ref={props.dropdownRef}>
|
||||
<AuthorFilterButtonWrapper
|
||||
@ -64,7 +64,7 @@ export function AuthorFilter({
|
||||
authors: string[]
|
||||
value: string | null
|
||||
onChange: (value: string | null) => void
|
||||
}) {
|
||||
}): JSX.Element {
|
||||
const props = useAuthorFilterProps(authors, value)
|
||||
|
||||
return (
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
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 (
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
{getMnemonicIcons(value).map((icon, idx) => (
|
||||
@ -23,7 +23,7 @@ export function AuthorFilterButtonContent({
|
||||
selectedAuthor: { name?: string; picture?: string } | null | undefined
|
||||
selectedDisplayName: string
|
||||
getMnemonicIcons: (pubkey: string) => string[]
|
||||
}) {
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
{value && (
|
||||
@ -38,7 +38,7 @@ export function AuthorFilterButtonContent({
|
||||
)
|
||||
}
|
||||
|
||||
export function DropdownArrowIcon({ isOpen }: { isOpen: boolean }) {
|
||||
export function DropdownArrowIcon({ isOpen }: { isOpen: boolean }): JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
className={`w-5 h-5 text-neon-cyan transition-transform ${isOpen ? 'rotate-180' : ''}`}
|
||||
@ -68,7 +68,7 @@ export function AuthorFilterButton({
|
||||
isOpen: boolean
|
||||
setIsOpen: (open: boolean) => void
|
||||
buttonRef: React.RefObject<HTMLButtonElement | null>
|
||||
}) {
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<button
|
||||
id="author-filter"
|
||||
@ -106,7 +106,7 @@ export function AuthorFilterButtonWrapper({
|
||||
isOpen: boolean
|
||||
setIsOpen: (open: boolean) => void
|
||||
buttonRef: React.RefObject<HTMLButtonElement | null>
|
||||
}) {
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<AuthorFilterButton
|
||||
value={value}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import Image from 'next/image'
|
||||
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) {
|
||||
return (
|
||||
<Image
|
||||
@ -32,7 +32,7 @@ export function AuthorOption({
|
||||
mnemonicIcons: string[]
|
||||
isSelected: boolean
|
||||
onSelect: () => void
|
||||
}) {
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
@ -136,7 +136,7 @@ export function AuthorList({
|
||||
getMnemonicIcons: (pubkey: string) => string[]
|
||||
onChange: (value: string | null) => void
|
||||
setIsOpen: (open: boolean) => void
|
||||
}) {
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
{authors.map((pubkey) => (
|
||||
@ -167,7 +167,7 @@ export function AuthorDropdownContent({
|
||||
getMnemonicIcons: (pubkey: string) => string[]
|
||||
onChange: (value: string | null) => void
|
||||
setIsOpen: (open: boolean) => void
|
||||
}) {
|
||||
}): JSX.Element {
|
||||
return loading ? (
|
||||
<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
|
||||
getPicture: (pubkey: string) => string | undefined
|
||||
getMnemonicIcons: (pubkey: string) => string[]
|
||||
}) {
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<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"
|
||||
|
||||
@ -3,12 +3,12 @@ import { useAuthorsProfiles } from '@/hooks/useAuthorsProfiles'
|
||||
import { generateMnemonicIcons } from '@/lib/mnemonicIcons'
|
||||
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 buttonRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
const handleClickOutside = (event: MouseEvent): void => {
|
||||
if (
|
||||
dropdownRef.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') {
|
||||
setIsOpen(false)
|
||||
buttonRef.current?.focus()
|
||||
@ -40,7 +40,7 @@ export function useAuthorFilterDropdown(isOpen: boolean, setIsOpen: (open: boole
|
||||
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 profile = profiles.get(pubkey)
|
||||
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 }
|
||||
}
|
||||
|
||||
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 [isOpen, setIsOpen] = useState(false)
|
||||
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)
|
||||
return { ...state, authors, value }
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ import { PresentationFormHeader } from './PresentationFormHeader'
|
||||
import { extractPresentationData } from '@/lib/presentationParsing'
|
||||
import type { Article } from '@/types/nostr'
|
||||
import { t } from '@/lib/i18n'
|
||||
import { userConfirm } from '@/lib/userConfirm'
|
||||
|
||||
interface AuthorPresentationDraft {
|
||||
authorName: string
|
||||
@ -220,10 +221,10 @@ function PresentationForm({
|
||||
<div className="flex-1">
|
||||
<button
|
||||
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"
|
||||
>
|
||||
{loading || deleting
|
||||
{loading ?? deleting
|
||||
? t('publish.publishing')
|
||||
: hasExistingPresentation
|
||||
? t('presentation.update.button')
|
||||
@ -244,12 +245,12 @@ function useAuthorPresentationState(pubkey: string | null, existingAuthorName?:
|
||||
const [draft, setDraft] = useState<AuthorPresentationDraft>(() => {
|
||||
if (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 {
|
||||
authorName,
|
||||
presentation,
|
||||
contentDescription,
|
||||
mainnetAddress: existingPresentation.mainnetAddress || '',
|
||||
mainnetAddress: existingPresentation.mainnetAddress ?? '',
|
||||
...(existingPresentation.bannerUrl ? { pictureUrl: existingPresentation.bannerUrl } : {}),
|
||||
}
|
||||
}
|
||||
@ -293,7 +294,7 @@ function useAuthorPresentationState(pubkey: string | null, existingAuthorName?:
|
||||
return
|
||||
}
|
||||
|
||||
if (!confirm(t('presentation.delete.confirm'))) {
|
||||
if (!userConfirm(t('presentation.delete.confirm'))) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -472,7 +473,7 @@ function AuthorPresentationFormView({
|
||||
handleSubmit={state.handleSubmit}
|
||||
deleting={state.deleting}
|
||||
handleDelete={state.handleDelete}
|
||||
hasExistingPresentation={!!existingPresentation}
|
||||
hasExistingPresentation={existingPresentation !== null && existingPresentation !== undefined}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@ interface AuthorsListProps {
|
||||
error: string | null
|
||||
}
|
||||
|
||||
function LoadingState() {
|
||||
function LoadingState(): JSX.Element {
|
||||
return (
|
||||
<div className="text-center py-12">
|
||||
<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 (
|
||||
<div className="bg-red-900/20 border border-red-500/50 rounded-lg p-4 mb-4">
|
||||
<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 (
|
||||
<div className="text-center py-12">
|
||||
<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) {
|
||||
return <LoadingState />
|
||||
}
|
||||
|
||||
@ -17,7 +17,7 @@ export function CategorySelect({
|
||||
onChange,
|
||||
required = false,
|
||||
helpText,
|
||||
}: CategorySelectProps) {
|
||||
}: CategorySelectProps): JSX.Element {
|
||||
return (
|
||||
<div>
|
||||
<label htmlFor={id} className="block text-sm font-medium text-gray-700 mb-1">
|
||||
|
||||
@ -7,7 +7,7 @@ interface CategoryTabsProps {
|
||||
onCategoryChange: (category: CategoryFilter) => void
|
||||
}
|
||||
|
||||
export function CategoryTabs({ selectedCategory, onCategoryChange }: CategoryTabsProps) {
|
||||
export function CategoryTabs({ selectedCategory, onCategoryChange }: CategoryTabsProps): JSX.Element {
|
||||
return (
|
||||
<div className="mb-6">
|
||||
<div className="border-b border-neon-cyan/30">
|
||||
|
||||
@ -4,7 +4,7 @@ interface ClearButtonProps {
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
export function ClearButton({ onClick }: ClearButtonProps) {
|
||||
export function ClearButton({ onClick }: ClearButtonProps): JSX.Element {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
|
||||
@ -21,11 +21,11 @@ function AuthorProfileLink({ presentation, profile }: { presentation: Article; p
|
||||
// Title format: "Présentation de <name>" or just use profile name
|
||||
let authorName = presentation.title.replace(/^Présentation de /, '').trim()
|
||||
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
|
||||
const picture = presentation.bannerUrl || profile?.picture
|
||||
const picture = presentation.bannerUrl ?? profile?.picture
|
||||
|
||||
return (
|
||||
<Link
|
||||
|
||||
@ -161,8 +161,8 @@ export function ConnectButton() {
|
||||
return (
|
||||
<>
|
||||
<DisconnectedState
|
||||
loading={loading || creatingAccount}
|
||||
error={error || createError}
|
||||
loading={loading ?? creatingAccount}
|
||||
error={error ?? createError}
|
||||
showUnlockModal={showUnlockModal}
|
||||
setShowUnlockModal={setShowUnlockModal}
|
||||
onCreateAccount={handleCreateAccount}
|
||||
|
||||
@ -15,7 +15,7 @@ export function ConnectedUserMenu({
|
||||
profile,
|
||||
onDisconnect,
|
||||
loading,
|
||||
}: ConnectedUserMenuProps) {
|
||||
}: ConnectedUserMenuProps): JSX.Element {
|
||||
const displayName = profile?.name ?? `${pubkey.slice(0, 8)}...`
|
||||
|
||||
return (
|
||||
|
||||
@ -11,7 +11,7 @@ interface CreateAccountModalProps {
|
||||
type Step = 'choose' | 'import' | 'recovery'
|
||||
|
||||
async function createAccountWithKey(key?: string) {
|
||||
return await nostrAuthService.createAccount(key)
|
||||
return nostrAuthService.createAccount(key)
|
||||
}
|
||||
|
||||
async function handleAccountCreation(
|
||||
|
||||
@ -19,7 +19,7 @@ export function RecoveryPhraseDisplay({
|
||||
recoveryPhrase: string[]
|
||||
copied: boolean
|
||||
onCopy: () => void
|
||||
}) {
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<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">
|
||||
@ -45,7 +45,7 @@ export function RecoveryPhraseDisplay({
|
||||
)
|
||||
}
|
||||
|
||||
export function PublicKeyDisplay({ npub }: { npub: string }) {
|
||||
export function PublicKeyDisplay({ npub }: { npub: string }): JSX.Element {
|
||||
return (
|
||||
<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>
|
||||
@ -62,7 +62,7 @@ export function ImportKeyForm({
|
||||
importKey: string
|
||||
setImportKey: (key: string) => void
|
||||
error: string | null
|
||||
}) {
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<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 (
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
@ -116,7 +116,7 @@ export function ChooseStepButtons({
|
||||
onGenerate: () => void
|
||||
onImport: () => void
|
||||
onClose: () => void
|
||||
}) {
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<button
|
||||
|
||||
@ -10,10 +10,10 @@ export function RecoveryStep({
|
||||
recoveryPhrase: string[]
|
||||
npub: string
|
||||
onContinue: () => void
|
||||
}) {
|
||||
}): JSX.Element {
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const handleCopy = async () => {
|
||||
const handleCopy = async (): Promise<void> => {
|
||||
if (recoveryPhrase.length > 0) {
|
||||
await navigator.clipboard.writeText(recoveryPhrase.join(' '))
|
||||
setCopied(true)
|
||||
@ -55,7 +55,7 @@ export function ImportStep({
|
||||
error: string | null
|
||||
onImport: () => void
|
||||
onBack: () => void
|
||||
}) {
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<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">
|
||||
@ -79,7 +79,7 @@ export function ChooseStep({
|
||||
onGenerate: () => void
|
||||
onImport: () => void
|
||||
onClose: () => void
|
||||
}) {
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<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">
|
||||
|
||||
@ -6,7 +6,7 @@ interface DocsContentProps {
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
export function DocsContent({ content, loading }: DocsContentProps) {
|
||||
export function DocsContent({ content, loading }: DocsContentProps): JSX.Element {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="text-center py-12">
|
||||
|
||||
@ -7,7 +7,7 @@ interface DocsSidebarProps {
|
||||
onSelectDoc: (docId: DocSection) => void
|
||||
}
|
||||
|
||||
export function DocsSidebar({ docs, selectedDoc, onSelectDoc }: DocsSidebarProps) {
|
||||
export function DocsSidebar({ docs, selectedDoc, onSelectDoc }: DocsSidebarProps): JSX.Element {
|
||||
return (
|
||||
<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">
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import Link from 'next/link'
|
||||
import { t } from '@/lib/i18n'
|
||||
|
||||
export function Footer() {
|
||||
export function Footer(): JSX.Element {
|
||||
return (
|
||||
<footer className="bg-cyber-dark border-t border-neon-cyan/30 mt-12">
|
||||
<div className="max-w-4xl mx-auto px-4 py-6">
|
||||
|
||||
@ -6,7 +6,7 @@ interface FundingProgressBarProps {
|
||||
progressPercent: number
|
||||
}
|
||||
|
||||
function FundingProgressBar({ progressPercent }: FundingProgressBarProps) {
|
||||
function FundingProgressBar({ progressPercent }: FundingProgressBarProps): JSX.Element {
|
||||
return (
|
||||
<div className="relative w-full h-4 bg-cyber-dark rounded-full overflow-hidden border border-neon-cyan/30">
|
||||
<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)
|
||||
return (
|
||||
<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 [certificationStats, setCertificationStats] = useState(estimatePlatformFunds())
|
||||
const [loading, setLoading] = useState(true)
|
||||
@ -49,7 +49,7 @@ export function FundingGauge() {
|
||||
useEffect(() => {
|
||||
// In a real implementation, this would fetch actual data
|
||||
// For now, we use the estimate
|
||||
const loadStats = async () => {
|
||||
const loadStats = async (): Promise<void> => {
|
||||
try {
|
||||
const fundingStats = estimatePlatformFunds()
|
||||
setStats(fundingStats)
|
||||
|
||||
@ -2,6 +2,7 @@ import { useState, useEffect } from 'react'
|
||||
import { configStorage } from '@/lib/configStorage'
|
||||
import type { Nip95Config } from '@/lib/configStorageTypes'
|
||||
import { t } from '@/lib/i18n'
|
||||
import { userConfirm } from '@/lib/userConfirm'
|
||||
|
||||
interface Nip95ConfigManagerProps {
|
||||
onConfigChange?: () => void
|
||||
@ -97,7 +98,7 @@ export function Nip95ConfigManager({ onConfigChange }: Nip95ConfigManagerProps)
|
||||
}
|
||||
|
||||
async function handleRemoveApi(id: string) {
|
||||
if (!confirm(t('settings.nip95.remove.confirm'))) {
|
||||
if (!userConfirm(t('settings.nip95.remove.confirm'))) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ export function NotificationPanelHeader({
|
||||
unreadCount,
|
||||
onMarkAllAsRead,
|
||||
onClose,
|
||||
}: NotificationPanelHeaderProps) {
|
||||
}: NotificationPanelHeaderProps): JSX.Element {
|
||||
return (
|
||||
<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>
|
||||
|
||||
@ -4,7 +4,7 @@ import { LanguageSelector } from './LanguageSelector'
|
||||
import { t } from '@/lib/i18n'
|
||||
import { KeyIndicator } from './KeyIndicator'
|
||||
|
||||
function GitIcon() {
|
||||
function GitIcon(): JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
@ -17,7 +17,7 @@ function GitIcon() {
|
||||
)
|
||||
}
|
||||
|
||||
export function PageHeader() {
|
||||
export function PageHeader(): JSX.Element {
|
||||
return (
|
||||
<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">
|
||||
|
||||
@ -11,14 +11,14 @@ interface PaymentModalProps {
|
||||
onPaymentComplete: () => void
|
||||
}
|
||||
|
||||
function useInvoiceTimer(expiresAt?: number) {
|
||||
function useInvoiceTimer(expiresAt?: number): number | null {
|
||||
const [timeRemaining, setTimeRemaining] = useState<number | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!expiresAt) {
|
||||
return
|
||||
}
|
||||
const updateTimeRemaining = () => {
|
||||
const updateTimeRemaining = (): void => {
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
const remaining = expiresAt - now
|
||||
setTimeRemaining(remaining > 0 ? remaining : 0)
|
||||
@ -39,8 +39,8 @@ function PaymentHeader({
|
||||
amount: number
|
||||
timeRemaining: number | null
|
||||
onClose: () => void
|
||||
}) {
|
||||
const timeLabel = useMemo(() => {
|
||||
}): JSX.Element {
|
||||
const timeLabel = useMemo((): string | null => {
|
||||
if (timeRemaining === 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 (
|
||||
<div className="mb-4">
|
||||
<p className="text-sm text-cyber-accent mb-2">{t('payment.modal.lightningInvoice')}</p>
|
||||
@ -97,7 +97,7 @@ function PaymentActions({
|
||||
copied: boolean
|
||||
onCopy: () => Promise<void>
|
||||
onOpenWallet: () => void
|
||||
}) {
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
@ -118,7 +118,7 @@ function PaymentActions({
|
||||
)
|
||||
}
|
||||
|
||||
function ExpiredNotice({ show }: { show: boolean }) {
|
||||
function ExpiredNotice({ show }: { show: boolean }): JSX.Element | null {
|
||||
if (!show) {
|
||||
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 [errorMessage, setErrorMessage] = useState<string | null>(null)
|
||||
const paymentUrl = `lightning:${invoice.invoice}`
|
||||
const timeRemaining = useInvoiceTimer(invoice.expiresAt)
|
||||
|
||||
const handleCopy = useCallback(async () => {
|
||||
const handleCopy = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(invoice.invoice)
|
||||
setCopied(true)
|
||||
@ -147,7 +154,7 @@ function usePaymentModalState(invoice: AlbyInvoice, onPaymentComplete: () => voi
|
||||
}
|
||||
}, [invoice.invoice])
|
||||
|
||||
const handleOpenWallet = useCallback(async () => {
|
||||
const handleOpenWallet = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
const alby = getAlbyService()
|
||||
if (!isWebLNAvailable()) {
|
||||
@ -169,10 +176,10 @@ function usePaymentModalState(invoice: AlbyInvoice, onPaymentComplete: () => voi
|
||||
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 } =
|
||||
usePaymentModalState(invoice, onPaymentComplete)
|
||||
const handleOpenWalletSync = () => {
|
||||
const handleOpenWalletSync = (): void => {
|
||||
void handleOpenWallet()
|
||||
}
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ interface SearchBarProps {
|
||||
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 [localValue, setLocalValue] = useState(value)
|
||||
|
||||
@ -17,13 +17,13 @@ export function SearchBar({ value, onChange, placeholder }: SearchBarProps) {
|
||||
setLocalValue(value)
|
||||
}, [value])
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
const newValue = e.target.value
|
||||
setLocalValue(newValue)
|
||||
onChange(newValue)
|
||||
}
|
||||
|
||||
const handleClear = () => {
|
||||
const handleClear = (): void => {
|
||||
setLocalValue('')
|
||||
onChange('')
|
||||
}
|
||||
|
||||
@ -33,11 +33,11 @@ const ArticlesError = ({ message }: { message: string }) => (
|
||||
)
|
||||
|
||||
const EmptyState = ({ show }: { show: boolean }) =>
|
||||
show ? (
|
||||
(show ? (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-gray-500">{t('common.empty.articles')}</p>
|
||||
</div>
|
||||
) : null
|
||||
) : null)
|
||||
|
||||
function ArticleActions({
|
||||
article,
|
||||
|
||||
@ -11,7 +11,22 @@ export default [
|
||||
},
|
||||
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: {
|
||||
parser: typescriptParser,
|
||||
parserOptions: {
|
||||
|
||||
@ -10,16 +10,26 @@ interface EditState {
|
||||
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 [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const updateDraft = (draft: ArticleDraft | null) => {
|
||||
const updateDraft = (draft: ArticleDraft | null): void => {
|
||||
setState((prev) => ({ ...prev, draft }))
|
||||
}
|
||||
|
||||
const startEditing = async (article: Article) => {
|
||||
const startEditing = async (article: Article): Promise<void> => {
|
||||
if (!authorPubkey) {
|
||||
setError('Connect your Nostr wallet to edit')
|
||||
return
|
||||
@ -52,7 +62,7 @@ export function useArticleEditing(authorPubkey: string | null) {
|
||||
}
|
||||
}
|
||||
|
||||
const cancelEditing = () => {
|
||||
const cancelEditing = (): void => {
|
||||
setState({ draft: null, articleId: null })
|
||||
setError(null)
|
||||
}
|
||||
|
||||
@ -9,13 +9,20 @@ export function useArticlePayment(
|
||||
pubkey: string | null,
|
||||
onUnlockSuccess?: () => 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 [error, setError] = useState<string | null>(null)
|
||||
const [paymentInvoice, setPaymentInvoice] = useState<AlbyInvoice | 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 {
|
||||
const hasPaid = await paymentService.waitForArticlePayment(
|
||||
hash,
|
||||
@ -41,7 +48,7 @@ export function useArticlePayment(
|
||||
}
|
||||
}
|
||||
|
||||
const handleUnlock = async () => {
|
||||
const handleUnlock = async (): Promise<void> => {
|
||||
if (!pubkey) {
|
||||
if (connect) {
|
||||
setLoading(true)
|
||||
@ -80,13 +87,13 @@ export function useArticlePayment(
|
||||
}
|
||||
}
|
||||
|
||||
const handlePaymentComplete = async () => {
|
||||
const handlePaymentComplete = async (): Promise<void> => {
|
||||
if (paymentHash && pubkey) {
|
||||
await checkPaymentStatus(paymentHash, pubkey)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCloseModal = () => {
|
||||
const handleCloseModal = (): void => {
|
||||
setPaymentInvoice(null)
|
||||
setPaymentHash(null)
|
||||
}
|
||||
|
||||
@ -3,7 +3,12 @@ import { articlePublisher } from '@/lib/articlePublisher'
|
||||
import { nostrService } from '@/lib/nostr'
|
||||
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 [error, setError] = useState<string | null>(null)
|
||||
const [success, setSuccess] = useState(false)
|
||||
@ -48,4 +53,3 @@ export function useArticlePublishing(pubkey: string | null) {
|
||||
publishArticle,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -5,7 +5,13 @@ import { applyFiltersAndSort } from '@/lib/articleFiltering'
|
||||
import type { ArticleFilters } from '@/components/ArticleFilters'
|
||||
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 [loading, setLoading] = useState(true)
|
||||
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 {
|
||||
const article = await nostrService.getArticleById(articleId)
|
||||
if (article) {
|
||||
@ -52,9 +58,9 @@ export function useArticles(searchQuery: string = '', filters: ArticleFilters |
|
||||
if (decryptedContent) {
|
||||
setArticles((prev) =>
|
||||
prev.map((a) =>
|
||||
a.id === articleId
|
||||
(a.id === articleId
|
||||
? { ...a, content: decryptedContent, paid: true }
|
||||
: a
|
||||
: a)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@ -12,7 +12,14 @@ interface AuthorPresentationDraft {
|
||||
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 [error, setError] = useState<string | null>(null)
|
||||
const [success, setSuccess] = useState(false)
|
||||
@ -85,7 +92,7 @@ export function useAuthorPresentation(pubkey: string | null) {
|
||||
}
|
||||
|
||||
try {
|
||||
return await articlePublisher.getAuthorPresentation(pubkey)
|
||||
return articlePublisher.getAuthorPresentation(pubkey)
|
||||
} catch (e) {
|
||||
console.error('Error checking presentation:', e)
|
||||
return null
|
||||
|
||||
@ -22,7 +22,7 @@ export function useAuthorsProfiles(authorPubkeys: string[]): {
|
||||
return
|
||||
}
|
||||
|
||||
const loadProfiles = async () => {
|
||||
const loadProfiles = async (): Promise<void> => {
|
||||
setLoading(true)
|
||||
const profilesMap = new Map<string, AuthorProfile>()
|
||||
|
||||
|
||||
@ -8,14 +8,19 @@ export interface DocLink {
|
||||
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 [docContent, setDocContent] = useState<string>('')
|
||||
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)
|
||||
if (!doc) return
|
||||
if (!doc) {return}
|
||||
|
||||
setLoading(true)
|
||||
setSelectedDoc(docId)
|
||||
|
||||
@ -1,12 +1,16 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
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 [currentLocale, setCurrentLocale] = useState<Locale>(getLocale())
|
||||
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
const load = async (): Promise<void> => {
|
||||
try {
|
||||
// Get saved locale from IndexedDB or use provided locale
|
||||
let savedLocale: Locale | null = null
|
||||
|
||||
@ -2,7 +2,14 @@ import { useState, useEffect } from 'react'
|
||||
import { nostrAuthService } from '@/lib/nostrAuth'
|
||||
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 [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
@ -19,7 +26,7 @@ export function useNostrAuth() {
|
||||
return unsubscribe
|
||||
}, [])
|
||||
|
||||
const connect = async () => {
|
||||
const connect = async (): Promise<void> => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
@ -31,7 +38,7 @@ export function useNostrAuth() {
|
||||
}
|
||||
}
|
||||
|
||||
const disconnect = async () => {
|
||||
const disconnect = async (): Promise<void> => {
|
||||
setLoading(true)
|
||||
try {
|
||||
nostrAuthService.disconnect()
|
||||
|
||||
@ -2,7 +2,14 @@ import { useState, useEffect, useCallback } from 'react'
|
||||
import { notificationService, loadStoredNotifications, saveNotifications, markNotificationAsRead, markAllAsRead, deleteNotification } from '@/lib/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 [loading, setLoading] = useState(true)
|
||||
|
||||
@ -14,7 +21,7 @@ export function useNotifications(userPubkey: string | null) {
|
||||
return
|
||||
}
|
||||
|
||||
const loadStored = async () => {
|
||||
const loadStored = async (): Promise<void> => {
|
||||
const storedNotifications = await loadStoredNotifications(userPubkey)
|
||||
setNotifications(storedNotifications)
|
||||
}
|
||||
@ -55,23 +62,23 @@ export function useNotifications(userPubkey: string | null) {
|
||||
const unreadCount = notifications.filter((n) => !n.read).length
|
||||
|
||||
const markAsRead = useCallback(
|
||||
(notificationId: string) => {
|
||||
if (!userPubkey) return
|
||||
(notificationId: string): void => {
|
||||
if (!userPubkey) {return}
|
||||
|
||||
setNotifications((prev) => markNotificationAsRead(userPubkey, notificationId, prev))
|
||||
},
|
||||
[userPubkey]
|
||||
)
|
||||
|
||||
const markAllAsReadHandler = useCallback(() => {
|
||||
if (!userPubkey) return
|
||||
const markAllAsReadHandler = useCallback((): void => {
|
||||
if (!userPubkey) {return}
|
||||
|
||||
setNotifications((prev) => markAllAsRead(userPubkey, prev))
|
||||
}, [userPubkey])
|
||||
|
||||
const deleteNotificationHandler = useCallback(
|
||||
(notificationId: string) => {
|
||||
if (!userPubkey) return
|
||||
(notificationId: string): void => {
|
||||
if (!userPubkey) {return}
|
||||
|
||||
setNotifications((prev) => deleteNotification(userPubkey, notificationId, prev))
|
||||
},
|
||||
|
||||
@ -11,7 +11,13 @@ export function useUserArticles(
|
||||
userPubkey: string,
|
||||
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 [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
@ -71,7 +77,7 @@ export function useUserArticles(
|
||||
return applyFiltersAndSort(articles, searchQuery, effectiveFilters)
|
||||
}, [articles, searchQuery, filters])
|
||||
|
||||
const loadArticleContent = async (articleId: string, authorPubkey: string) => {
|
||||
const loadArticleContent = async (articleId: string, authorPubkey: string): Promise<Article | null> => {
|
||||
try {
|
||||
const article = await nostrService.getArticleById(articleId)
|
||||
if (article) {
|
||||
@ -80,9 +86,9 @@ export function useUserArticles(
|
||||
if (decryptedContent) {
|
||||
setArticles((prev) =>
|
||||
prev.map((a) =>
|
||||
a.id === articleId
|
||||
(a.id === articleId
|
||||
? { ...a, content: decryptedContent, paid: true }
|
||||
: a
|
||||
: a)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@ -57,7 +57,7 @@ export async function publishSeries(params: {
|
||||
authorPrivateKey?: string
|
||||
}): Promise<Series> {
|
||||
ensureKeys(params.authorPubkey, params.authorPrivateKey)
|
||||
const category = params.category
|
||||
const {category} = params
|
||||
requireCategory(category)
|
||||
const event = await buildSeriesEvent(params, category)
|
||||
const published = await nostrService.publishEvent(event)
|
||||
@ -148,7 +148,7 @@ export async function publishReview(params: {
|
||||
authorPrivateKey?: string
|
||||
}): Promise<Review> {
|
||||
ensureKeys(params.reviewerPubkey, params.authorPrivateKey)
|
||||
const category = params.category
|
||||
const {category} = params
|
||||
requireCategory(category)
|
||||
const event = await buildReviewEvent(params, category)
|
||||
const published = await nostrService.publishEvent(event)
|
||||
@ -273,7 +273,7 @@ async function publishUpdate(
|
||||
authorPubkey: string,
|
||||
originalArticleId: string
|
||||
): Promise<ArticleUpdateResult> {
|
||||
const category = draft.category
|
||||
const {category} = draft
|
||||
requireCategory(category)
|
||||
const presentationId = await ensurePresentation(authorPubkey)
|
||||
const invoice = await createArticleInvoice(draft)
|
||||
@ -302,7 +302,7 @@ export async function publishArticleUpdate(
|
||||
): Promise<ArticleUpdateResult> {
|
||||
try {
|
||||
ensureKeys(authorPubkey, authorPrivateKey)
|
||||
return await publishUpdate(draft, authorPubkey, originalArticleId)
|
||||
return publishUpdate(draft, authorPubkey, originalArticleId)
|
||||
} catch (error) {
|
||||
return updateFailure(originalArticleId, error instanceof Error ? error.message : 'Unknown error')
|
||||
}
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { nostrService } from './nostr'
|
||||
import type { SimplePoolWithSub } from '@/types/nostr-tools-extended'
|
||||
import type { AlbyInvoice } from '@/types/alby'
|
||||
import { getStoredPrivateContent, getStoredInvoice, removeStoredPrivateContent } from './articleStorage'
|
||||
import { buildPresentationEvent, fetchAuthorPresentationFromPool, sendEncryptedContent } from './articlePublisherHelpers'
|
||||
@ -72,7 +71,7 @@ export class ArticlePublisher {
|
||||
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) {
|
||||
console.error('Error publishing article:', error)
|
||||
return buildFailure(error instanceof Error ? error.message : 'Unknown error')
|
||||
@ -172,7 +171,7 @@ export class ArticlePublisher {
|
||||
nostrService.setPrivateKey(authorPrivateKey)
|
||||
|
||||
// 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
|
||||
const eventTemplate = await buildPresentationEvent(draft, authorPubkey, authorName, 'sciencefiction')
|
||||
@ -211,7 +210,7 @@ export class ArticlePublisher {
|
||||
if (!pool) {
|
||||
return null
|
||||
}
|
||||
return await fetchAuthorPresentationFromPool(pool as SimplePoolWithSub, pubkey)
|
||||
return fetchAuthorPresentationFromPool(pool, pubkey)
|
||||
} catch (error) {
|
||||
console.error('Error getting author presentation:', error)
|
||||
return null
|
||||
|
||||
@ -123,7 +123,7 @@ export function parsePresentationEvent(event: Event): import('@/types/nostr').Au
|
||||
if (!profileData) {
|
||||
// Try invisible format (with zero-width characters)
|
||||
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 {
|
||||
// Remove zero-width characters from JSON
|
||||
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
|
||||
if (!profileData) {
|
||||
const jsonMatch = event.content.match(/\[Metadata JSON\]\n(.+)$/s)
|
||||
if (jsonMatch && jsonMatch[1]) {
|
||||
if (jsonMatch?.[1]) {
|
||||
try {
|
||||
profileData = JSON.parse(jsonMatch[1].trim())
|
||||
} catch (e) {
|
||||
|
||||
@ -92,7 +92,7 @@ export class ConfigStorage {
|
||||
return this.getDefaultConfig()
|
||||
}
|
||||
|
||||
const db = this.db
|
||||
const {db} = this
|
||||
return new Promise((resolve) => {
|
||||
const transaction = db.transaction([STORE_NAME], 'readonly')
|
||||
const store = transaction.objectStore(STORE_NAME)
|
||||
@ -131,7 +131,7 @@ export class ConfigStorage {
|
||||
throw new Error('Database not initialized')
|
||||
}
|
||||
|
||||
const db = this.db
|
||||
const {db} = this
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([STORE_NAME], 'readwrite')
|
||||
const store = transaction.objectStore(STORE_NAME)
|
||||
|
||||
@ -94,14 +94,14 @@ export class KeyManagementService {
|
||||
* Check if an account exists (encrypted key is stored)
|
||||
*/
|
||||
async accountExists(): Promise<boolean> {
|
||||
return await accountExistsTwoLevel()
|
||||
return accountExistsTwoLevel()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the public key and npub if account exists
|
||||
*/
|
||||
async getPublicKeys(): Promise<{ publicKey: string; npub: string } | null> {
|
||||
return await getPublicKeysTwoLevel()
|
||||
return getPublicKeysTwoLevel()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -31,7 +31,7 @@ export async function removeAccountFlag(): Promise<void> {
|
||||
}
|
||||
|
||||
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> {
|
||||
|
||||
@ -19,7 +19,7 @@ const PBKDF2_HASH = 'SHA-256'
|
||||
* Generate a random KEK (Key Encryption Key)
|
||||
*/
|
||||
async function generateKEK(): Promise<CryptoKey> {
|
||||
return await crypto.subtle.generateKey(
|
||||
return crypto.subtle.generateKey(
|
||||
{ name: 'AES-GCM', length: 256 },
|
||||
true, // extractable
|
||||
['encrypt', 'decrypt']
|
||||
@ -81,7 +81,7 @@ async function importKEK(keyBytes: Uint8Array): Promise<CryptoKey> {
|
||||
const buffer = new ArrayBuffer(keyBytes.length)
|
||||
const view = new Uint8Array(buffer)
|
||||
view.set(keyBytes)
|
||||
return await crypto.subtle.importKey(
|
||||
return crypto.subtle.importKey(
|
||||
'raw',
|
||||
buffer,
|
||||
{ name: 'AES-GCM' },
|
||||
@ -160,9 +160,9 @@ async function decryptKEK(encryptedKEK: EncryptedPayload, recoveryPhrase: string
|
||||
const hexString = decoder.decode(decrypted)
|
||||
|
||||
// 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 }
|
||||
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')
|
||||
}
|
||||
|
||||
@ -265,7 +265,7 @@ async function storeEncryptedKEK(encryptedKEK: EncryptedPayload): Promise<void>
|
||||
id: 'nostr_kek',
|
||||
name: 'Nostr KEK',
|
||||
password: JSON.stringify(encryptedKEK),
|
||||
iconURL: window.location.origin + '/favicon.ico',
|
||||
iconURL: `${window.location.origin }/favicon.ico`,
|
||||
})
|
||||
|
||||
await navigator.credentials.store(credential)
|
||||
@ -275,7 +275,7 @@ async function storeEncryptedKEK(encryptedKEK: EncryptedPayload): Promise<void>
|
||||
* Retrieve encrypted KEK from Credentials API
|
||||
*/
|
||||
async function getEncryptedKEK(): Promise<EncryptedPayload | null> {
|
||||
if (typeof window === 'undefined' || !navigator.credentials || !navigator.credentials.get) {
|
||||
if (typeof window === 'undefined' || !navigator.credentials?.get) {
|
||||
return null
|
||||
}
|
||||
|
||||
@ -423,7 +423,7 @@ export async function accountExistsTwoLevel(): Promise<boolean> {
|
||||
export async function getPublicKeysTwoLevel(): Promise<{ publicKey: string; npub: string } | null> {
|
||||
try {
|
||||
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 {
|
||||
return null
|
||||
}
|
||||
|
||||
@ -18,7 +18,7 @@ export class MempoolSpaceService {
|
||||
* Fetch transaction from mempool.space
|
||||
*/
|
||||
async getTransaction(txid: string): Promise<MempoolTransaction | null> {
|
||||
return await getTransaction(txid)
|
||||
return getTransaction(txid)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -29,7 +29,7 @@ export class MempoolSpaceService {
|
||||
txid: string,
|
||||
authorMainnetAddress: string
|
||||
): Promise<TransactionVerificationResult> {
|
||||
return await verifySponsoringTransaction(txid, authorMainnetAddress)
|
||||
return verifySponsoringTransaction(txid, authorMainnetAddress)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -41,7 +41,7 @@ export class MempoolSpaceService {
|
||||
timeout: number = 600000, // 10 minutes
|
||||
interval: number = 10000 // 10 seconds
|
||||
): Promise<TransactionVerificationResult | null> {
|
||||
return await waitForConfirmation(txid, timeout, interval)
|
||||
return waitForConfirmation(txid, timeout, interval)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -78,7 +78,7 @@ export async function validateTransactionOutputs(
|
||||
)
|
||||
|
||||
const valid = Boolean(authorOutput && platformOutput)
|
||||
const confirmed = transaction.status.confirmed
|
||||
const {confirmed} = transaction.status
|
||||
const confirmations = confirmed && transaction.status.block_height
|
||||
? await getConfirmations(transaction.status.block_height)
|
||||
: 0
|
||||
|
||||
@ -107,7 +107,7 @@ export type ExtractedObject =
|
||||
*/
|
||||
function extractMetadataJsonFromTag(event: { tags: string[][] }): Record<string, unknown> | null {
|
||||
const jsonTag = event.tags.find((tag) => tag[0] === 'json')
|
||||
if (jsonTag && jsonTag[1]) {
|
||||
if (jsonTag?.[1]) {
|
||||
try {
|
||||
return JSON.parse(jsonTag[1])
|
||||
} catch (e) {
|
||||
@ -121,7 +121,7 @@ function extractMetadataJsonFromTag(event: { tags: string[][] }): Record<string,
|
||||
function extractMetadataJson(content: string): Record<string, unknown> | null {
|
||||
// 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)
|
||||
if (invisibleJsonMatch && invisibleJsonMatch[1]) {
|
||||
if (invisibleJsonMatch?.[1]) {
|
||||
try {
|
||||
// Remove zero-width characters from JSON
|
||||
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)
|
||||
const jsonMatch = content.match(/\[Metadata JSON\]\n(.+)$/s)
|
||||
if (jsonMatch && jsonMatch[1]) {
|
||||
if (jsonMatch?.[1]) {
|
||||
try {
|
||||
return JSON.parse(jsonMatch[1].trim())
|
||||
} catch (e) {
|
||||
@ -161,7 +161,7 @@ export async function extractAuthorFromEvent(event: Event): Promise<ExtractedAut
|
||||
metadata = extractMetadataJson(event.content)
|
||||
}
|
||||
|
||||
if (metadata && metadata.type === 'author') {
|
||||
if (metadata?.type === 'author') {
|
||||
const authorData = {
|
||||
pubkey: (metadata.pubkey as string) ?? event.pubkey,
|
||||
authorName: (metadata.authorName as string) ?? '',
|
||||
@ -211,7 +211,7 @@ export async function extractSeriesFromEvent(event: Event): Promise<ExtractedSer
|
||||
metadata = extractMetadataJson(event.content)
|
||||
}
|
||||
|
||||
if (metadata && metadata.type === 'series') {
|
||||
if (metadata?.type === 'series') {
|
||||
const seriesData = {
|
||||
pubkey: (metadata.pubkey as string) ?? event.pubkey,
|
||||
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) {
|
||||
const seriesData = {
|
||||
pubkey: event.pubkey,
|
||||
title: tags.title as string,
|
||||
description: tags.description as string,
|
||||
title: tags.title,
|
||||
description: tags.description,
|
||||
preview: (tags.preview as string) ?? event.content.substring(0, 200),
|
||||
coverUrl: tags.coverUrl as string | undefined,
|
||||
coverUrl: tags.coverUrl,
|
||||
category: tags.category ?? 'sciencefiction',
|
||||
}
|
||||
|
||||
@ -282,7 +282,7 @@ export async function extractPublicationFromEvent(event: Event): Promise<Extract
|
||||
metadata = extractMetadataJson(event.content)
|
||||
}
|
||||
|
||||
if (metadata && metadata.type === 'publication') {
|
||||
if (metadata?.type === 'publication') {
|
||||
const publicationData = {
|
||||
pubkey: (metadata.pubkey as string) ?? event.pubkey,
|
||||
title: (metadata.title as string) ?? (tags.title as string) ?? '',
|
||||
@ -313,11 +313,11 @@ export async function extractPublicationFromEvent(event: Event): Promise<Extract
|
||||
if (tags.title) {
|
||||
const publicationData = {
|
||||
pubkey: event.pubkey,
|
||||
title: tags.title as string,
|
||||
title: tags.title,
|
||||
preview: (tags.preview as string) ?? event.content.substring(0, 200),
|
||||
category: tags.category ?? 'sciencefiction',
|
||||
seriesId: tags.seriesId as string | undefined,
|
||||
bannerUrl: tags.bannerUrl as string | undefined,
|
||||
seriesId: tags.seriesId,
|
||||
bannerUrl: tags.bannerUrl,
|
||||
zapAmount: tags.zapAmount ?? 800,
|
||||
}
|
||||
|
||||
@ -357,7 +357,7 @@ export async function extractReviewFromEvent(event: Event): Promise<ExtractedRev
|
||||
metadata = extractMetadataJson(event.content)
|
||||
}
|
||||
|
||||
if (metadata && metadata.type === 'review') {
|
||||
if (metadata?.type === 'review') {
|
||||
const reviewData = {
|
||||
pubkey: (metadata.pubkey as string) ?? event.pubkey,
|
||||
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) {
|
||||
const reviewData = {
|
||||
pubkey: event.pubkey,
|
||||
articleId: tags.articleId as string,
|
||||
reviewerPubkey: tags.reviewerPubkey as string,
|
||||
articleId: tags.articleId,
|
||||
reviewerPubkey: tags.reviewerPubkey,
|
||||
content: event.content,
|
||||
title: tags.title as string | undefined,
|
||||
title: tags.title,
|
||||
}
|
||||
|
||||
const id = await generateReviewHashId({
|
||||
@ -570,25 +570,25 @@ export async function extractObjectsFromEvent(event: Event): Promise<ExtractedOb
|
||||
|
||||
// Try to extract each type
|
||||
const author = await extractAuthorFromEvent(event)
|
||||
if (author) results.push(author)
|
||||
if (author) {results.push(author)}
|
||||
|
||||
const series = await extractSeriesFromEvent(event)
|
||||
if (series) results.push(series)
|
||||
if (series) {results.push(series)}
|
||||
|
||||
const publication = await extractPublicationFromEvent(event)
|
||||
if (publication) results.push(publication)
|
||||
if (publication) {results.push(publication)}
|
||||
|
||||
const review = await extractReviewFromEvent(event)
|
||||
if (review) results.push(review)
|
||||
if (review) {results.push(review)}
|
||||
|
||||
const purchase = await extractPurchaseFromEvent(event)
|
||||
if (purchase) results.push(purchase)
|
||||
if (purchase) {results.push(purchase)}
|
||||
|
||||
const reviewTip = await extractReviewTipFromEvent(event)
|
||||
if (reviewTip) results.push(reviewTip)
|
||||
if (reviewTip) {results.push(reviewTip)}
|
||||
|
||||
const sponsoring = await extractSponsoringFromEvent(event)
|
||||
if (sponsoring) results.push(sponsoring)
|
||||
if (sponsoring) {results.push(sponsoring)}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
@ -163,7 +163,7 @@ export async function uploadNip95Media(file: File): Promise<MediaRef> {
|
||||
// Always use proxy to avoid CORS, 405, and name resolution issues
|
||||
// Pass endpoint and auth token as query parameters to proxy
|
||||
const proxyUrlParams = new URLSearchParams({
|
||||
endpoint: endpoint,
|
||||
endpoint,
|
||||
})
|
||||
if (authToken) {
|
||||
proxyUrlParams.set('auth', authToken)
|
||||
|
||||
@ -51,9 +51,9 @@ export async function generateNip98Token(method: string, url: string, payloadHas
|
||||
const eventTemplate: EventTemplate & { pubkey: string } = {
|
||||
kind: 27235, // NIP-98 kind for HTTP auth
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: tags,
|
||||
tags,
|
||||
content: '',
|
||||
pubkey: pubkey,
|
||||
pubkey,
|
||||
}
|
||||
|
||||
// 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 {
|
||||
const pubkey = nostrService.getPublicKey()
|
||||
const isUnlocked = nostrAuthService.isUnlocked()
|
||||
return !!pubkey && isUnlocked
|
||||
return Boolean(pubkey) && isUnlocked
|
||||
}
|
||||
|
||||
@ -178,7 +178,7 @@ class NostrService {
|
||||
if (!this.privateKey || !this.pool || !this.publicKey) {
|
||||
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> {
|
||||
@ -199,7 +199,7 @@ class NostrService {
|
||||
return null
|
||||
}
|
||||
|
||||
return await decryptArticleContentWithKey(event.content, decryptionKey)
|
||||
return decryptArticleContentWithKey(event.content, decryptionKey)
|
||||
} catch (error) {
|
||||
console.error('Error decrypting article content', {
|
||||
eventId,
|
||||
|
||||
@ -13,7 +13,7 @@ export function parseArticleFromEvent(event: Event): Article | null {
|
||||
if (tags.type !== 'publication') {
|
||||
return null
|
||||
}
|
||||
const { previewContent } = getPreviewContent(event.content, tags.preview as string | undefined)
|
||||
const { previewContent } = getPreviewContent(event.content, tags.preview)
|
||||
return buildArticle(event, tags, previewContent)
|
||||
} catch (e) {
|
||||
console.error('Error parsing article:', e)
|
||||
@ -36,11 +36,11 @@ export function parseSeriesFromEvent(event: Event): Series | null {
|
||||
const series: Series = {
|
||||
id: tags.id ?? event.id,
|
||||
pubkey: event.pubkey,
|
||||
title: tags.title as string,
|
||||
description: tags.description as string,
|
||||
preview: (tags.preview as string | undefined) ?? event.content.substring(0, 200),
|
||||
title: tags.title,
|
||||
description: tags.description,
|
||||
preview: (tags.preview) ?? event.content.substring(0, 200),
|
||||
category,
|
||||
...(tags.coverUrl ? { coverUrl: tags.coverUrl as string } : {}),
|
||||
...(tags.coverUrl ? { coverUrl: tags.coverUrl } : {}),
|
||||
}
|
||||
series.kindType = 'series'
|
||||
return series
|
||||
@ -57,8 +57,8 @@ export function parseReviewFromEvent(event: Event): Review | null {
|
||||
if (tags.type !== 'quote') {
|
||||
return null
|
||||
}
|
||||
const articleId = tags.articleId as string | undefined
|
||||
const reviewer = tags.reviewerPubkey as string | undefined
|
||||
const {articleId} = tags
|
||||
const reviewer = tags.reviewerPubkey
|
||||
if (!articleId || !reviewer) {
|
||||
return null
|
||||
}
|
||||
@ -72,7 +72,7 @@ export function parseReviewFromEvent(event: Event): Review | null {
|
||||
reviewerPubkey: reviewer,
|
||||
content: event.content,
|
||||
createdAt: event.created_at,
|
||||
...(tags.title ? { title: tags.title as string } : {}),
|
||||
...(tags.title ? { title: tags.title } : {}),
|
||||
...(rewardedTag ? { rewarded: true } : {}),
|
||||
...(rewardAmountTag ? { rewardAmount: parseInt(rewardAmountTag[1] ?? '0', 10) } : {}),
|
||||
}
|
||||
|
||||
@ -17,7 +17,7 @@ function createPrivateMessageFilters(eventId: string, publicKey: string, authorP
|
||||
|
||||
function decryptContent(privateKey: string, event: Event): Promise<string | null> {
|
||||
return Promise.resolve(nip04.decrypt(privateKey, event.pubkey, event.content)).then((decrypted) =>
|
||||
decrypted ? decrypted : null
|
||||
(decrypted ? decrypted : null)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -67,7 +67,7 @@ export class NostrRemoteSigner {
|
||||
*/
|
||||
isAvailable(): boolean {
|
||||
const state = nostrAuthService.getState()
|
||||
return state.connected && !!state.pubkey
|
||||
return state.connected && Boolean(state.pubkey)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -87,13 +87,13 @@ export function buildTags(tags: AuthorTags | SeriesTags | PublicationTags | Quot
|
||||
const result = buildBaseTags(tags)
|
||||
|
||||
if (tags.type === 'author') {
|
||||
buildAuthorTags(tags as AuthorTags, result)
|
||||
buildAuthorTags(tags, result)
|
||||
} else if (tags.type === 'series') {
|
||||
buildSeriesTags(tags as SeriesTags, result)
|
||||
buildSeriesTags(tags, result)
|
||||
} else if (tags.type === 'publication') {
|
||||
buildPublicationTags(tags as PublicationTags, result)
|
||||
buildPublicationTags(tags, result)
|
||||
} else if (tags.type === 'quote') {
|
||||
buildQuoteTags(tags as QuoteTags, result)
|
||||
buildQuoteTags(tags, result)
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
@ -144,7 +144,7 @@ export function markNotificationAsRead(
|
||||
notifications: Notification[]
|
||||
): Notification[] {
|
||||
const updated = notifications.map((n) =>
|
||||
n.id === notificationId ? { ...n, read: true } : n
|
||||
(n.id === notificationId ? { ...n, read: true } : n)
|
||||
)
|
||||
saveNotifications(userPubkey, updated)
|
||||
return updated
|
||||
|
||||
@ -109,7 +109,7 @@ class ObjectCacheService {
|
||||
const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result
|
||||
if (cursor) {
|
||||
const obj = cursor.value as CachedObject
|
||||
if (obj && obj.hashId === hashId && !obj.hidden) {
|
||||
if (obj?.hashId === hashId && !obj.hidden) {
|
||||
objects.push(obj)
|
||||
}
|
||||
cursor.continue()
|
||||
@ -149,7 +149,7 @@ class ObjectCacheService {
|
||||
const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result
|
||||
if (cursor) {
|
||||
const obj = cursor.value as CachedObject
|
||||
if (obj && obj.event.pubkey === pubkey && !obj.hidden) {
|
||||
if (obj?.event.pubkey === pubkey && !obj.hidden) {
|
||||
objects.push(obj)
|
||||
}
|
||||
cursor.continue()
|
||||
|
||||
@ -49,7 +49,7 @@ export async function waitForArticlePayment(
|
||||
const interval = 2000
|
||||
const deadline = Date.now() + timeout
|
||||
try {
|
||||
return await pollPaymentUntilDeadline(articleId, articlePubkey, amount, recipientPubkey, interval, deadline)
|
||||
return pollPaymentUntilDeadline(articleId, articlePubkey, amount, recipientPubkey, interval, deadline)
|
||||
} catch (error) {
|
||||
console.error('Wait for payment error:', error)
|
||||
return false
|
||||
|
||||
@ -116,7 +116,7 @@ export function logPaymentResult(
|
||||
if (result.success && result.messageEventId) {
|
||||
logPaymentSuccess(articleId, recipientPubkey, amount, result.messageEventId, result.verified ?? false)
|
||||
return true
|
||||
} else {
|
||||
}
|
||||
console.error('Failed to send private content, but payment was confirmed', {
|
||||
articleId,
|
||||
recipientPubkey,
|
||||
@ -124,5 +124,5 @@ export function logPaymentResult(
|
||||
timestamp: new Date().toISOString(),
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -40,7 +40,7 @@ export class PlatformTrackingService {
|
||||
return null
|
||||
}
|
||||
|
||||
return { pool: pool as SimplePoolWithSub, authorPubkey }
|
||||
return { pool, authorPubkey }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -15,7 +15,7 @@ export function extractPresentationData(presentation: Article): {
|
||||
presentation: string
|
||||
contentDescription: string
|
||||
} {
|
||||
const content = presentation.content
|
||||
const {content} = presentation
|
||||
|
||||
// Try new format first
|
||||
const newFormatMatch = content.match(/Présentation personnelle : (.+?)(?:\nDescription de votre contenu :|$)/s)
|
||||
|
||||
@ -6,7 +6,7 @@ export async function transferReviewerPortionIfAvailable(
|
||||
request: ReviewRewardRequest,
|
||||
split: { total: number; reviewer: number; platform: number }
|
||||
): Promise<void> {
|
||||
let reviewerLightningAddress: string | undefined = request.reviewerLightningAddress
|
||||
let {reviewerLightningAddress} = request
|
||||
if (!reviewerLightningAddress) {
|
||||
const address = await lightningAddressService.getLightningAddress(request.reviewerPubkey)
|
||||
reviewerLightningAddress = address ?? undefined
|
||||
|
||||
@ -38,7 +38,7 @@ function subscribeToPresentation(pool: import('nostr-tools').SimplePool, pubkey:
|
||||
if (tags.type !== 'author') {
|
||||
return
|
||||
}
|
||||
const total = (tags.totalSponsoring as number | undefined) ?? 0
|
||||
const total = (tags.totalSponsoring) ?? 0
|
||||
finalize(total)
|
||||
})
|
||||
|
||||
|
||||
@ -2,7 +2,6 @@ import { Event, EventTemplate, finalizeEvent } from 'nostr-tools'
|
||||
import { hexToBytes } from 'nostr-tools/utils'
|
||||
import { nostrService } from './nostr'
|
||||
import { PLATFORM_NPUB } from './platformConfig'
|
||||
import type { SimplePoolWithSub } from '@/types/nostr-tools-extended'
|
||||
import { getPrimaryRelaySync } from './config'
|
||||
|
||||
export interface SponsoringTracking {
|
||||
@ -75,7 +74,7 @@ export class SponsoringTrackingService {
|
||||
if (!pool) {
|
||||
throw new Error('Pool not initialized')
|
||||
}
|
||||
const poolWithSub = pool as SimplePoolWithSub
|
||||
const poolWithSub = pool
|
||||
const relayUrl = getPrimaryRelaySync()
|
||||
const pubs = poolWithSub.publish([relayUrl], event)
|
||||
await Promise.all(pubs)
|
||||
|
||||
@ -91,7 +91,7 @@ export class IndexedDBStorage {
|
||||
...(expiresIn ? { expiresAt: now + expiresIn } : {}),
|
||||
}
|
||||
|
||||
const db = this.db
|
||||
const {db} = this
|
||||
if (!db) {
|
||||
throw new Error('Database not initialized')
|
||||
}
|
||||
@ -129,7 +129,7 @@ export class IndexedDBStorage {
|
||||
}
|
||||
|
||||
private readValue<T>(key: string, secret: string): Promise<T | null> {
|
||||
const db = this.db
|
||||
const {db} = this
|
||||
if (!db) {
|
||||
throw new Error('Database not initialized')
|
||||
}
|
||||
@ -176,7 +176,7 @@ export class IndexedDBStorage {
|
||||
throw new Error('Database not initialized')
|
||||
}
|
||||
|
||||
const db = this.db
|
||||
const {db} = this
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([STORE_NAME], 'readwrite')
|
||||
@ -203,7 +203,7 @@ export class IndexedDBStorage {
|
||||
throw new Error('Database not initialized')
|
||||
}
|
||||
|
||||
const db = this.db
|
||||
const {db} = this
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([STORE_NAME], 'readwrite')
|
||||
|
||||
@ -32,7 +32,7 @@ export function parseObjectUrl(url: string): {
|
||||
version: number | null
|
||||
} {
|
||||
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 }
|
||||
}
|
||||
|
||||
|
||||
9
lib/userConfirm.ts
Normal file
9
lib/userConfirm.ts
Normal 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)
|
||||
}
|
||||
@ -118,7 +118,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
port: url.port || (isHttps ? 443 : 80),
|
||||
path: url.pathname + url.search,
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
headers,
|
||||
timeout: 30000, // 30 seconds timeout
|
||||
}
|
||||
|
||||
@ -126,7 +126,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
// Handle redirects (301, 302, 307, 308)
|
||||
const statusCode = proxyResponse.statusCode || 500
|
||||
if ((statusCode === 301 || statusCode === 302 || statusCode === 307 || statusCode === 308) && proxyResponse.headers.location) {
|
||||
const location = proxyResponse.headers.location
|
||||
const {location} = proxyResponse.headers
|
||||
let redirectUrl: URL
|
||||
try {
|
||||
// Handle relative and absolute URLs
|
||||
@ -175,9 +175,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
}
|
||||
|
||||
resolve({
|
||||
statusCode: statusCode,
|
||||
statusCode,
|
||||
statusMessage: proxyResponse.statusMessage || 'Internal Server Error',
|
||||
body: body,
|
||||
body,
|
||||
})
|
||||
})
|
||||
proxyResponse.on('error', (error) => {
|
||||
@ -278,7 +278,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
finalUrl: currentUrl.toString(),
|
||||
status: response.statusCode,
|
||||
statusText: response.statusMessage,
|
||||
errorText: errorText,
|
||||
errorText,
|
||||
})
|
||||
|
||||
// Provide more specific error messages for common HTTP status codes
|
||||
|
||||
@ -15,7 +15,7 @@ import Image from 'next/image'
|
||||
import { CreateSeriesModal } from '@/components/CreateSeriesModal'
|
||||
import { useNostrAuth } from '@/hooks/useNostrAuth'
|
||||
|
||||
function AuthorPageHeader({ presentation }: { presentation: AuthorPresentationArticle | null }) {
|
||||
function AuthorPageHeader({ presentation }: { presentation: AuthorPresentationArticle | null }): JSX.Element | null {
|
||||
if (!presentation) {
|
||||
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
|
||||
|
||||
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 [showCreateModal, setShowCreateModal] = useState(false)
|
||||
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()
|
||||
if (!pool) {
|
||||
throw new Error('Pool not initialized')
|
||||
}
|
||||
|
||||
const [pres, seriesList, sponsoring] = await Promise.all([
|
||||
fetchAuthorPresentationFromPool(pool as import('@/types/nostr-tools-extended').SimplePoolWithSub, authorPubkey),
|
||||
fetchAuthorPresentationFromPool(pool, authorPubkey),
|
||||
getSeriesByAuthor(authorPubkey),
|
||||
getAuthorSponsoring(authorPubkey),
|
||||
])
|
||||
@ -124,14 +124,21 @@ async function loadAuthorData(authorPubkey: string) {
|
||||
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 [series, setSeries] = useState<Series[]>([])
|
||||
const [totalSponsoring, setTotalSponsoring] = useState<number>(0)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const reload = async () => {
|
||||
const reload = async (): Promise<void> => {
|
||||
if (!authorPubkey) {
|
||||
return
|
||||
}
|
||||
@ -174,7 +181,7 @@ function AuthorPageContent({
|
||||
loading: boolean
|
||||
error: string | null
|
||||
onSeriesCreated: () => void
|
||||
}) {
|
||||
}): JSX.Element {
|
||||
if (loading) {
|
||||
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 { pubkey } = router.query
|
||||
const authorPubkey = typeof pubkey === 'string' ? pubkey : ''
|
||||
|
||||
@ -6,7 +6,7 @@ import { Footer } from '@/components/Footer'
|
||||
import { useDocs, type DocLink, type DocSection } from '@/hooks/useDocs'
|
||||
import { t } from '@/lib/i18n'
|
||||
|
||||
export default function DocsPage() {
|
||||
export default function DocsPage(): JSX.Element {
|
||||
const docs: DocLink[] = [
|
||||
{
|
||||
id: 'user-guide',
|
||||
|
||||
@ -7,7 +7,7 @@ import type { Article } from '@/types/nostr'
|
||||
import type { ArticleFilters } from '@/components/ArticleFilters'
|
||||
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())
|
||||
useEffect(() => {
|
||||
const presentations = new Map<string, Article>()
|
||||
@ -21,7 +21,16 @@ function usePresentationArticles(allArticles: Article[]) {
|
||||
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 [selectedCategory, setSelectedCategory] = useState<ArticleFilters['category']>(null)
|
||||
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 presentationArticles = usePresentationArticles(allArticles)
|
||||
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(() => {
|
||||
setFilters((prev) => ({
|
||||
...prev,
|
||||
@ -63,7 +79,7 @@ function useFilteredArticles(
|
||||
searchQuery: string,
|
||||
filters: ArticleFilters,
|
||||
presentationArticles: Map<string, Article>
|
||||
) {
|
||||
): Article[] {
|
||||
return useMemo(
|
||||
() => applyFiltersAndSort(allArticlesRaw, searchQuery, filters, presentationArticles),
|
||||
[allArticlesRaw, searchQuery, filters, presentationArticles]
|
||||
@ -73,7 +89,7 @@ function useFilteredArticles(
|
||||
function useUnlockHandler(
|
||||
loadArticleContent: (id: string, pubkey: string) => Promise<Article | null>,
|
||||
setUnlockedArticles: React.Dispatch<React.SetStateAction<Set<string>>>
|
||||
) {
|
||||
): (article: Article) => Promise<void> {
|
||||
return useCallback(
|
||||
async (article: Article) => {
|
||||
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 {
|
||||
searchQuery,
|
||||
@ -133,7 +164,7 @@ function useHomeController() {
|
||||
}
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
export default function Home(): JSX.Element {
|
||||
const controller = useHomeController()
|
||||
|
||||
return (
|
||||
|
||||
@ -2,7 +2,7 @@ import Head from 'next/head'
|
||||
import Link from 'next/link'
|
||||
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 (
|
||||
<section>
|
||||
<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 (
|
||||
<LegalSection title="1. Éditeur du site">
|
||||
<p className="text-cyber-accent mb-2">
|
||||
@ -30,7 +30,7 @@ function EditorSection() {
|
||||
)
|
||||
}
|
||||
|
||||
function HostingSection() {
|
||||
function HostingSection(): JSX.Element {
|
||||
return (
|
||||
<LegalSection title="2. Hébergement">
|
||||
<p className="text-cyber-accent">
|
||||
@ -40,7 +40,7 @@ function HostingSection() {
|
||||
)
|
||||
}
|
||||
|
||||
function IntellectualPropertySection() {
|
||||
function IntellectualPropertySection(): JSX.Element {
|
||||
return (
|
||||
<LegalSection title="3. Propriété intellectuelle">
|
||||
<p className="text-cyber-accent mb-2">
|
||||
@ -53,7 +53,7 @@ function IntellectualPropertySection() {
|
||||
)
|
||||
}
|
||||
|
||||
function DataProtectionSection() {
|
||||
function DataProtectionSection(): JSX.Element {
|
||||
return (
|
||||
<LegalSection title="4. Protection des données personnelles">
|
||||
<p className="text-cyber-accent mb-2">
|
||||
@ -71,7 +71,7 @@ function DataProtectionSection() {
|
||||
)
|
||||
}
|
||||
|
||||
function ResponsibilitySection() {
|
||||
function ResponsibilitySection(): JSX.Element {
|
||||
return (
|
||||
<LegalSection title="5. Responsabilité">
|
||||
<p className="text-cyber-accent mb-2">
|
||||
@ -86,7 +86,7 @@ function ResponsibilitySection() {
|
||||
)
|
||||
}
|
||||
|
||||
function CookiesSection() {
|
||||
function CookiesSection(): JSX.Element {
|
||||
return (
|
||||
<LegalSection title="6. Cookies">
|
||||
<p className="text-cyber-accent">
|
||||
@ -97,7 +97,7 @@ function CookiesSection() {
|
||||
)
|
||||
}
|
||||
|
||||
function ApplicableLawSection() {
|
||||
function ApplicableLawSection(): JSX.Element {
|
||||
return (
|
||||
<LegalSection title="7. Loi applicable">
|
||||
<p className="text-cyber-accent">
|
||||
@ -108,7 +108,7 @@ function ApplicableLawSection() {
|
||||
)
|
||||
}
|
||||
|
||||
export default function LegalPage() {
|
||||
export default function LegalPage(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
|
||||
@ -8,11 +8,11 @@ import { useNostrAuth } from '@/hooks/useNostrAuth'
|
||||
import { useAuthorPresentation } from '@/hooks/useAuthorPresentation'
|
||||
import { t } from '@/lib/i18n'
|
||||
|
||||
function usePresentationRedirect(connected: boolean, pubkey: string | null) {
|
||||
function usePresentationRedirect(connected: boolean, pubkey: string | null): void {
|
||||
const router = useRouter()
|
||||
const { checkPresentationExists } = useAuthorPresentation(pubkey ?? null)
|
||||
|
||||
const redirectIfExists = useCallback(async () => {
|
||||
const redirectIfExists = useCallback(async (): Promise<void> => {
|
||||
if (!connected || !pubkey) {
|
||||
return
|
||||
}
|
||||
@ -27,7 +27,7 @@ function usePresentationRedirect(connected: boolean, pubkey: string | null) {
|
||||
}, [redirectIfExists])
|
||||
}
|
||||
|
||||
function PresentationLayout() {
|
||||
function PresentationLayout(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
@ -51,7 +51,7 @@ function PresentationLayout() {
|
||||
)
|
||||
}
|
||||
|
||||
export default function PresentationPage() {
|
||||
export default function PresentationPage(): JSX.Element {
|
||||
const { connected, pubkey } = useNostrAuth()
|
||||
usePresentationRedirect(connected, pubkey)
|
||||
return <PresentationLayout />
|
||||
|
||||
@ -2,7 +2,7 @@ import Head from 'next/head'
|
||||
import Link from 'next/link'
|
||||
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 (
|
||||
<section>
|
||||
<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 (
|
||||
<PrivacySection title="1. Introduction">
|
||||
<p className="text-cyber-accent">
|
||||
@ -22,7 +22,7 @@ function IntroductionSection() {
|
||||
)
|
||||
}
|
||||
|
||||
function CollectedDataSection() {
|
||||
function CollectedDataSection(): JSX.Element {
|
||||
return (
|
||||
<PrivacySection title="2. Données collectées">
|
||||
<p className="text-cyber-accent mb-2">
|
||||
@ -55,7 +55,7 @@ function CollectedDataSection() {
|
||||
)
|
||||
}
|
||||
|
||||
function ProcessingPurposeSection() {
|
||||
function ProcessingPurposeSection(): JSX.Element {
|
||||
return (
|
||||
<PrivacySection title="3. Finalité du traitement">
|
||||
<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 (
|
||||
<PrivacySection title="4. Base légale du traitement">
|
||||
<p className="text-cyber-accent">
|
||||
@ -80,7 +80,7 @@ function LegalBasisSection() {
|
||||
)
|
||||
}
|
||||
|
||||
function DataRetentionSection() {
|
||||
function DataRetentionSection(): JSX.Element {
|
||||
return (
|
||||
<PrivacySection title="5. Conservation des données">
|
||||
<p className="text-cyber-accent mb-2">
|
||||
@ -96,7 +96,7 @@ function DataRetentionSection() {
|
||||
)
|
||||
}
|
||||
|
||||
function DataSharingSection() {
|
||||
function DataSharingSection(): JSX.Element {
|
||||
return (
|
||||
<PrivacySection title="6. Partage des données">
|
||||
<p className="text-cyber-accent">
|
||||
@ -108,7 +108,7 @@ function DataSharingSection() {
|
||||
)
|
||||
}
|
||||
|
||||
function DataSecuritySection() {
|
||||
function DataSecuritySection(): JSX.Element {
|
||||
return (
|
||||
<PrivacySection title="7. Sécurité des données">
|
||||
<p className="text-cyber-accent mb-2">
|
||||
@ -126,7 +126,7 @@ function DataSecuritySection() {
|
||||
)
|
||||
}
|
||||
|
||||
function UserRightsSection() {
|
||||
function UserRightsSection(): JSX.Element {
|
||||
return (
|
||||
<PrivacySection title="8. Droits des utilisateurs">
|
||||
<p className="text-cyber-accent mb-2">
|
||||
@ -147,7 +147,7 @@ function UserRightsSection() {
|
||||
)
|
||||
}
|
||||
|
||||
function CookiesSection() {
|
||||
function CookiesSection(): JSX.Element {
|
||||
return (
|
||||
<PrivacySection title="9. Cookies">
|
||||
<p className="text-cyber-accent">
|
||||
@ -158,7 +158,7 @@ function CookiesSection() {
|
||||
)
|
||||
}
|
||||
|
||||
function ModificationsSection() {
|
||||
function ModificationsSection(): JSX.Element {
|
||||
return (
|
||||
<PrivacySection title="10. Modifications">
|
||||
<p className="text-cyber-accent">
|
||||
@ -169,7 +169,7 @@ function ModificationsSection() {
|
||||
)
|
||||
}
|
||||
|
||||
function ContactSection() {
|
||||
function ContactSection(): JSX.Element {
|
||||
return (
|
||||
<PrivacySection title="11. Contact">
|
||||
<p className="text-cyber-accent mb-2">
|
||||
@ -185,7 +185,7 @@ function ContactSection() {
|
||||
)
|
||||
}
|
||||
|
||||
export default function PrivacyPage() {
|
||||
export default function PrivacyPage(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
|
||||
@ -7,7 +7,10 @@ import { useNostrAuth } from '@/hooks/useNostrAuth'
|
||||
import { useUserArticles } from '@/hooks/useUserArticles'
|
||||
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 [loadingProfile, setLoadingProfile] = useState(true)
|
||||
|
||||
@ -20,7 +23,7 @@ function useUserProfileData(currentPubkey: string | null) {
|
||||
pubkey: currentPubkey,
|
||||
})
|
||||
|
||||
const load = async () => {
|
||||
const load = async (): Promise<void> => {
|
||||
try {
|
||||
const loadedProfile = await nostrService.getProfile(currentPubkey)
|
||||
setProfile(loadedProfile ?? createMinimalProfile())
|
||||
@ -39,7 +42,7 @@ function useUserProfileData(currentPubkey: string | null) {
|
||||
return { profile, loadingProfile }
|
||||
}
|
||||
|
||||
function useRedirectWhenDisconnected(connected: boolean, pubkey: string | null) {
|
||||
function useRedirectWhenDisconnected(connected: boolean, pubkey: string | null): void {
|
||||
const router = useRouter()
|
||||
useEffect(() => {
|
||||
if (!connected || !pubkey) {
|
||||
@ -48,7 +51,23 @@ function useRedirectWhenDisconnected(connected: boolean, pubkey: string | null)
|
||||
}, [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 [searchQuery, setSearchQuery] = useState('')
|
||||
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 { connected, currentPubkey } = controller
|
||||
|
||||
|
||||
@ -16,7 +16,7 @@ function PublishHeader() {
|
||||
)
|
||||
}
|
||||
|
||||
function PublishHero({ onBack }: { onBack: () => void }) {
|
||||
function PublishHero({ onBack }: { onBack: () => void }): JSX.Element {
|
||||
return (
|
||||
<div className="mb-6">
|
||||
<button
|
||||
@ -31,12 +31,12 @@ function PublishHero({ onBack }: { onBack: () => void }) {
|
||||
)
|
||||
}
|
||||
|
||||
export default function PublishPage() {
|
||||
export default function PublishPage(): JSX.Element {
|
||||
const router = useRouter()
|
||||
const { pubkey } = useNostrAuth()
|
||||
const [seriesOptions, setSeriesOptions] = useState<{ id: string; title: string }[]>([])
|
||||
|
||||
const handlePublishSuccess = () => {
|
||||
const handlePublishSuccess = (): void => {
|
||||
setTimeout(() => {
|
||||
void router.push('/')
|
||||
}, 2000)
|
||||
@ -47,7 +47,7 @@ export default function PublishPage() {
|
||||
setSeriesOptions([])
|
||||
return
|
||||
}
|
||||
const load = async () => {
|
||||
const load = async (): Promise<void> => {
|
||||
const items = await getSeriesByAuthor(pubkey)
|
||||
setSeriesOptions(items.map((s) => ({ id: s.id, title: s.title })))
|
||||
}
|
||||
@ -76,7 +76,7 @@ function PublishLayout({
|
||||
onBack: () => void
|
||||
onPublishSuccess: () => void
|
||||
seriesOptions: { id: string; title: string }[]
|
||||
}) {
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<main className="min-h-screen bg-gray-50">
|
||||
<header className="bg-white shadow-sm">
|
||||
|
||||
@ -11,7 +11,7 @@ import { t } from '@/lib/i18n'
|
||||
import Image from 'next/image'
|
||||
import { ArticleReviews } from '@/components/ArticleReviews'
|
||||
|
||||
function SeriesHeader({ series }: { series: Series }) {
|
||||
function SeriesHeader({ series }: { series: Series }): JSX.Element {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{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 { id } = router.query
|
||||
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) {
|
||||
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 [articles, setArticles] = useState<Article[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
@ -104,7 +110,7 @@ function useSeriesPageData(seriesId: string) {
|
||||
if (!seriesId) {
|
||||
return
|
||||
}
|
||||
const load = async () => {
|
||||
const load = async (): Promise<void> => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
|
||||
@ -5,7 +5,7 @@ import { Nip95ConfigManager } from '@/components/Nip95ConfigManager'
|
||||
import { KeyManagementManager } from '@/components/KeyManagementManager'
|
||||
import { t } from '@/lib/i18n'
|
||||
|
||||
export default function SettingsPage() {
|
||||
export default function SettingsPage(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
|
||||
@ -2,7 +2,7 @@ import Head from 'next/head'
|
||||
import Link from 'next/link'
|
||||
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 (
|
||||
<section>
|
||||
<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 (
|
||||
<TermsSection title="1. Objet">
|
||||
<p className="text-cyber-accent">
|
||||
@ -22,7 +22,7 @@ function ObjectSection() {
|
||||
)
|
||||
}
|
||||
|
||||
function AcceptanceSection() {
|
||||
function AcceptanceSection(): JSX.Element {
|
||||
return (
|
||||
<TermsSection title="2. Acceptation des CGU">
|
||||
<p className="text-cyber-accent">
|
||||
@ -33,7 +33,7 @@ function AcceptanceSection() {
|
||||
)
|
||||
}
|
||||
|
||||
function ServiceDescriptionSection() {
|
||||
function ServiceDescriptionSection(): JSX.Element {
|
||||
return (
|
||||
<TermsSection title="3. Description du service">
|
||||
<p className="text-cyber-accent mb-2">
|
||||
@ -49,7 +49,7 @@ function ServiceDescriptionSection() {
|
||||
)
|
||||
}
|
||||
|
||||
function UserObligationsSection() {
|
||||
function UserObligationsSection(): JSX.Element {
|
||||
return (
|
||||
<TermsSection title="4. Obligations de l'utilisateur">
|
||||
<p className="text-cyber-accent mb-2">L'utilisateur s'engage à :</p>
|
||||
@ -64,7 +64,7 @@ function UserObligationsSection() {
|
||||
)
|
||||
}
|
||||
|
||||
function ResponsibilitySection() {
|
||||
function ResponsibilitySection(): JSX.Element {
|
||||
return (
|
||||
<TermsSection title="5. Responsabilité">
|
||||
<p className="text-cyber-accent mb-2">
|
||||
@ -83,7 +83,7 @@ function ResponsibilitySection() {
|
||||
)
|
||||
}
|
||||
|
||||
function FinancialTransactionsSection() {
|
||||
function FinancialTransactionsSection(): JSX.Element {
|
||||
return (
|
||||
<TermsSection title="6. Transactions financières">
|
||||
<p className="text-cyber-accent mb-2">
|
||||
@ -108,7 +108,7 @@ function FinancialTransactionsSection() {
|
||||
)
|
||||
}
|
||||
|
||||
function IntellectualPropertySection() {
|
||||
function IntellectualPropertySection(): JSX.Element {
|
||||
return (
|
||||
<TermsSection title="7. Propriété intellectuelle">
|
||||
<p className="text-cyber-accent">
|
||||
@ -119,7 +119,7 @@ function IntellectualPropertySection() {
|
||||
)
|
||||
}
|
||||
|
||||
function ModificationSection() {
|
||||
function ModificationSection(): JSX.Element {
|
||||
return (
|
||||
<TermsSection title="8. Modification des CGU">
|
||||
<p className="text-cyber-accent">
|
||||
@ -130,7 +130,7 @@ function ModificationSection() {
|
||||
)
|
||||
}
|
||||
|
||||
function TerminationSection() {
|
||||
function TerminationSection(): JSX.Element {
|
||||
return (
|
||||
<TermsSection title="9. Résiliation">
|
||||
<p className="text-cyber-accent">
|
||||
@ -141,7 +141,7 @@ function TerminationSection() {
|
||||
)
|
||||
}
|
||||
|
||||
function ApplicableLawSection() {
|
||||
function ApplicableLawSection(): JSX.Element {
|
||||
return (
|
||||
<TermsSection title="10. Droit applicable">
|
||||
<p className="text-cyber-accent">
|
||||
@ -151,7 +151,7 @@ function ApplicableLawSection() {
|
||||
)
|
||||
}
|
||||
|
||||
export default function TermsPage() {
|
||||
export default function TermsPage(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
|
||||
@ -22,10 +22,19 @@ try {
|
||||
// If next lint fails, try eslint directly with flat config
|
||||
console.log('Falling back to eslint directly...')
|
||||
try {
|
||||
execSync('npx eslint . --ext .ts,.tsx', {
|
||||
stdio: 'inherit',
|
||||
cwd: projectRoot,
|
||||
})
|
||||
// Try auto-fix first
|
||||
try {
|
||||
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) {
|
||||
console.error('Both next lint and eslint failed')
|
||||
process.exit(1)
|
||||
|
||||
@ -32,7 +32,7 @@ export function createSubscription(
|
||||
|
||||
const subscription = pool.subscribe(
|
||||
relays,
|
||||
filters[0] || {},
|
||||
filters[0] ?? {},
|
||||
{
|
||||
onevent: (event: Event) => {
|
||||
events.push(event)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user