Add feed
This commit is contained in:
parent
e27fb1964f
commit
13597cd9da
38
src/feeds.rs
38
src/feeds.rs
@ -4,6 +4,7 @@ use rocket::serde::{json::Json, Deserialize, Serialize};
|
|||||||
use rocket_db_pools::Connection;
|
use rocket_db_pools::Connection;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
use reqwest;
|
||||||
|
|
||||||
use crate::Db;
|
use crate::Db;
|
||||||
use crate::user::AuthenticatedUser;
|
use crate::user::AuthenticatedUser;
|
||||||
@ -36,8 +37,7 @@ impl Feed {
|
|||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(crate = "rocket::serde")]
|
#[serde(crate = "rocket::serde")]
|
||||||
pub struct NewFeed {
|
pub struct NewFeed {
|
||||||
pub name: String,
|
pub url: Url, // Only URL is required now
|
||||||
pub url: Url, // Changed from String to Url
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/feeds", data = "<new_feed>")]
|
#[post("/feeds", data = "<new_feed>")]
|
||||||
@ -47,13 +47,41 @@ pub async fn create_feed(
|
|||||||
user: AuthenticatedUser,
|
user: AuthenticatedUser,
|
||||||
) -> Result<Json<Feed>, Status> {
|
) -> Result<Json<Feed>, Status> {
|
||||||
let new_feed = new_feed.into_inner();
|
let new_feed = new_feed.into_inner();
|
||||||
|
|
||||||
// URL validation only needs to check scheme now
|
|
||||||
if !new_feed.url.scheme().eq("http") && !new_feed.url.scheme().eq("https") {
|
if !new_feed.url.scheme().eq("http") && !new_feed.url.scheme().eq("https") {
|
||||||
|
eprintln!("Invalid URL scheme: {}", new_feed.url.scheme());
|
||||||
return Err(Status::UnprocessableEntity);
|
return Err(Status::UnprocessableEntity);
|
||||||
}
|
}
|
||||||
|
|
||||||
let feed = Feed::new(new_feed.name, new_feed.url, user.user_id);
|
// Fetch the feed content
|
||||||
|
let response = reqwest::get(new_feed.url.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
eprintln!("Failed to fetch feed: {}", e);
|
||||||
|
Status::UnprocessableEntity
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let content = response.text()
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
eprintln!("Failed to read response body: {}", e);
|
||||||
|
Status::UnprocessableEntity
|
||||||
|
})?;
|
||||||
|
|
||||||
|
|
||||||
|
// Parse the feed
|
||||||
|
let feed_data = feed_rs::parser::parse(content.as_bytes())
|
||||||
|
.map_err(|e| {
|
||||||
|
eprintln!("Failed to parse feed content: {}", e);
|
||||||
|
Status::UnprocessableEntity
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Use the feed title as the name, or URL if no title is available
|
||||||
|
let name = feed_data.title
|
||||||
|
.map(|t| t.content)
|
||||||
|
.unwrap_or_else(|| new_feed.url.host_str().unwrap_or("Unknown").to_string());
|
||||||
|
|
||||||
|
let feed = Feed::new(name, new_feed.url, user.user_id);
|
||||||
|
|
||||||
let query = sqlx::query(
|
let query = sqlx::query(
|
||||||
"INSERT INTO feeds (feed_id, name, url, user_id, added_time, last_checked_time)
|
"INSERT INTO feeds (feed_id, name, url, user_id, added_time, last_checked_time)
|
||||||
|
@ -260,4 +260,30 @@ body.with-sidebar {
|
|||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
top: 3rem;
|
top: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Spinner and loading state */
|
||||||
|
.spinner {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
margin: 0 auto 10px;
|
||||||
|
border: 3px solid #f3f3f3;
|
||||||
|
border-top: 3px solid var(--primary-red);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-message {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button states */
|
||||||
|
button:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
}
|
}
|
@ -20,22 +20,35 @@ const cancelButton = document.getElementById('cancelAddFeed');
|
|||||||
const confirmButton = document.getElementById('confirmAddFeed');
|
const confirmButton = document.getElementById('confirmAddFeed');
|
||||||
const addFeedForm = document.getElementById('addFeedForm');
|
const addFeedForm = document.getElementById('addFeedForm');
|
||||||
const errorMessage = document.getElementById('feedErrorMessage');
|
const errorMessage = document.getElementById('feedErrorMessage');
|
||||||
|
const loadingMessage = document.getElementById('loadingMessage');
|
||||||
|
|
||||||
function showModal() {
|
function showModal() {
|
||||||
modal.classList.add('show');
|
modal.classList.add('show');
|
||||||
errorMessage.style.display = 'none';
|
errorMessage.style.display = 'none';
|
||||||
|
loadingMessage.style.display = 'none';
|
||||||
addFeedForm.reset();
|
addFeedForm.reset();
|
||||||
|
confirmButton.disabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideModal() {
|
function hideModal() {
|
||||||
modal.classList.remove('show');
|
modal.classList.remove('show');
|
||||||
errorMessage.style.display = 'none';
|
errorMessage.style.display = 'none';
|
||||||
|
loadingMessage.style.display = 'none';
|
||||||
addFeedForm.reset();
|
addFeedForm.reset();
|
||||||
|
confirmButton.disabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function showError(message) {
|
function showError(message) {
|
||||||
errorMessage.textContent = message;
|
errorMessage.textContent = message;
|
||||||
errorMessage.style.display = 'block';
|
errorMessage.style.display = 'block';
|
||||||
|
loadingMessage.style.display = 'none';
|
||||||
|
confirmButton.disabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showLoading() {
|
||||||
|
loadingMessage.style.display = 'block';
|
||||||
|
errorMessage.style.display = 'none';
|
||||||
|
confirmButton.disabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
addFeedButton.addEventListener('click', showModal);
|
addFeedButton.addEventListener('click', showModal);
|
||||||
@ -49,21 +62,22 @@ modal.addEventListener('click', (e) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
confirmButton.addEventListener('click', async () => {
|
confirmButton.addEventListener('click', async () => {
|
||||||
const name = document.getElementById('feedName').value.trim();
|
|
||||||
const url = document.getElementById('feedUrl').value.trim();
|
const url = document.getElementById('feedUrl').value.trim();
|
||||||
|
|
||||||
if (!name || !url) {
|
if (!url) {
|
||||||
showError('Please fill in all fields');
|
showError('Please enter a feed URL');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showLoading();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/feeds', {
|
const response = await fetch('/feeds', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ name, url }),
|
body: JSON.stringify({ url }),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
@ -72,13 +86,12 @@ confirmButton.addEventListener('click', async () => {
|
|||||||
// For now, we'll just reload the page
|
// For now, we'll just reload the page
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
} else {
|
} else {
|
||||||
const errorText = await response.text();
|
|
||||||
switch (response.status) {
|
switch (response.status) {
|
||||||
case 409:
|
case 409:
|
||||||
showError('You already have this feed added');
|
showError('You already have this feed added');
|
||||||
break;
|
break;
|
||||||
case 422:
|
case 422:
|
||||||
showError('Invalid feed URL. Please make sure it starts with http:// or https://');
|
showError('Invalid feed URL. Please make sure it\'s a valid RSS or Atom feed.');
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
showError('Failed to add feed. Please try again.');
|
showError('Failed to add feed. Please try again.');
|
||||||
|
@ -22,22 +22,21 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<form id="addFeedForm">
|
<form id="addFeedForm">
|
||||||
<div class="form-group">
|
|
||||||
<label for="feedName">Feed Name</label>
|
|
||||||
<input type="text" id="feedName" name="name" required
|
|
||||||
placeholder="My Blog Feed">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="feedUrl">Feed URL</label>
|
<label for="feedUrl">Feed URL</label>
|
||||||
<input type="url" id="feedUrl" name="url" required
|
<input type="url" id="feedUrl" name="url" required
|
||||||
placeholder="https://example.com/feed.xml">
|
placeholder="https://example.com/feed.xml">
|
||||||
</div>
|
</div>
|
||||||
<div class="error-message" id="feedErrorMessage" style="display: none; color: red; margin-bottom: 10px;"></div>
|
<div class="error-message" id="feedErrorMessage" style="display: none; color: red; margin-bottom: 10px;"></div>
|
||||||
|
<div class="loading-message" id="loadingMessage" style="display: none; color: #666; margin-bottom: 10px; text-align: center;">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<div>Loading feed information...</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn-secondary" id="cancelAddFeed">Cancel</button>
|
<button type="button" class="btn-secondary" id="cancelAddFeed">Cancel</button>
|
||||||
<button type="button" class="btn-primary" id="confirmAddFeed">OK</button>
|
<button type="button" class="btn-primary" id="confirmAddFeed">Add Feed</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user