From a6ae8c0e3b6f6ab8285fdb89af5b24ef77872ef2 Mon Sep 17 00:00:00 2001 From: micraj Date: Sun, 8 Jun 2025 16:32:14 +0100 Subject: [PATCH 1/8] Database and api setup --- backend/app/api/data.py | 81 ++++++++++++++++++++++++++++----- backend/app/main.py | 2 - backend/app/models/blog_post.py | 11 +++++ backend/app/models/yesno.py | 6 --- 4 files changed, 80 insertions(+), 20 deletions(-) create mode 100644 backend/app/models/blog_post.py delete mode 100644 backend/app/models/yesno.py diff --git a/backend/app/api/data.py b/backend/app/api/data.py index 4342132..a236158 100644 --- a/backend/app/api/data.py +++ b/backend/app/api/data.py @@ -1,4 +1,5 @@ -from fastapi import APIRouter, File, UploadFile, Request +from fastapi import APIRouter, File, UploadFile, Request, HTTPException + import datetime import shutil @@ -6,19 +7,75 @@ from sqlmodel import Session, select, text -from app.models.yesno import YesNo +from app.models.blog_post import BlogPost from app.core.database import SessionDep - +from typing import List router = APIRouter() -@router.post("/upload") -async def upload_answer(request: Request, db: SessionDep): - answer = await request.body() - answer = answer.decode("utf-8") - db.add(YesNo(timestamp=datetime.datetime.now(), answer=answer)) +#Upload post +#Posts by ID +#Top 10 posts latest and their slugs +@router.post("/posts/", response_model=BlogPost) +def create_post(post: BlogPost, db: SessionDep): + db.add(post) + db.commit() + db.refresh(post) + return post + +@router.post("/posts/upload_markdown/", response_model=BlogPost) +def upload_markdown( + title: str, + slug: str, + db: SessionDep, + file: UploadFile = File(...), + +): + if not file.filename.endswith(".md"): + raise HTTPException(status_code=400, detail="Only markdown files (.md) are supported.") + + content = file.file.read().decode("utf-8") + + post = BlogPost( + title=title, + slug=slug, + content_markdown=content, + ) + + db.add(post) db.commit() + db.refresh(post) -@router.get(("/get")) -def get_answer(db: SessionDep): - all_answers = db.exec(select(YesNo)).all() - return all_answers \ No newline at end of file + return post + +@router.get("/posts/", response_model=List[BlogPost]) +def read_posts(db: SessionDep): + return db.exec(select(BlogPost)).all() + +@router.get("/posts/{post_id}", response_model=BlogPost) +def read_post(post_id: int, db: SessionDep): + post = db.get(BlogPost, post_id) + if not post: + raise HTTPException(status_code=404, detail="Post not found") + return post + +@router.put("/posts/{post_id}", response_model=BlogPost) +def update_post(post_id: int, updated_post: BlogPost, db: SessionDep): + db_post = db.get(BlogPost, post_id) + if not db_post: + raise HTTPException(status_code=404, detail="Post not found") + db_post.title = updated_post.title + db_post.slug = updated_post.slug + db_post.content_markdown = updated_post.content_markdown + db.add(db_post) + db.commit() + db.refresh(db_post) + return db_post + +@router.delete("/posts/{post_id}") +def delete_post(post_id: int, db: SessionDep): + post = db.get(BlogPost, post_id) + if not post: + raise HTTPException(status_code=404, detail="Post not found") + db.delete(post) + db.commit() + return {"detail": "Post deleted"} diff --git a/backend/app/main.py b/backend/app/main.py index b8e9313..0b7055f 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -17,8 +17,6 @@ async def lifespan(app: FastAPI): # Allowing CORS from the specific origin (localhost:5173) or any origin origins = [ - "http://localhost:5173", # Frontend app URL - "http://127.0.0.1:5173", "*" # Or you can use localhost and 127.0.0.1 as the origin ] diff --git a/backend/app/models/blog_post.py b/backend/app/models/blog_post.py new file mode 100644 index 0000000..c34dae2 --- /dev/null +++ b/backend/app/models/blog_post.py @@ -0,0 +1,11 @@ +from sqlmodel import Field, SQLModel +from datetime import datetime +import time +from typing import Optional + +class BlogPost(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + timestamp: int = Field(default_factory=lambda: int(time.time())) + title: str + slug: str + content_markdown: str \ No newline at end of file diff --git a/backend/app/models/yesno.py b/backend/app/models/yesno.py deleted file mode 100644 index 25adaf4..0000000 --- a/backend/app/models/yesno.py +++ /dev/null @@ -1,6 +0,0 @@ -from sqlmodel import Field, SQLModel - -class YesNo(SQLModel, table=True): - id: int = Field(default=None, primary_key=True) - timestamp: str - answer: str \ No newline at end of file From 07d26838e6efcdb77eca9fe7a9e7bb1cbd77dfd2 Mon Sep 17 00:00:00 2001 From: micraj Date: Fri, 27 Jun 2025 15:16:03 +0100 Subject: [PATCH 2/8] updated backend to store raw html --- backend/app/api/data.py | 65 ++++++++++++++++----------------- backend/app/models/blog_post.py | 18 +++++++-- 2 files changed, 46 insertions(+), 37 deletions(-) diff --git a/backend/app/api/data.py b/backend/app/api/data.py index a236158..6df0544 100644 --- a/backend/app/api/data.py +++ b/backend/app/api/data.py @@ -2,55 +2,49 @@ import datetime import shutil +import time +import re import json from sqlmodel import Session, select, text -from app.models.blog_post import BlogPost +from app.models.blog_post import BlogPost, BlogPostCreate, BlogPostUpdate from app.core.database import SessionDep from typing import List + router = APIRouter() -#Upload post -#Posts by ID -#Top 10 posts latest and their slugs -@router.post("/posts/", response_model=BlogPost) -def create_post(post: BlogPost, db: SessionDep): - db.add(post) - db.commit() - db.refresh(post) - return post -@router.post("/posts/upload_markdown/", response_model=BlogPost) -def upload_markdown( - title: str, - slug: str, - db: SessionDep, - file: UploadFile = File(...), - -): - if not file.filename.endswith(".md"): - raise HTTPException(status_code=400, detail="Only markdown files (.md) are supported.") - - content = file.file.read().decode("utf-8") - - post = BlogPost( - title=title, - slug=slug, - content_markdown=content, - ) +def generate_unique_slug(title: str, db: Session) -> str: + # Slugify title (basic version; you can improve with unidecode or slugify lib) + base_slug = re.sub(r"[^a-z0-9]+", "-", title.lower()).strip("-") + slug = base_slug + counter = 1 + while db.exec(select(BlogPost).where(BlogPost.slug == slug)).first(): + slug = f"{base_slug}-{counter}" + counter += 1 + return slug + - db.add(post) +@router.post("/posts/create_post", response_model=BlogPost) +def create_post(post: BlogPostCreate, db: SessionDep): + created_post = BlogPost( + title=post.title, + content=post.content, + slug=generate_unique_slug(post.title, db), + ) + db.add(created_post) db.commit() - db.refresh(post) + db.refresh(created_post) + return created_post - return post @router.get("/posts/", response_model=List[BlogPost]) def read_posts(db: SessionDep): return db.exec(select(BlogPost)).all() + @router.get("/posts/{post_id}", response_model=BlogPost) def read_post(post_id: int, db: SessionDep): post = db.get(BlogPost, post_id) @@ -58,19 +52,22 @@ def read_post(post_id: int, db: SessionDep): raise HTTPException(status_code=404, detail="Post not found") return post + @router.put("/posts/{post_id}", response_model=BlogPost) -def update_post(post_id: int, updated_post: BlogPost, db: SessionDep): +def update_post(post_id: int, updated_post: BlogPostUpdate, db: SessionDep): db_post = db.get(BlogPost, post_id) if not db_post: raise HTTPException(status_code=404, detail="Post not found") db_post.title = updated_post.title - db_post.slug = updated_post.slug - db_post.content_markdown = updated_post.content_markdown + db_post.slug = updated_post.title.lower().replace(" ", "-") + db_post.content = updated_post.content + db_post.timestamp = int(time.time()) db.add(db_post) db.commit() db.refresh(db_post) return db_post + @router.delete("/posts/{post_id}") def delete_post(post_id: int, db: SessionDep): post = db.get(BlogPost, post_id) diff --git a/backend/app/models/blog_post.py b/backend/app/models/blog_post.py index c34dae2..8c58d11 100644 --- a/backend/app/models/blog_post.py +++ b/backend/app/models/blog_post.py @@ -2,10 +2,22 @@ from datetime import datetime import time from typing import Optional +from pydantic import BaseModel + class BlogPost(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - timestamp: int = Field(default_factory=lambda: int(time.time())) + timestamp: Optional[int] = Field(default_factory=lambda: int(time.time())) + title: str + slug: str = Field(index=True, unique=True) + content: str + + +class BlogPostCreate(BaseModel): + title: str + content: str + + +class BlogPostUpdate(BaseModel): title: str - slug: str - content_markdown: str \ No newline at end of file + content: str From c00373b1d10fdebffc02ab1b5f3b1b88adf11f19 Mon Sep 17 00:00:00 2001 From: micraj Date: Fri, 27 Jun 2025 15:21:16 +0100 Subject: [PATCH 3/8] add html sanitisation placeholder function --- backend/app/api/data.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/backend/app/api/data.py b/backend/app/api/data.py index 6df0544..726aa1e 100644 --- a/backend/app/api/data.py +++ b/backend/app/api/data.py @@ -16,6 +16,10 @@ router = APIRouter() +def sanitise_html(html: str) -> str: + return html + + def generate_unique_slug(title: str, db: Session) -> str: # Slugify title (basic version; you can improve with unidecode or slugify lib) base_slug = re.sub(r"[^a-z0-9]+", "-", title.lower()).strip("-") @@ -31,7 +35,7 @@ def generate_unique_slug(title: str, db: Session) -> str: def create_post(post: BlogPostCreate, db: SessionDep): created_post = BlogPost( title=post.title, - content=post.content, + content=sanitise_html(post.content), slug=generate_unique_slug(post.title, db), ) db.add(created_post) From 12e16d2e83f2f5d59de2ff464de2ba2ed31f4a51 Mon Sep 17 00:00:00 2001 From: micraj Date: Mon, 7 Jul 2025 14:12:38 +0100 Subject: [PATCH 4/8] Add cover image field --- backend/app/api/data.py | 25 +++++++++++++++++++------ backend/app/models/blog_post.py | 15 ++++++++++++--- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/backend/app/api/data.py b/backend/app/api/data.py index 726aa1e..3084e24 100644 --- a/backend/app/api/data.py +++ b/backend/app/api/data.py @@ -11,7 +11,7 @@ from app.models.blog_post import BlogPost, BlogPostCreate, BlogPostUpdate from app.core.database import SessionDep -from typing import List +from typing import List, Optional router = APIRouter() @@ -37,6 +37,7 @@ def create_post(post: BlogPostCreate, db: SessionDep): title=post.title, content=sanitise_html(post.content), slug=generate_unique_slug(post.title, db), + cover_image=post.cover_image, ) db.add(created_post) db.commit() @@ -58,14 +59,26 @@ def read_post(post_id: int, db: SessionDep): @router.put("/posts/{post_id}", response_model=BlogPost) -def update_post(post_id: int, updated_post: BlogPostUpdate, db: SessionDep): +def update_post( + post_id: int, + updated_post: BlogPostUpdate, + db: SessionDep, +): db_post = db.get(BlogPost, post_id) if not db_post: raise HTTPException(status_code=404, detail="Post not found") - db_post.title = updated_post.title - db_post.slug = updated_post.title.lower().replace(" ", "-") - db_post.content = updated_post.content - db_post.timestamp = int(time.time()) + + if updated_post.title is not None: + db_post.title = updated_post.title + db_post.slug = generate_unique_slug(updated_post.title, db) + + if updated_post.content is not None: + db_post.content = sanitise_html(updated_post.content) + + if updated_post.cover_image is not None: + db_post.cover_image = updated_post.cover_image + + # db_post.timestamp = int(time.time()) db.add(db_post) db.commit() db.refresh(db_post) diff --git a/backend/app/models/blog_post.py b/backend/app/models/blog_post.py index 8c58d11..587baf8 100644 --- a/backend/app/models/blog_post.py +++ b/backend/app/models/blog_post.py @@ -5,19 +5,28 @@ from pydantic import BaseModel +from typing import Optional +from sqlmodel import SQLModel, Field +from pydantic import BaseModel +import time + + class BlogPost(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - timestamp: Optional[int] = Field(default_factory=lambda: int(time.time())) + timestamp: int = Field(default_factory=lambda: int(time.time())) title: str slug: str = Field(index=True, unique=True) content: str + cover_image: str # Path to image file, .etc '/hamster_neutral.jpg' class BlogPostCreate(BaseModel): title: str content: str + cover_image: str class BlogPostUpdate(BaseModel): - title: str - content: str + title: Optional[str] = None + content: Optional[str] = None + cover_image: Optional[str] = None From 0c0310df6b04ada98c114750c2f616eb61d46f5e Mon Sep 17 00:00:00 2001 From: micraj Date: Mon, 7 Jul 2025 14:27:41 +0100 Subject: [PATCH 5/8] Add post list --- frontend/src/lib/components/PostList.svelte | 42 +++++++++++++++++++ frontend/src/lib/components/TinyEditor.svelte | 2 +- frontend/src/routes/+page.svelte | 2 + 3 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 frontend/src/lib/components/PostList.svelte diff --git a/frontend/src/lib/components/PostList.svelte b/frontend/src/lib/components/PostList.svelte new file mode 100644 index 0000000..9018ae3 --- /dev/null +++ b/frontend/src/lib/components/PostList.svelte @@ -0,0 +1,42 @@ + + +{#if loading} +

