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