FEATZAFF commited on
Commit
cce1e0e
·
verified ·
1 Parent(s): 9a5f7e0

Upload 31 files

Browse files
app/__init__.py ADDED
File without changes
app/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (174 Bytes). View file
 
app/__pycache__/database.cpython-312.pyc ADDED
Binary file (895 Bytes). View file
 
app/__pycache__/main.cpython-312.pyc ADDED
Binary file (1.6 kB). View file
 
app/crud/__init__.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ CRUD operations for the Item model.
3
+ This module imports and exposes all CRUD operations from their respective modules.
4
+ """
5
+
6
+ # Import operations
7
+ from app.crud.create import create_item
8
+ from app.crud.read import get_item, get_items
9
+ from app.crud.update import update_item
10
+ from app.crud.delete import delete_item
11
+
12
+ # Re-export all operations
13
+ __all__ = [
14
+ "create_item", # Create operations
15
+ "get_item", "get_items", # Read operations
16
+ "update_item", # Update operations
17
+ "delete_item", # Delete operations
18
+ ]
app/crud/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (597 Bytes). View file
 
app/crud/__pycache__/create.cpython-312.pyc ADDED
Binary file (806 Bytes). View file
 
app/crud/__pycache__/delete.cpython-312.pyc ADDED
Binary file (1.07 kB). View file
 
app/crud/__pycache__/read.cpython-312.pyc ADDED
Binary file (2.17 kB). View file
 
app/crud/__pycache__/update.cpython-312.pyc ADDED
Binary file (1.25 kB). View file
 
app/crud/create.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy.orm import Session
2
+ from app.models.item import Item
3
+ from app.schemas.item import ItemCreate
4
+
5
+ def create_item(db: Session, item: ItemCreate):
6
+ db_item = Item(title=item.title,description=item.description,completed=item.completed)
7
+ db.add(db_item)
8
+ db.commit()
9
+ db.refresh(db_item)
10
+ return db_item
11
+ """
12
+ Create a new item in the database.
13
+
14
+ Args:
15
+ db (Session): Database session
16
+ item (ItemCreate): Item data to create
17
+
18
+ Returns:
19
+ Item: The created item
20
+ """
21
+ # TODO: Implement the create_item function
22
+ # The function should:
23
+ # 1. Create a new Item object with the data from item
24
+ # 2. Add the item to the database session
25
+ # 3. Commit the transaction
26
+ # 4. Refresh the item to get the generated ID
27
+ # 5. Return the created item
28
+
29
+
app/crud/delete.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy.orm import Session
2
+ from typing import Optional
3
+ from app.models.item import Item
4
+
5
+
6
+ def delete_item(db: Session, item_id: int) -> bool:
7
+ """
8
+ Delete an item from the database.
9
+
10
+ Args:
11
+ db (Session): Database session
12
+ item_id (int): ID of the item to delete
13
+
14
+ Returns:
15
+ bool: True if the item was deleted, False if the item was not found
16
+ """
17
+
18
+ item = db.query(Item).filter(Item.id == item_id).first()
19
+
20
+
21
+ if not item:
22
+ return False
23
+
24
+
25
+ db.delete(item)
26
+
27
+
28
+ db.commit()
29
+
30
+
31
+ return True
32
+
33
+
34
+
35
+
36
+
37
+
38
+ # TODO: Implement the delete_item function
39
+ # The function should:
40
+ # 1. Get the item with the given item_id from the database
41
+ # 2. If the item doesn't exist, return False
42
+ # 3. Delete the item from the database
43
+ # 4. Commit the changes
44
+ # 5. Return True to indicate successful deletion
45
+
46
+ # Delete the code below and implement your solution
47
+
48
+
app/crud/read.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy.orm import Session
2
+ from typing import List, Optional
3
+ from app.models.item import Item
4
+
5
+ def get_item(db: Session, item_id: int) -> Optional[Item]:
6
+ return db.query(Item).filter(Item.id == item_id).first()
7
+
8
+ def get_items(db: Session, skip: int = 0, limit: int = 100) -> List[Item]:
9
+ item_list = db.query(Item).offset(skip).limit(limit).all()
10
+ return item_list
11
+
12
+ from typing import List
13
+ from fastapi import Header, HTTPException
14
+ from sqlalchemy.orm import Session
15
+
16
+ API_KEY = "1234"
17
+
18
+
19
+ def get_items(
20
+ db: Session,
21
+ skip: int = 0,
22
+ limit: int = 100,
23
+ api_key: str = Header(..., alias="api_key")
24
+ ) -> List[Item]:
25
+ """
26
+ Get multiple items with pagination.
27
+
28
+ Args:
29
+ db (Session): Database session
30
+ skip (int): Number of records to skip (for pagination)
31
+ limit (int): Maximum number of records to return
32
+ api_key (str): Required API key from request header
33
+
34
+ Returns:
35
+ List[Item]: List of found items
36
+ """
37
+
38
+ # Validate API key
39
+ if api_key != API_KEY:
40
+ raise HTTPException(
41
+ status_code=401,
42
+ detail="Invalid API key"
43
+ )
44
+
45
+ # Query all items with pagination
46
+ return db.query(Item).offset(skip).limit(limit).all()
app/crud/update.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy.orm import Session
2
+ from typing import Dict, Any, Optional
3
+ from app.models.item import Item
4
+ from app.schemas.item import ItemCreate
5
+
6
+ def update_item(db: Session, item_id: int, item: ItemCreate) -> Optional[Item]:
7
+ update_data = item.dict(exclude_unset=True)
8
+ result = db.query(Item).filter(Item.id == item_id).update(update_data)
9
+
10
+
11
+ if result == 0:
12
+ return None
13
+
14
+
15
+
16
+ db.commit()
17
+ updated_item = db.query(Item).filter(Item.id == item_id).first()
18
+ db.refresh(updated_item)
19
+ return updated_item
20
+
21
+
22
+
23
+
24
+
25
+ """
26
+ Update an existing item in the database.
27
+
28
+ Args:
29
+ db (Session): Database session
30
+ item_id (int): ID of the item to update
31
+ item (ItemCreate): New item data
32
+
33
+ Returns:
34
+ Optional[Item]: The updated item or None if not found
35
+ """
36
+
37
+
38
+ # TODO: Implement the update_item function
39
+ # The function should:
40
+ # 1. Get the item with the given item_id from the database
41
+ # 2. If the item doesn't exist, return None
42
+ # 3. Update the item's attributes with the new values
43
+ # 4. Commit the changes to the database
44
+ # 5. Refresh the item from the database
45
+ # 6. Return the updated item
46
+
47
+ # Delete the code below and implement your solution
48
+
app/database.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import create_engine
2
+ from sqlalchemy.ext.declarative import declarative_base
3
+ from sqlalchemy.orm import sessionmaker
4
+
5
+ SQLALCHEMY_DATABASE_URL = "sqlite:///./database/items.db"
6
+
7
+ # Create SQLite engine
8
+ engine = create_engine(
9
+ SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
10
+ )
11
+
12
+ # Create SessionLocal class
13
+ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
14
+
15
+ # Create Base class
16
+ Base = declarative_base()
17
+
18
+ # Dependency
19
+ def get_db():
20
+ db = SessionLocal()
21
+ try:
22
+ yield db
23
+ finally:
24
+ db.close()
app/main.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ from fastapi import FastAPI, Request
3
+ from fastapi.responses import HTMLResponse
4
+ from fastapi.staticfiles import StaticFiles
5
+ from fastapi.templating import Jinja2Templates
6
+ import os
7
+
8
+ from app.database import engine
9
+ from app.models.item import Item
10
+ import app.routes.item as item_routes
11
+
12
+ # Create tables in the database
13
+ from app.database import Base
14
+ Base.metadata.create_all(bind=engine)
15
+
16
+ # Initialize FastAPI app
17
+ app = FastAPI(title="FastAPI CRUD App")
18
+
19
+ # Mount static files
20
+ app.mount("/static", StaticFiles(directory="app/static"), name="static")
21
+
22
+ # Set up templates
23
+ templates = Jinja2Templates(directory="app/templates")
24
+
25
+ # Include routers
26
+ app.include_router(item_routes.router)
27
+
28
+ # Root endpoint
29
+ @app.get("/", response_class=HTMLResponse)
30
+ async def read_root(request: Request):
31
+ return templates.TemplateResponse("index.html", {"request": request})
32
+
33
+ # Run with: uvicorn app.main:app --reload
34
+ if __name__ == "__main__":
35
+ import uvicorn
36
+ uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
37
+
38
+ """
39
+ # IGNORE THE FOLLOWING SECTION
40
+ from fastapi import FastAPI, Request
41
+ from fastapi.responses import HTMLResponse
42
+ from fastapi.staticfiles import StaticFiles
43
+ from fastapi.templating import Jinja2Templates
44
+ import os
45
+ import sys
46
+ import importlib
47
+
48
+ from app.database import engine
49
+ from app.models.item import Item
50
+
51
+ # Create tables in the database
52
+ from app.database import Base
53
+ Base.metadata.create_all(bind=engine)
54
+
55
+ # Initialize FastAPI app
56
+ app = FastAPI(title="FastAPI CRUD App")
57
+
58
+ # Mount static files
59
+ app.mount("/static", StaticFiles(directory="app/static"), name="static")
60
+
61
+ # Set up templates
62
+ templates = Jinja2Templates(directory="app/templates")
63
+
64
+ # TEMP: Use the solutions for testing
65
+ # Import CRUD operations from solutions directory
66
+ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
67
+ from solutions.crud.create import create_item
68
+ from solutions.crud.read import get_item, get_items
69
+ from solutions.crud.update import update_item
70
+ from solutions.crud.delete import delete_item
71
+
72
+ # Create a temporary crud module that uses solutions
73
+ class TempCrud:
74
+ create_item = create_item
75
+ get_item = get_item
76
+ get_items = get_items
77
+ update_item = update_item
78
+ delete_item = delete_item
79
+
80
+ # Import the router
81
+ import app.routes.item as item_routes
82
+
83
+ # Replace the crud import in the routes with our temporary one
84
+ item_routes.crud = TempCrud
85
+
86
+ # Include routers
87
+ app.include_router(item_routes.router)
88
+
89
+ # Root endpoint
90
+ @app.get("/", response_class=HTMLResponse)
91
+ async def read_root(request: Request):
92
+ return templates.TemplateResponse("index.html", {"request": request})
93
+
94
+ # Run with: uvicorn app.main:app --reload
95
+ if __name__ == "__main__":
96
+ import uvicorn
97
+ uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
98
+ """
app/models/__init__.py ADDED
File without changes
app/models/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (181 Bytes). View file
 
app/models/__pycache__/item.cpython-312.pyc ADDED
Binary file (742 Bytes). View file
 
app/models/item.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import Column, Integer, String, Boolean
2
+ from app.database import Base
3
+
4
+ class Item(Base):
5
+ __tablename__ = "items"
6
+
7
+ id = Column(Integer, primary_key=True, index=True)
8
+ title = Column(String, index=True)
9
+ description = Column(String)
10
+ completed = Column(Boolean, default=False)
app/routes/__init__.py ADDED
File without changes
app/routes/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (181 Bytes). View file
 
app/routes/__pycache__/item.cpython-312.pyc ADDED
Binary file (2.97 kB). View file
 
app/routes/item.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException, status
2
+ from sqlalchemy.orm import Session
3
+ from typing import List
4
+
5
+ from app.database import get_db
6
+ from app.schemas.item import Item, ItemCreate
7
+ import app.crud as crud
8
+
9
+ router = APIRouter(
10
+ prefix="/api/items",
11
+ tags=["items"],
12
+ )
13
+
14
+ # CREATE operation
15
+ @router.post("/", response_model=Item, status_code=status.HTTP_201_CREATED)
16
+ def create_item(item: ItemCreate, db: Session = Depends(get_db)):
17
+ """Create a new item"""
18
+ return crud.create_item(db=db, item=item)
19
+
20
+ from fastapi import Header
21
+ @router.get("/", response_model=List[Item])
22
+ def read_items(skip: int = 0, limit: int = 100, db: Session = Depends(get_db), api_key: str = Header(..., alias="api_key")):
23
+ """Get all items with pagination"""
24
+ return crud.get_items(db=db, skip=skip, limit=limit, api_key=api_key)
25
+
26
+ @router.get("/{item_id}", response_model=Item)
27
+ def read_item(item_id: int, db: Session = Depends(get_db)):
28
+ """Get a specific item by ID"""
29
+ db_item = crud.get_item(db=db, item_id=item_id)
30
+ if db_item is None:
31
+ raise HTTPException(status_code=404, detail="Item not found")
32
+ return db_item
33
+
34
+ # UPDATE operation
35
+ @router.put("/{item_id}", response_model=Item)
36
+ def update_item(item_id: int, item: ItemCreate, db: Session = Depends(get_db)):
37
+ """Update an existing item"""
38
+ db_item = crud.update_item(db=db, item_id=item_id, item=item)
39
+ if db_item is None:
40
+ raise HTTPException(status_code=404, detail="Item not found")
41
+ return db_item
42
+
43
+ # DELETE operation
44
+ @router.delete("/{item_id}", status_code=status.HTTP_204_NO_CONTENT)
45
+ def delete_item(item_id: int, db: Session = Depends(get_db)):
46
+ """Delete a specific item"""
47
+ success = crud.delete_item(db=db, item_id=item_id)
48
+ if not success:
49
+ raise HTTPException(status_code=404, detail="Item not found")
50
+ return None
app/schemas/__init__.py ADDED
File without changes
app/schemas/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (182 Bytes). View file
 
app/schemas/__pycache__/item.cpython-312.pyc ADDED
Binary file (1.11 kB). View file
 
app/schemas/item.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+ from typing import Optional
3
+
4
+ class ItemBase(BaseModel):
5
+ title: str
6
+ description: Optional[str] = None
7
+ completed: bool = False
8
+
9
+ class ItemCreate(ItemBase):
10
+ pass
11
+
12
+ class Item(ItemBase):
13
+ id: int
14
+
15
+ class Config:
16
+ from_attributes = True
app/static/css/styles.css ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Custom styles for the CRUD application */
2
+
3
+ body {
4
+ background-color: #f8f9fa;
5
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
6
+ }
7
+
8
+ header {
9
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
10
+ }
11
+
12
+ .card {
13
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
14
+ border: none;
15
+ transition: transform 0.3s;
16
+ }
17
+
18
+ .card:hover {
19
+ transform: translateY(-5px);
20
+ }
21
+
22
+ .card-header {
23
+ font-weight: 600;
24
+ }
25
+
26
+ .btn-primary {
27
+ background-color: #0d6efd;
28
+ border-color: #0d6efd;
29
+ }
30
+
31
+ .btn-primary:hover {
32
+ background-color: #0b5ed7;
33
+ border-color: #0a58ca;
34
+ }
35
+
36
+ .table th {
37
+ background-color: #f1f3f5;
38
+ }
39
+
40
+ .badge-status {
41
+ font-size: 0.8rem;
42
+ padding: 0.35em 0.65em;
43
+ }
44
+
45
+ .completed-true {
46
+ background-color: #198754;
47
+ }
48
+
49
+ .completed-false {
50
+ background-color: #dc3545;
51
+ }
52
+
53
+ .action-btn {
54
+ transition: all 0.2s;
55
+ }
56
+
57
+ .action-btn:hover {
58
+ transform: scale(1.2);
59
+ }
60
+
61
+ .edit-btn {
62
+ color: #0d6efd;
63
+ }
64
+
65
+ .delete-btn {
66
+ color: #dc3545;
67
+ }
68
+
69
+ #no-items-message {
70
+ padding: 2rem;
71
+ background-color: #f8f9fa;
72
+ border-radius: 0.5rem;
73
+ }
74
+
75
+ /* Toast customization */
76
+ .toast-container {
77
+ z-index: 11;
78
+ }
79
+
80
+ .toast {
81
+ opacity: 1;
82
+ box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
83
+ }
84
+
85
+ /* Animation for new/updated items */
86
+ @keyframes highlight {
87
+ 0% {
88
+ background-color: #e3f2fd;
89
+ }
90
+ 100% {
91
+ background-color: transparent;
92
+ }
93
+ }
94
+
95
+ .highlight-row {
96
+ animation: highlight 2s ease-in-out;
97
+ }
app/static/js/script.js ADDED
@@ -0,0 +1,303 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // DOM Elements
2
+ const itemForm = document.getElementById('item-form');
3
+ const formTitle = document.getElementById('form-title');
4
+ const itemIdInput = document.getElementById('item-id');
5
+ const titleInput = document.getElementById('title');
6
+ const descriptionInput = document.getElementById('description');
7
+ const completedCheckbox = document.getElementById('completed');
8
+ const submitBtn = document.getElementById('submit-btn');
9
+ const cancelBtn = document.getElementById('cancel-btn');
10
+ const itemsTableBody = document.getElementById('items-table-body');
11
+ const noItemsMessage = document.getElementById('no-items-message');
12
+ const createFirstItemBtn = document.getElementById('create-first-item-btn');
13
+ const loadingSpinner = document.getElementById('loading-spinner');
14
+ const toastElement = document.getElementById('toast');
15
+ const toastTitle = document.getElementById('toast-title');
16
+ const toastMessage = document.getElementById('toast-message');
17
+ const toastIcon = document.getElementById('toast-icon');
18
+
19
+ // Create Bootstrap toast instance
20
+ const toast = new bootstrap.Toast(toastElement, {
21
+ delay: 3000
22
+ });
23
+
24
+ // API Endpoints
25
+ const API_URL = '/api/items';
26
+
27
+ // App State
28
+ let isEditing = false;
29
+ let items = [];
30
+
31
+ // Event Listeners
32
+ document.addEventListener('DOMContentLoaded', fetchItems);
33
+ itemForm.addEventListener('submit', handleFormSubmit);
34
+ cancelBtn.addEventListener('click', resetForm);
35
+ if (createFirstItemBtn) {
36
+ createFirstItemBtn.addEventListener('click', () => {
37
+ window.scrollTo({
38
+ top: itemForm.offsetTop - 100,
39
+ behavior: 'smooth'
40
+ });
41
+ titleInput.focus();
42
+ });
43
+ }
44
+
45
+ // Functions
46
+ async function fetchItems() {
47
+ try {
48
+ showLoading(true);
49
+ const response = await fetch(API_URL);
50
+ const data = await response.json();
51
+
52
+ // Keep a reference to all items
53
+ items = data;
54
+
55
+ renderItems(items);
56
+ showLoading(false);
57
+ } catch (error) {
58
+ console.error('Error fetching items:', error);
59
+ showToast('Error', 'Failed to load items. Please try again.', 'error');
60
+ showLoading(false);
61
+ }
62
+ }
63
+
64
+ function renderItems(items) {
65
+ itemsTableBody.innerHTML = '';
66
+
67
+ if (items.length === 0) {
68
+ noItemsMessage.style.display = 'block';
69
+ document.querySelector('.table-responsive').style.display = 'none';
70
+ return;
71
+ }
72
+
73
+ noItemsMessage.style.display = 'none';
74
+ document.querySelector('.table-responsive').style.display = 'block';
75
+
76
+ items.forEach(item => {
77
+ const row = document.createElement('tr');
78
+ row.id = `item-row-${item.id}`;
79
+
80
+ row.innerHTML = `
81
+ <td>${item.id}</td>
82
+ <td class="fw-medium">${escapeHtml(item.title)}</td>
83
+ <td>${escapeHtml(item.description || '-')}</td>
84
+ <td>
85
+ <span class="badge completed-${item.completed}">
86
+ ${item.completed ?
87
+ '<i class="bi bi-check-circle me-1"></i>Completed' :
88
+ '<i class="bi bi-clock me-1"></i>Pending'}
89
+ </span>
90
+ </td>
91
+ <td class="text-center">
92
+ <div class="d-flex justify-content-center">
93
+ <button class="action-btn edit-btn" data-id="${item.id}" title="Edit Item">
94
+ <i class="bi bi-pencil-square"></i>
95
+ </button>
96
+ <button class="action-btn delete-btn" data-id="${item.id}" title="Delete Item">
97
+ <i class="bi bi-trash"></i>
98
+ </button>
99
+ </div>
100
+ </td>
101
+ `;
102
+
103
+ itemsTableBody.appendChild(row);
104
+
105
+ // Add event listeners to buttons
106
+ const editBtn = row.querySelector('.edit-btn');
107
+ const deleteBtn = row.querySelector('.delete-btn');
108
+
109
+ editBtn.addEventListener('click', () => editItem(item));
110
+ deleteBtn.addEventListener('click', () => deleteItem(item.id));
111
+ });
112
+ }
113
+
114
+ async function handleFormSubmit(event) {
115
+ event.preventDefault();
116
+
117
+ const itemData = {
118
+ title: titleInput.value.trim(),
119
+ description: descriptionInput.value.trim(),
120
+ completed: completedCheckbox.checked
121
+ };
122
+
123
+ if (!itemData.title) {
124
+ showToast('Validation Error', 'Title is required', 'error');
125
+ titleInput.focus();
126
+ return;
127
+ }
128
+
129
+ try {
130
+ showLoading(true);
131
+
132
+ if (isEditing) {
133
+ // Update existing item
134
+ const itemId = parseInt(itemIdInput.value);
135
+ await updateItem(itemId, itemData);
136
+ showToast('Success', 'Item updated successfully!', 'success');
137
+ } else {
138
+ // Create new item
139
+ await createItem(itemData);
140
+ showToast('Success', 'New item created successfully!', 'success');
141
+ }
142
+
143
+ resetForm();
144
+ fetchItems();
145
+ } catch (error) {
146
+ console.error('Error saving item:', error);
147
+ showToast('Error', 'Failed to save item. Please try again.', 'error');
148
+ showLoading(false);
149
+ }
150
+ }
151
+
152
+ async function createItem(itemData) {
153
+ const response = await fetch(API_URL, {
154
+ method: 'POST',
155
+ headers: {
156
+ 'Content-Type': 'application/json'
157
+ },
158
+ body: JSON.stringify(itemData)
159
+ });
160
+
161
+ if (!response.ok) {
162
+ const error = await response.json();
163
+ throw new Error(error.detail || 'Failed to create item');
164
+ }
165
+
166
+ return await response.json();
167
+ }
168
+
169
+ async function updateItem(itemId, itemData) {
170
+ const response = await fetch(`${API_URL}/${itemId}`, {
171
+ method: 'PUT',
172
+ headers: {
173
+ 'Content-Type': 'application/json'
174
+ },
175
+ body: JSON.stringify(itemData)
176
+ });
177
+
178
+ if (!response.ok) {
179
+ const error = await response.json();
180
+ throw new Error(error.detail || 'Failed to update item');
181
+ }
182
+
183
+ return await response.json();
184
+ }
185
+
186
+ async function deleteItem(itemId) {
187
+ // Create custom confirm dialog
188
+ if (!confirm('Are you sure you want to delete this item? This action cannot be undone.')) {
189
+ return;
190
+ }
191
+
192
+ try {
193
+ showLoading(true);
194
+
195
+ const response = await fetch(`${API_URL}/${itemId}`, {
196
+ method: 'DELETE'
197
+ });
198
+
199
+ if (!response.ok) {
200
+ const error = await response.json();
201
+ throw new Error(error.detail || 'Failed to delete item');
202
+ }
203
+
204
+ showToast('Success', 'Item deleted successfully!', 'success');
205
+
206
+ // Remove item from local array
207
+ items = items.filter(item => item.id !== itemId);
208
+
209
+ // Re-render items
210
+ renderItems(items);
211
+ showLoading(false);
212
+ } catch (error) {
213
+ console.error('Error deleting item:', error);
214
+ showToast('Error', 'Failed to delete item. Please try again.', 'error');
215
+ showLoading(false);
216
+ }
217
+ }
218
+
219
+ function editItem(item) {
220
+ // Set form to edit mode
221
+ isEditing = true;
222
+ formTitle.innerHTML = '<i class="bi bi-pencil-square me-2"></i>Edit Item';
223
+ submitBtn.innerHTML = '<i class="bi bi-save me-2"></i>Update';
224
+ cancelBtn.style.display = 'block';
225
+
226
+ // Populate form with item data
227
+ itemIdInput.value = item.id;
228
+ titleInput.value = item.title;
229
+ descriptionInput.value = item.description || '';
230
+ completedCheckbox.checked = item.completed;
231
+
232
+ // Scroll to form
233
+ window.scrollTo({
234
+ top: itemForm.offsetTop - 100,
235
+ behavior: 'smooth'
236
+ });
237
+
238
+ // Add highlight class to row
239
+ const row = document.getElementById(`item-row-${item.id}`);
240
+ if (row) {
241
+ row.classList.add('highlight-row');
242
+ setTimeout(() => {
243
+ row.classList.remove('highlight-row');
244
+ }, 2000);
245
+ }
246
+
247
+ // Focus on title input
248
+ titleInput.focus();
249
+ }
250
+
251
+ function resetForm() {
252
+ // Reset form state
253
+ isEditing = false;
254
+ formTitle.innerHTML = '<i class="bi bi-plus-circle me-2"></i>Add New Item';
255
+ submitBtn.innerHTML = '<i class="bi bi-save me-2"></i>Save';
256
+ cancelBtn.style.display = 'none';
257
+
258
+ // Clear form inputs
259
+ itemForm.reset();
260
+ itemIdInput.value = '';
261
+ }
262
+
263
+ function showToast(title, message, type = 'info') {
264
+ toastTitle.textContent = title;
265
+ toastMessage.textContent = message;
266
+
267
+ // Remove any existing classes
268
+ toastElement.classList.remove('bg-success', 'bg-danger', 'bg-info', 'bg-warning', 'text-white');
269
+ toastIcon.classList.remove('bi-check-circle-fill', 'bi-exclamation-triangle-fill', 'bi-info-circle-fill');
270
+
271
+ // Add appropriate styling based on type
272
+ if (type === 'success') {
273
+ toastElement.classList.add('bg-success', 'text-white');
274
+ toastIcon.classList.add('bi-check-circle-fill');
275
+ } else if (type === 'error') {
276
+ toastElement.classList.add('bg-danger', 'text-white');
277
+ toastIcon.classList.add('bi-exclamation-triangle-fill');
278
+ } else {
279
+ toastElement.classList.add('bg-info', 'text-white');
280
+ toastIcon.classList.add('bi-info-circle-fill');
281
+ }
282
+
283
+ toast.show();
284
+ }
285
+
286
+ function showLoading(isLoading) {
287
+ if (isLoading) {
288
+ loadingSpinner.style.display = 'inline-block';
289
+ } else {
290
+ loadingSpinner.style.display = 'none';
291
+ }
292
+ }
293
+
294
+ // Utility function to prevent XSS
295
+ function escapeHtml(str) {
296
+ if (!str) return '';
297
+ return str
298
+ .replace(/&/g, '&amp;')
299
+ .replace(/</g, '&lt;')
300
+ .replace(/>/g, '&gt;')
301
+ .replace(/"/g, '&quot;')
302
+ .replace(/'/g, '&#039;');
303
+ }
app/templates/index.html ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>FastAPI CRUD App</title>
7
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/css/bootstrap.min.css" rel="stylesheet">
8
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css">
9
+ <link rel="preconnect" href="https://fonts.googleapis.com">
10
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
+ <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
12
+ <link rel="stylesheet" href="{{ url_for('static', path='/css/styles.css') }}">
13
+ </head>
14
+ <body>
15
+ <header class="text-center py-4 mb-5">
16
+ <div class="container">
17
+ <h1 class="display-4 fw-bold">FastAPI CRUD Application</h1>
18
+ <p class="lead mb-0">Create, Read, Update, and Delete Items</p>
19
+ </div>
20
+ </header>
21
+
22
+ <main class="container mb-5">
23
+ <div class="row g-4">
24
+ <!-- Form section -->
25
+ <div class="col-lg-4 col-md-5">
26
+ <div class="card h-100">
27
+ <div class="card-header d-flex align-items-center">
28
+ <i class="bi bi-plus-circle me-2"></i>
29
+ <h2 class="h5 mb-0" id="form-title">Add New Item</h2>
30
+ </div>
31
+ <div class="card-body">
32
+ <form id="item-form">
33
+ <input type="hidden" id="item-id">
34
+ <div class="mb-4">
35
+ <label for="title" class="form-label">Title</label>
36
+ <input type="text" class="form-control" id="title" placeholder="Enter item title" required>
37
+ </div>
38
+ <div class="mb-4">
39
+ <label for="description" class="form-label">Description</label>
40
+ <textarea class="form-control" id="description" rows="4" placeholder="Enter item description"></textarea>
41
+ </div>
42
+ <div class="mb-4 form-check">
43
+ <input type="checkbox" class="form-check-input" id="completed">
44
+ <label class="form-check-label" for="completed">Mark as completed</label>
45
+ </div>
46
+ <div class="d-grid gap-2 d-md-flex justify-content-md-between">
47
+ <button type="submit" class="btn btn-primary px-4" id="submit-btn">
48
+ <i class="bi bi-save me-2"></i>Save
49
+ </button>
50
+ <button type="button" class="btn btn-secondary px-4" id="cancel-btn" style="display: none;">
51
+ <i class="bi bi-x-circle me-2"></i>Cancel
52
+ </button>
53
+ </div>
54
+ </form>
55
+ </div>
56
+ </div>
57
+ </div>
58
+
59
+ <!-- Items list section -->
60
+ <div class="col-lg-8 col-md-7">
61
+ <div class="card h-100">
62
+ <div class="card-header d-flex justify-content-between align-items-center">
63
+ <div class="d-flex align-items-center">
64
+ <i class="bi bi-list-ul me-2"></i>
65
+ <h2 class="h5 mb-0">Items List</h2>
66
+ </div>
67
+ <div class="spinner-border text-light spinner-border-sm" role="status" id="loading-spinner">
68
+ <span class="visually-hidden">Loading...</span>
69
+ </div>
70
+ </div>
71
+ <div class="card-body">
72
+ <div class="table-responsive">
73
+ <table class="table table-hover">
74
+ <thead>
75
+ <tr>
76
+ <th scope="col" width="5%">ID</th>
77
+ <th scope="col" width="25%">Title</th>
78
+ <th scope="col" width="40%">Description</th>
79
+ <th scope="col" width="15%">Status</th>
80
+ <th scope="col" width="15%" class="text-center">Actions</th>
81
+ </tr>
82
+ </thead>
83
+ <tbody id="items-table-body">
84
+ <!-- Items will be loaded here -->
85
+ </tbody>
86
+ </table>
87
+ </div>
88
+ <div id="no-items-message" style="display: none;">
89
+ <i class="bi bi-inbox text-muted d-block text-center" style="font-size: 3rem;"></i>
90
+ <p class="text-center mt-3">No items found. Create a new item to get started.</p>
91
+ <div class="text-center">
92
+ <button class="btn btn-primary" id="create-first-item-btn">
93
+ <i class="bi bi-plus-circle me-2"></i>Create First Item
94
+ </button>
95
+ </div>
96
+ </div>
97
+ </div>
98
+ </div>
99
+ </div>
100
+ </div>
101
+ </main>
102
+
103
+ <!-- Toast notifications -->
104
+ <div class="toast-container position-fixed top-0 end-0 p-3">
105
+ <div id="toast" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
106
+ <div class="toast-header">
107
+ <i class="bi me-2" id="toast-icon"></i>
108
+ <strong class="me-auto" id="toast-title">Notification</strong>
109
+ <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
110
+ </div>
111
+ <div class="toast-body" id="toast-message">
112
+ <!-- Toast message will be inserted here -->
113
+ </div>
114
+ </div>
115
+ </div>
116
+
117
+ <footer class="py-4">
118
+ <div class="container">
119
+ <p>FastAPI CRUD Application &copy; 2025 | Built with <i class="bi bi-heart-fill text-danger"></i> using FastAPI & SQLite</p>
120
+ </div>
121
+ </footer>
122
+
123
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/js/bootstrap.bundle.min.js"></script>
124
+ <script src="{{ url_for('static', path='/js/script.js') }}"></script>
125
+ </body>
126
+ </html>