Loading posts...

+{:else if error} +

Error: {error}

+{:else} + {#each posts as post} +
+

{post.id}

+

{post.title}

+
{@html post.content}
+
+ {/each} +{/if} diff --git a/frontend/src/lib/components/TinyEditor.svelte b/frontend/src/lib/components/TinyEditor.svelte index a259c58..d31feec 100644 --- a/frontend/src/lib/components/TinyEditor.svelte +++ b/frontend/src/lib/components/TinyEditor.svelte @@ -58,7 +58,7 @@ function handleSubmit() { console.log('Submitted Title:', title); console.log('Submitted Content:', content); - fetch('https://jsonplaceholder.typicode.com/posts', { + fetch('http://127.0.0.1:8000/posts/create_post', { method: 'POST', headers: { 'Content-type': 'application/json; charset=UTF-8' diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index 58dc6ff..6fc364c 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -1,10 +1,12 @@

My Editor

+ From 022f58ac59080c74aeb1c128738e62e631d38110 Mon Sep 17 00:00:00 2001 From: micraj Date: Mon, 7 Jul 2025 16:19:14 +0100 Subject: [PATCH 6/8] Cover image in frontend --- frontend/src/lib/components/PostList.svelte | 3 +++ frontend/src/lib/components/TinyEditor.svelte | 11 ++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/frontend/src/lib/components/PostList.svelte b/frontend/src/lib/components/PostList.svelte index 9018ae3..f733d58 100644 --- a/frontend/src/lib/components/PostList.svelte +++ b/frontend/src/lib/components/PostList.svelte @@ -7,6 +7,7 @@ title: string; slug: string; content: string; + cover_image: string; }; let posts: Post[] = []; @@ -36,6 +37,8 @@

{post.id}

{post.title}

+

{post.slug}

+ {post.title}
{@html post.content}
{/each} diff --git a/frontend/src/lib/components/TinyEditor.svelte b/frontend/src/lib/components/TinyEditor.svelte index d31feec..dd3acb9 100644 --- a/frontend/src/lib/components/TinyEditor.svelte +++ b/frontend/src/lib/components/TinyEditor.svelte @@ -4,6 +4,7 @@ let editorId = 'my-tinymce-editor'; let title: string = ''; + let cover_image_path: string = ''; export let content: string = ''; export let height: number = 300; @@ -57,6 +58,7 @@ function handleSubmit() { console.log('Submitted Title:', title); + console.log('Submitted Cover Image:', cover_image_path); console.log('Submitted Content:', content); fetch('http://127.0.0.1:8000/posts/create_post', { method: 'POST', @@ -65,7 +67,8 @@ }, body: JSON.stringify({ title: title, - content: content + content: content, + cover_image: cover_image_path }) }) .then((response) => response.json()) @@ -81,6 +84,12 @@ {#if browser}
+ From b317ec501c91510b7f47acc986dd8c27c16f2348 Mon Sep 17 00:00:00 2001 From: micraj Date: Mon, 7 Jul 2025 18:45:01 +0100 Subject: [PATCH 7/8] Seperate tinymce editor and post editor --- frontend/src/lib/components/PostEditor.svelte | 89 +++++++++++++++++++ frontend/src/lib/components/TinyEditor.svelte | 82 +---------------- frontend/src/routes/+page.svelte | 3 +- 3 files changed, 92 insertions(+), 82 deletions(-) create mode 100644 frontend/src/lib/components/PostEditor.svelte diff --git a/frontend/src/lib/components/PostEditor.svelte b/frontend/src/lib/components/PostEditor.svelte new file mode 100644 index 0000000..3092ce6 --- /dev/null +++ b/frontend/src/lib/components/PostEditor.svelte @@ -0,0 +1,89 @@ + + +{#if browser} +
+ + + + + + +
+{:else} +

Editor loading...

+{/if} + + diff --git a/frontend/src/lib/components/TinyEditor.svelte b/frontend/src/lib/components/TinyEditor.svelte index dd3acb9..172851f 100644 --- a/frontend/src/lib/components/TinyEditor.svelte +++ b/frontend/src/lib/components/TinyEditor.svelte @@ -3,8 +3,6 @@ import { onMount, onDestroy } from 'svelte'; let editorId = 'my-tinymce-editor'; - let title: string = ''; - let cover_image_path: string = ''; export let content: string = ''; export let height: number = 300; @@ -55,84 +53,6 @@ }); } }); - - function handleSubmit() { - console.log('Submitted Title:', title); - console.log('Submitted Cover Image:', cover_image_path); - console.log('Submitted Content:', content); - fetch('http://127.0.0.1:8000/posts/create_post', { - method: 'POST', - headers: { - 'Content-type': 'application/json; charset=UTF-8' - }, - body: JSON.stringify({ - title: title, - content: content, - cover_image: cover_image_path - }) - }) - .then((response) => response.json()) - .then((data) => { - console.log('Response from server:', data); - }) - .catch((error) => { - console.error('Error submitting content:', error); - }); - } -{#if browser} -
- - - - - - -
-{:else} -

Editor loading...

-{/if} - - + diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index 6fc364c..46cb2aa 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -1,11 +1,12 @@

My Editor

- +