8 changed files with 268 additions and 0 deletions
@ -0,0 +1,102 @@
@@ -0,0 +1,102 @@
|
||||
import { Controller } from '@hotwired/stimulus'; |
||||
|
||||
export default class extends Controller { |
||||
static targets = ['publishButton', 'status']; |
||||
static values = { |
||||
publishUrl: String, |
||||
csrfToken: String, |
||||
eventData: Object |
||||
}; |
||||
|
||||
connect() { |
||||
console.log('Tabular data publish controller connected'); |
||||
} |
||||
|
||||
async publish(event) { |
||||
event.preventDefault(); |
||||
|
||||
if (!this.publishUrlValue) { |
||||
this.showError('Publish URL is not configured'); |
||||
return; |
||||
} |
||||
if (!this.csrfTokenValue) { |
||||
this.showError('Missing CSRF token'); |
||||
return; |
||||
} |
||||
|
||||
if (!window.nostr) { |
||||
this.showError('Nostr extension not found'); |
||||
return; |
||||
} |
||||
|
||||
this.publishButtonTarget.disabled = true; |
||||
this.showStatus('Requesting signature from Nostr extension...'); |
||||
|
||||
try { |
||||
// Prepare the event data
|
||||
const eventData = this.eventDataValue; |
||||
delete eventData.sig; // Remove sig if present
|
||||
delete eventData.id; // Remove id if present
|
||||
|
||||
// Sign the event with Nostr extension
|
||||
const signedEvent = await window.nostr.signEvent(eventData); |
||||
|
||||
this.showStatus('Publishing tabular data...'); |
||||
|
||||
// Send to backend
|
||||
await this.sendToBackend(signedEvent); |
||||
|
||||
this.showSuccess('Tabular data published successfully!'); |
||||
|
||||
// Redirect to the event page
|
||||
setTimeout(() => { |
||||
window.location.href = `/e/${signedEvent.id}`; |
||||
}, 2000); |
||||
|
||||
} catch (error) { |
||||
console.error('Publishing error:', error); |
||||
this.showError(`Publishing failed: ${error.message}`); |
||||
} finally { |
||||
this.publishButtonTarget.disabled = false; |
||||
} |
||||
} |
||||
|
||||
async sendToBackend(signedEvent) { |
||||
const response = await fetch(this.publishUrlValue, { |
||||
method: 'POST', |
||||
headers: { |
||||
'Content-Type': 'application/json', |
||||
'X-Requested-With': 'XMLHttpRequest', |
||||
'X-CSRF-TOKEN': this.csrfTokenValue |
||||
}, |
||||
body: JSON.stringify({ |
||||
event: signedEvent |
||||
}) |
||||
}); |
||||
|
||||
if (!response.ok) { |
||||
const errorData = await response.json().catch(() => ({})); |
||||
throw new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`); |
||||
} |
||||
|
||||
return await response.json(); |
||||
} |
||||
|
||||
showStatus(message) { |
||||
if (this.hasStatusTarget) { |
||||
this.statusTarget.innerHTML = `<div class="alert alert-info">${message}</div>`; |
||||
} |
||||
} |
||||
|
||||
showSuccess(message) { |
||||
if (this.hasStatusTarget) { |
||||
this.statusTarget.innerHTML = `<div class="alert alert-success">${message}</div>`; |
||||
} |
||||
} |
||||
|
||||
showError(message) { |
||||
if (this.hasStatusTarget) { |
||||
this.statusTarget.innerHTML = `<div class="alert alert-danger">${message}</div>`; |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,96 @@
@@ -0,0 +1,96 @@
|
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
namespace App\Controller; |
||||
|
||||
use App\Enum\KindsEnum; |
||||
use App\Form\TabularDataType; |
||||
use App\Service\NostrClient; |
||||
use swentel\nostr\Event\Event; |
||||
use swentel\nostr\Sign\Sign; |
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; |
||||
use Symfony\Component\HttpFoundation\Request; |
||||
use Symfony\Component\HttpFoundation\Response; |
||||
use Symfony\Component\Routing\Attribute\Route; |
||||
|
||||
class TabularDataController extends AbstractController |
||||
{ |
||||
#[Route('/tabular-data', name: 'tabular_data_publish')] |
||||
public function publish(Request $request, NostrClient $nostrClient): Response |
||||
{ |
||||
$user = $this->getUser(); |
||||
if (!$user) { |
||||
return $this->redirectToRoute('login'); |
||||
} |
||||
|
||||
$form = $this->createForm(TabularDataType::class); |
||||
$form->handleRequest($request); |
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) { |
||||
$data = $form->getData(); |
||||
|
||||
// Create the event |
||||
$event = new Event(); |
||||
$event->setKind(KindsEnum::TABULAR_DATA->value); |
||||
$event->setContent($data['csvContent']); |
||||
|
||||
// Add tags |
||||
$tags = [ |
||||
['title', $data['title']], |
||||
['m', 'text/csv'], |
||||
['M', 'text/csv; charset=utf-8'], |
||||
]; |
||||
|
||||
if (!empty($data['license'])) { |
||||
$tags[] = ['license', $data['license']]; |
||||
} |
||||
|
||||
if (!empty($data['units'])) { |
||||
// Parse units, e.g., "col=2,EH/s" -> ['unit', 'col=2', 'EH/s'] |
||||
$unitParts = explode(',', $data['units'], 2); |
||||
if (count($unitParts) == 2) { |
||||
$tags[] = ['unit', trim($unitParts[0]), trim($unitParts[1])]; |
||||
} |
||||
} |
||||
|
||||
foreach ($tags as $tag) { |
||||
$event->addTag($tag); |
||||
} |
||||
|
||||
// For now, just render the event JSON |
||||
return $this->render('tabular_data/preview.html.twig', [ |
||||
'event' => $event, |
||||
'data' => $data, |
||||
]); |
||||
} |
||||
|
||||
return $this->render('tabular_data/publish.html.twig', [ |
||||
'form' => $form->createView(), |
||||
]); |
||||
} |
||||
|
||||
#[Route('/tabular-data/publish', name: 'tabular_data_publish_event', methods: ['POST'])] |
||||
public function publishEvent(Request $request, NostrClient $nostrClient): Response |
||||
{ |
||||
$data = json_decode($request->getContent(), true); |
||||
if (!$data || !isset($data['event'])) { |
||||
return $this->json(['error' => 'Invalid data'], 400); |
||||
} |
||||
|
||||
$signedEvent = $data['event']; |
||||
|
||||
// Validate the event |
||||
if ($signedEvent['kind'] !== KindsEnum::TABULAR_DATA->value) { |
||||
return $this->json(['error' => 'Invalid event kind'], 400); |
||||
} |
||||
|
||||
// Publish the event |
||||
try { |
||||
$nostrClient->publishEvent($signedEvent, ['wss://relay.damus.io', 'wss://nos.lol']); |
||||
return $this->json(['success' => true, 'eventId' => $signedEvent['id']]); |
||||
} catch (\Exception $e) { |
||||
return $this->json(['error' => 'Publishing failed: ' . $e->getMessage()], 500); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,43 @@
@@ -0,0 +1,43 @@
|
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
namespace App\Form; |
||||
|
||||
use Symfony\Component\Form\AbstractType; |
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType; |
||||
use Symfony\Component\Form\Extension\Core\Type\TextType; |
||||
use Symfony\Component\Form\FormBuilderInterface; |
||||
use Symfony\Component\OptionsResolver\OptionsResolver; |
||||
|
||||
class TabularDataType extends AbstractType |
||||
{ |
||||
public function buildForm(FormBuilderInterface $builder, array $options): void |
||||
{ |
||||
$builder |
||||
->add('title', TextType::class, [ |
||||
'label' => 'Title', |
||||
'required' => true, |
||||
]) |
||||
->add('csvContent', TextareaType::class, [ |
||||
'label' => 'CSV Content', |
||||
'required' => true, |
||||
'attr' => ['rows' => 10, 'placeholder' => 'date,hashrate\n2025-10-01,795\n2025-10-02,802'], |
||||
]) |
||||
->add('license', TextType::class, [ |
||||
'label' => 'License (optional)', |
||||
'required' => false, |
||||
]) |
||||
->add('units', TextType::class, [ |
||||
'label' => 'Units (optional, e.g., col=2,EH/s)', |
||||
'required' => false, |
||||
]); |
||||
} |
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void |
||||
{ |
||||
$resolver->setDefaults([ |
||||
// Configure your form options here |
||||
]); |
||||
} |
||||
} |
||||
@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
|
||||
{% extends 'layout.html.twig' %} |
||||
|
||||
{% block title %}Preview Tabular Data Event{% endblock %} |
||||
|
||||
{% block body %} |
||||
<div class="w-container" |
||||
data-controller="tabular-publish" |
||||
data-tabular-publish-publish-url-value="{{ path('tabular_data_publish_event') }}" |
||||
data-tabular-publish-csrf-token-value="{{ csrf_token('tabular_publish') }}" |
||||
data-tabular-publish-event-data-value="{{ event|json_encode }}"> |
||||
<div class="card"> |
||||
<div class="card-header"> |
||||
<h1 class="card-title">Event Preview</h1> |
||||
</div> |
||||
<div class="card-body"> |
||||
<p>Here is the generated event. Click "Sign and Publish" to sign with your Nostr key and publish to relays.</p> |
||||
<pre>{{ event|json_encode(constant('JSON_PRETTY_PRINT')) }}</pre> |
||||
<div data-tabular-publish-target="status"></div> |
||||
<button data-tabular-publish-target="publishButton" data-action="tabular-publish#publish" class="btn btn-primary">Sign and Publish</button> |
||||
<a href="{{ path('tabular_data_publish') }}" class="btn btn-secondary">Back</a> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
{% endblock %} |
||||
Loading…
Reference in new issue