diff --git a/add-ssh-key-plink.sh b/add-ssh-key-plink.sh new file mode 100644 index 0000000..4254f6c --- /dev/null +++ b/add-ssh-key-plink.sh @@ -0,0 +1,47 @@ +#!/bin/bash + +# Script to add SSH public key to remote server using plink (PuTTY) +# Usage: ./add-ssh-key-plink.sh + +set -e + +REMOTE_HOST="155.133.129.88" +REMOTE_USER="admin" +REMOTE_PASSWORD="GHDKkpNUNuFePQb4KZ4%" +SSH_KEY="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFURI5emj/BtYj6fgO6JnqH8csxJeSlyWkLs1DcPdoVp titouan.tabere@gmail.com" + +echo "Adding SSH key to ${REMOTE_USER}@${REMOTE_HOST} using plink..." + +# Remote script to execute +REMOTE_SCRIPT="mkdir -p ~/.ssh && chmod 700 ~/.ssh && AUTH_KEYS_FILE=\$HOME/.ssh/authorized_keys && KEY_TO_ADD='${SSH_KEY}' && if [ -f \"\$AUTH_KEYS_FILE\" ] && grep -qF \"\$KEY_TO_ADD\" \"\$AUTH_KEYS_FILE\"; then echo 'SSH key already exists in authorized_keys'; else echo \"\$KEY_TO_ADD\" >> \"\$AUTH_KEYS_FILE\" && echo 'SSH key added successfully'; fi && chmod 600 \"\$AUTH_KEYS_FILE\" && echo 'SSH key setup completed'" + +# Check if plink is available +if command -v plink &> /dev/null || [ -f "/c/Program Files/PuTTY/plink.exe" ]; then + PLINK_CMD="plink" + if [ -f "/c/Program Files/PuTTY/plink.exe" ]; then + PLINK_CMD="/c/Program Files/PuTTY/plink.exe" + fi + + echo "Using plink for authentication..." + + # Use plink with -pw flag to pass password + # Accept host key using the fingerprint provided + "$PLINK_CMD" -ssh -pw "${REMOTE_PASSWORD}" -batch -hostkey "SHA256:QtU+b4Fx3PSYDUwrgpXcZKZQCe9N8yZWnxY43Wh/bUA" \ + "${REMOTE_USER}@${REMOTE_HOST}" "${REMOTE_SCRIPT}" + +else + echo "Error: plink not found." + echo "Please ensure PuTTY is installed or use the manual method below." + echo "" + echo "Manual method - run these commands:" + echo " ssh ${REMOTE_USER}@${REMOTE_HOST}" + echo "" + echo "Then execute on the remote server:" + echo " mkdir -p ~/.ssh" + echo " chmod 700 ~/.ssh" + echo " echo '${SSH_KEY}' >> ~/.ssh/authorized_keys" + echo " chmod 600 ~/.ssh/authorized_keys" + exit 1 +fi + +echo "Done!" diff --git a/add-ssh-key.sh b/add-ssh-key.sh new file mode 100644 index 0000000..b92d6b8 --- /dev/null +++ b/add-ssh-key.sh @@ -0,0 +1,47 @@ +#!/bin/bash + +# Script to add SSH public key to remote server using plink (PuTTY) +# Usage: ./add-ssh-key.sh + +set -e + +REMOTE_HOST="155.133.129.88" +REMOTE_USER="admin" +REMOTE_PASSWORD="GHDKkpNUNuFePQb4KZ4%" +SSH_KEY="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFURI5emj/BtYj6fgO6JnqH8csxJeSlyWkLs1DcPdoVp titouan.tabere@gmail.com" + +echo "Adding SSH key to ${REMOTE_USER}@${REMOTE_HOST} using plink..." + +# Remote script to execute +REMOTE_SCRIPT="mkdir -p ~/.ssh && chmod 700 ~/.ssh && AUTH_KEYS_FILE=\$HOME/.ssh/authorized_keys && KEY_TO_ADD='${SSH_KEY}' && if [ -f \"\$AUTH_KEYS_FILE\" ] && grep -qF \"\$KEY_TO_ADD\" \"\$AUTH_KEYS_FILE\"; then echo 'SSH key already exists in authorized_keys'; else echo \"\$KEY_TO_ADD\" >> \"\$AUTH_KEYS_FILE\" && echo 'SSH key added successfully'; fi && chmod 600 \"\$AUTH_KEYS_FILE\" && echo 'SSH key setup completed'" + +# Check if plink is available +if command -v plink &> /dev/null || [ -f "/c/Program Files/PuTTY/plink.exe" ]; then + PLINK_CMD="plink" + if [ -f "/c/Program Files/PuTTY/plink.exe" ]; then + PLINK_CMD="/c/Program Files/PuTTY/plink.exe" + fi + + echo "Using plink for authentication..." + + # Use plink with -pw flag to pass password + # Accept host key using the fingerprint provided + "$PLINK_CMD" -ssh -pw "${REMOTE_PASSWORD}" -batch -hostkey "SHA256:QtU+b4Fx3PSYDUwrgpXcZKZQCe9N8yZWnxY43Wh/bUA" \ + "${REMOTE_USER}@${REMOTE_HOST}" "${REMOTE_SCRIPT}" + +else + echo "Error: plink not found." + echo "Please ensure PuTTY is installed or use the manual method below." + echo "" + echo "Manual method - run these commands:" + echo " ssh ${REMOTE_USER}@${REMOTE_HOST}" + echo "" + echo "Then execute on the remote server:" + echo " mkdir -p ~/.ssh" + echo " chmod 700 ~/.ssh" + echo " echo '${SSH_KEY}' >> ~/.ssh/authorized_keys" + echo " chmod 600 ~/.ssh/authorized_keys" + exit 1 +fi + +echo "Done!" diff --git a/components/ArticleCard.tsx b/components/ArticleCard.tsx index 82531a6..873b680 100644 --- a/components/ArticleCard.tsx +++ b/components/ArticleCard.tsx @@ -24,8 +24,8 @@ function ArticleMeta({ }) { return ( <> - {error &&

