Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 82 additions & 11 deletions backend/app/api/data.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,95 @@
from fastapi import APIRouter, File, UploadFile, Request
from fastapi import APIRouter, File, UploadFile, Request, HTTPException

import datetime
import shutil
import time
import re

import json

from sqlmodel import Session, select, text

from app.models.yesno import YesNo
from app.models.blog_post import BlogPost, BlogPostCreate, BlogPostUpdate
from app.core.database import SessionDep
from typing import List, Optional

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))

def sanitise_html(html: str) -> str:

Copilot AI Aug 9, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sanitise_html function returns the input unchanged, providing no HTML sanitization. This creates a security risk as unsanitized HTML content could lead to XSS vulnerabilities when rendered in the frontend.

Copilot uses AI. Check for mistakes.
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("-")
slug = base_slug
counter = 1
while db.exec(select(BlogPost).where(BlogPost.slug == slug)).first():
slug = f"{base_slug}-{counter}"
counter += 1
return slug


@router.post("/posts/create_post", response_model=BlogPost)
def create_post(post: BlogPostCreate, db: SessionDep):
created_post = BlogPost(
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()
db.refresh(created_post)
return created_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.get(("/get"))
def get_answer(db: SessionDep):
all_answers = db.exec(select(YesNo)).all()
return all_answers

@router.put("/posts/{post_id}", response_model=BlogPost)
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")

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())

Copilot AI Aug 9, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commented-out code should be removed rather than left in the codebase to maintain code cleanliness.

Suggested change
# db_post.timestamp = int(time.time())

Copilot uses AI. Check for mistakes.
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"}
2 changes: 0 additions & 2 deletions backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
]

Expand Down
26 changes: 26 additions & 0 deletions backend/app/models/blog_post.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from typing import Optional

Copilot AI Aug 9, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate import statement. The typing.Optional import on line 8 is redundant as it's already imported on line 4.

Suggested change
from typing import Optional

Copilot uses AI. Check for mistakes.
from datetime import datetime
from sqlmodel import SQLModel, Field
from pydantic import BaseModel

Copilot AI Aug 9, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate import statement. The pydantic.BaseModel import on line 10 is redundant as it's already imported on line 5.

Suggested change
from pydantic import BaseModel

Copilot uses AI. Check for mistakes.
import time

Copilot AI Aug 9, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate import statement. The time import on line 11 is redundant as it's already imported on line 3.

Suggested change
import time

Copilot uses AI. Check for mistakes.


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 = 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: Optional[str] = None
content: Optional[str] = None
cover_image: Optional[str] = None
6 changes: 0 additions & 6 deletions backend/app/models/yesno.py

This file was deleted.

89 changes: 89 additions & 0 deletions frontend/src/lib/components/PostEditor.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<script lang="ts">
import { browser } from '$app/environment';
import { onMount, onDestroy } from 'svelte';
import TinyEditor from './TinyEditor.svelte';

let title: string = '';
let cover_image_path: string = '';
let content: string = '';

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);
});
}
</script>

{#if browser}
<div class="editor-container">
<input type="text" bind:value={title} placeholder="Enter title" class="title-input" />
<input
type="text"
bind:value={cover_image_path}
placeholder="Enter cover image path"
class="title-input"
/>

<TinyEditor bind:content />

<button on:click={handleSubmit} class="submit-button">Submit</button>
</div>
{:else}
<p>Editor loading...</p>
{/if}

<style>
.editor-container {
max-width: 60%;
margin: 2rem auto;
padding: 1rem;
display: flex;
flex-direction: column;
gap: 1rem;
}

.title-input {
padding: 0.5rem;
font-size: 1.5rem;
border: 1px solid #ccc;
border-radius: 4px;
width: 100%;
}

.submit-button {
align-self: flex-start;
padding: 0.6rem 1.2rem;
font-size: 1rem;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}

.submit-button:hover {
background-color: #0056b3;
}

:global(.tox) {
max-width: 100%;
}
</style>
45 changes: 45 additions & 0 deletions frontend/src/lib/components/PostList.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<script lang="ts">
import { onMount } from 'svelte';

type Post = {
id: number;
timestamp: number;
title: string;
slug: string;
content: string;
cover_image: string;
};

let posts: Post[] = [];
let loading = true;
let error: string | null = null;

onMount(async () => {
try {
const res = await fetch('http://127.0.0.1:8000/posts/');
if (!res.ok) throw new Error(`Failed to fetch: ${res.status}`);

posts = await res.json();
} catch (e: any) {
error = e.message;
} finally {
loading = false;
}
});
</script>

{#if loading}
<p>Loading posts...</p>
{:else if error}
<p style="color: red;">Error: {error}</p>
{:else}
{#each posts as post}
<article>
<h1>{post.id}</h1>

Copilot AI Aug 9, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using <h1> tag to display the post ID is semantically incorrect. The post ID should use a less prominent heading tag or span element, as <h1> should represent the main heading of the page or section.

Suggested change
<h1>{post.id}</h1>
<span>{post.id}</span>

Copilot uses AI. Check for mistakes.
<h2>{post.title}</h2>
<h2>{post.slug}</h2>
<img src={post.cover_image} alt={post.title} />
<div>{@html post.content}</div>
</article>
{/each}
{/if}
73 changes: 1 addition & 72 deletions frontend/src/lib/components/TinyEditor.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import { onMount, onDestroy } from 'svelte';

let editorId = 'my-tinymce-editor';
let title: string = '';

export let content: string = '';
export let height: number = 300;
Expand Down Expand Up @@ -54,76 +53,6 @@
});
}
});

function handleSubmit() {
console.log('Submitted Title:', title);
console.log('Submitted Content:', content);
fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
headers: {
'Content-type': 'application/json; charset=UTF-8'
},
body: JSON.stringify({
title: title,
content: content
})
})
.then((response) => response.json())
.then((data) => {
console.log('Response from server:', data);
})
.catch((error) => {
console.error('Error submitting content:', error);
});
}
</script>

{#if browser}
<div class="editor-container">
<input type="text" bind:value={title} placeholder="Enter title" class="title-input" />

<textarea id={editorId}></textarea>

<button on:click={handleSubmit} class="submit-button">Submit</button>
</div>
{:else}
<p>Editor loading...</p>
{/if}

<style>
.editor-container {
max-width: 60%;
margin: 2rem auto;
padding: 1rem;
display: flex;
flex-direction: column;
gap: 1rem;
}

.title-input {
padding: 0.5rem;
font-size: 1.5rem;
border: 1px solid #ccc;
border-radius: 4px;
width: 100%;
}

.submit-button {
align-self: flex-start;
padding: 0.6rem 1.2rem;
font-size: 1rem;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}

.submit-button:hover {
background-color: #0056b3;
}

:global(.tox) {
max-width: 100%;
}
</style>
<textarea id={editorId}></textarea>
5 changes: 4 additions & 1 deletion frontend/src/routes/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
<script lang="ts">
import PostList from '$lib/components/PostList.svelte';
import TinyEditor from '$lib/components/TinyEditor.svelte';
import PostEditor from '$lib/components/PostEditor.svelte';
let content: string = '<p>Hello, TinyMCE!</p>';
</script>

<h1>My Editor</h1>
<TinyEditor bind:content height={400} />
<PostEditor />
<PostList />

<!-- <h2>Preview:</h2>
<div>{@html content}</div> -->