{error}

} -
+ {error &&

{error}

} +
Published {new Date(article.createdAt * 1000).toLocaleDateString()}
{paymentInvoice && ( @@ -40,7 +40,7 @@ function ArticleMeta({ } export function ArticleCard({ article, onUnlock }: ArticleCardProps) { - const { connected, pubkey } = useNostrConnect() + const { pubkey, connect } = useNostrConnect() const { loading, error, @@ -50,15 +50,14 @@ export function ArticleCard({ article, onUnlock }: ArticleCardProps) { handleCloseModal, } = useArticlePayment(article, pubkey ?? null, () => { onUnlock?.(article) - }) + }, connect) return ( -
-

{article.title}

-
+
+

{article.title}

+
{ void handleUnlock() diff --git a/components/ArticleEditor.tsx b/components/ArticleEditor.tsx index 657d0d6..a4bb994 100644 --- a/components/ArticleEditor.tsx +++ b/components/ArticleEditor.tsx @@ -11,13 +11,6 @@ interface ArticleEditorProps { onSelectSeries?: ((seriesId: string | undefined) => void) | undefined } -function NotConnectedMessage() { - return ( -
-

Please connect with Nostr to publish articles

-
- ) -} function SuccessMessage() { return ( @@ -29,7 +22,7 @@ function SuccessMessage() { } export function ArticleEditor({ onPublishSuccess, onCancel, seriesOptions, onSelectSeries }: ArticleEditorProps) { - const { connected, pubkey } = useNostrConnect() + const { connected, pubkey, connect } = useNostrConnect() const { loading, error, success, publishArticle } = useArticlePublishing(pubkey ?? null) const [draft, setDraft] = useState({ title: '', @@ -39,11 +32,7 @@ export function ArticleEditor({ onPublishSuccess, onCancel, seriesOptions, onSel media: [], }) - const submit = buildSubmitHandler(publishArticle, draft, onPublishSuccess) - - if (!connected) { - return - } + const submit = buildSubmitHandler(publishArticle, draft, onPublishSuccess, connect, connected) if (success) { return @@ -69,9 +58,15 @@ export function ArticleEditor({ onPublishSuccess, onCancel, seriesOptions, onSel function buildSubmitHandler( publishArticle: (draft: ArticleDraft) => Promise, draft: ArticleDraft, - onPublishSuccess?: (articleId: string) => void + onPublishSuccess?: (articleId: string) => void, + connect?: () => Promise, + connected?: boolean ) { return async () => { + if (!connected && connect) { + await connect() + return + } const articleId = await publishArticle(draft) if (articleId) { onPublishSuccess?.(articleId) diff --git a/components/ArticleEditorForm.tsx b/components/ArticleEditorForm.tsx index afab2cc..8086fff 100644 --- a/components/ArticleEditorForm.tsx +++ b/components/ArticleEditorForm.tsx @@ -210,13 +210,13 @@ const ArticleFieldsRight = ({
onDraftChange({ ...draft, zapAmount: value as number })} required type="number" min={1} - helpText="Montant en satoshis pour débloquer le contenu complet" + helpText="Montant de sponsoring en satoshis pour débloquer le contenu complet (zap uniquement)" />
) diff --git a/components/ArticleFilters.tsx b/components/ArticleFilters.tsx index b965fe5..a4e9a00 100644 --- a/components/ArticleFilters.tsx +++ b/components/ArticleFilters.tsx @@ -1,11 +1,13 @@ +import { useState, useEffect, useRef } from 'react' +import Image from 'next/image' import type { Article } from '@/types/nostr' +import { useAuthorsProfiles } from '@/hooks/useAuthorsProfiles' +import { generateMnemonicIcons } from '@/lib/mnemonicIcons' -export type SortOption = 'newest' | 'oldest' | 'price-low' | 'price-high' +export type SortOption = 'newest' | 'oldest' export interface ArticleFilters { authorPubkey: string | null - minPrice: number | null - maxPrice: number | null sortBy: SortOption category: 'science-fiction' | 'scientific-research' | 'all' | null } @@ -18,17 +20,22 @@ interface ArticleFiltersProps { interface FiltersData { authors: string[] - minAvailablePrice: number - maxAvailablePrice: number } function useFiltersData(articles: Article[]): FiltersData { - const authors = Array.from(new Map(articles.map((a) => [a.pubkey, a.pubkey])).values()) - const prices = articles.map((a) => a.zapAmount).sort((a, b) => a - b) + const authorArticleCount = new Map() + articles.forEach((article) => { + if (!article.isPresentation) { + const count = authorArticleCount.get(article.pubkey) ?? 0 + authorArticleCount.set(article.pubkey, count + 1) + } + }) + const authors = Array.from(authorArticleCount.keys()).filter((pubkey) => { + const count = authorArticleCount.get(pubkey) ?? 0 + return count > 0 + }) return { authors, - minAvailablePrice: prices[0] ?? 0, - maxAvailablePrice: prices[prices.length - 1] ?? 1000, } } @@ -44,26 +51,8 @@ function FiltersGrid({ const update = (patch: Partial) => onFiltersChange({ ...filters, ...patch }) return ( -
+
update({ authorPubkey: value })} /> - update({ minPrice: value })} - /> - update({ maxPrice: value })} - /> update({ sortBy: value })} />
) @@ -78,9 +67,9 @@ function FiltersHeader({ }) { return (
-

Filters & Sort

+

Filters & Sort

{hasActiveFilters && ( - )} @@ -97,60 +86,180 @@ function AuthorFilter({ value: string | null onChange: (value: string | null) => void }) { + const { profiles, loading } = useAuthorsProfiles(authors) + const [isOpen, setIsOpen] = useState(false) + const dropdownRef = useRef(null) + const buttonRef = useRef(null) + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + dropdownRef.current && + buttonRef.current && + !dropdownRef.current.contains(event.target as Node) && + !buttonRef.current.contains(event.target as Node) + ) { + setIsOpen(false) + } + } + + const handleEscape = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + setIsOpen(false) + buttonRef.current?.focus() + } + } + + if (isOpen) { + document.addEventListener('mousedown', handleClickOutside) + document.addEventListener('keydown', handleEscape) + } + + return () => { + document.removeEventListener('mousedown', handleClickOutside) + document.removeEventListener('keydown', handleEscape) + } + }, [isOpen]) + + const getDisplayName = (pubkey: string): string => { + const profile = profiles.get(pubkey) + return profile?.name ?? `${pubkey.substring(0, 8)}...${pubkey.substring(pubkey.length - 8)}` + } + + const getPicture = (pubkey: string): string | undefined => { + return profiles.get(pubkey)?.picture + } + + const getMnemonicIcons = (pubkey: string): string[] => { + return generateMnemonicIcons(pubkey) + } + + const selectedAuthor = value ? profiles.get(value) : null + const selectedDisplayName = value ? getDisplayName(value) : 'All authors' + return ( -
-