Compare commits
1 Commits
test
..
d9b90f6d3e
| Author | SHA1 | Date | |
|---|---|---|---|
| d9b90f6d3e |
-29
@@ -1,30 +1 @@
|
|||||||
/backups
|
/backups
|
||||||
|
|
||||||
# Python
|
|
||||||
__pycache__
|
|
||||||
# Logs
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
pnpm-debug.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
|
|
||||||
node_modules
|
|
||||||
dist
|
|
||||||
dist-ssr
|
|
||||||
*.local
|
|
||||||
|
|
||||||
# Editor directories and files
|
|
||||||
.vscode/*
|
|
||||||
!.vscode/extensions.json
|
|
||||||
.idea
|
|
||||||
.DS_Store
|
|
||||||
*.suo
|
|
||||||
*.ntvs*
|
|
||||||
*.njsproj
|
|
||||||
*.sln
|
|
||||||
*.sw?
|
|
||||||
|
|
||||||
.env
|
|
||||||
|
|||||||
@@ -31,14 +31,5 @@ Has an automatic backup on a CRON that outputs to the root volume.
|
|||||||
### How to build?
|
### How to build?
|
||||||
|
|
||||||
The whole thing should run by using docker.
|
The whole thing should run by using docker.
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose up
|
|
||||||
```
|
|
||||||
|
|
||||||
There is a docker-compose file in the root, I would suggest changing the user/password to your db if you ever intend for this to leave your home network.
|
There is a docker-compose file in the root, I would suggest changing the user/password to your db if you ever intend for this to leave your home network.
|
||||||
I'm sure there are a lot of other security measures as well, but at the time of writing this I'm not particular about the security for the web application 2 people might view.
|
I'm sure there are a lot of other security measures as well, but at the time of writing this I'm not particular about the security for the web application 2 people might view.
|
||||||
|
|
||||||
### I have a bug
|
|
||||||
|
|
||||||
If you run into any issues feel free to email me at `alexander.muszynski@gmail.com` with the subject being `BlogSiteGitea: {myissue}` or make an issue on the gitea repo. I may or may not get around to it, I just work on this as I feel like it.
|
|
||||||
|
|||||||
@@ -1,9 +1 @@
|
|||||||
# Server
|
# Server
|
||||||
|
|
||||||
The server is built using FastAPI on port `8000`.
|
|
||||||
|
|
||||||
If you are running locally you can view all the apis by navigating to `localhost:8000/docs`
|
|
||||||
|
|
||||||
This project uses `sqlalchemy` and `alembic` to access and manage the database which is a `postgresql` instance that is also created during the docker compose step.
|
|
||||||
|
|
||||||
I would suggest using docker compose for all testing as that is what I will do, running the server in isolation may have bugs so be cautious.
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -1,12 +1,10 @@
|
|||||||
from logging.config import fileConfig
|
from logging.config import fileConfig
|
||||||
|
from app.database import Base
|
||||||
from sqlalchemy import engine_from_config
|
from sqlalchemy import engine_from_config
|
||||||
from sqlalchemy import pool
|
from sqlalchemy import pool
|
||||||
|
|
||||||
from alembic import context
|
from alembic import context
|
||||||
|
|
||||||
from app.database import Base
|
|
||||||
|
|
||||||
# this is the Alembic Config object, which provides
|
# this is the Alembic Config object, which provides
|
||||||
# access to the values within the .ini file in use.
|
# access to the values within the .ini file in use.
|
||||||
config = context.config
|
config = context.config
|
||||||
@@ -18,7 +16,6 @@ if config.config_file_name is not None:
|
|||||||
|
|
||||||
# add your model's MetaData object here
|
# add your model's MetaData object here
|
||||||
# for 'autogenerate' support
|
# for 'autogenerate' support
|
||||||
# from myapp import mymodel
|
|
||||||
target_metadata = Base.metadata
|
target_metadata = Base.metadata
|
||||||
|
|
||||||
# other values from the config, defined by the needs of env.py,
|
# other values from the config, defined by the needs of env.py,
|
||||||
|
|||||||
Binary file not shown.
@@ -0,0 +1,41 @@
|
|||||||
|
"""init
|
||||||
|
|
||||||
|
Revision ID: a3ac646e53a8
|
||||||
|
Revises:
|
||||||
|
Create Date: 2025-06-04 21:36:22.283823
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'a3ac646e53a8'
|
||||||
|
down_revision: Union[str, None] = None
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
"""Upgrade schema."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_index(op.f('ix_items_id'), table_name='items')
|
||||||
|
op.drop_index(op.f('ix_items_name'), table_name='items')
|
||||||
|
op.drop_table('items')
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
"""Downgrade schema."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('items',
|
||||||
|
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
|
||||||
|
sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=True),
|
||||||
|
sa.Column('description', sa.VARCHAR(), autoincrement=False, nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id', name=op.f('items_pkey'))
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_items_name'), 'items', ['name'], unique=False)
|
||||||
|
op.create_index(op.f('ix_items_id'), 'items', ['id'], unique=False)
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
"""Add author_name to blogs
|
|
||||||
|
|
||||||
Revision ID: c9b28e38d00c
|
|
||||||
Revises:
|
|
||||||
Create Date: 2025-06-24 20:51:56.034469
|
|
||||||
|
|
||||||
"""
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
from sqlalchemy.dialects import postgresql
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = 'c9b28e38d00c'
|
|
||||||
down_revision: Union[str, None] = None
|
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
"""Upgrade schema."""
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.drop_index(op.f('ix_users_email'), table_name='users')
|
|
||||||
op.drop_index(op.f('ix_users_id'), table_name='users')
|
|
||||||
op.drop_index(op.f('ix_users_username'), table_name='users')
|
|
||||||
op.drop_table('users')
|
|
||||||
op.drop_index(op.f('ix_blogs_author_id'), table_name='blogs')
|
|
||||||
op.drop_index(op.f('ix_blogs_id'), table_name='blogs')
|
|
||||||
op.drop_index(op.f('ix_blogs_title'), table_name='blogs')
|
|
||||||
op.drop_table('blogs')
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
"""Downgrade schema."""
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.create_table('blogs',
|
|
||||||
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
|
|
||||||
sa.Column('title', sa.VARCHAR(), autoincrement=False, nullable=True),
|
|
||||||
sa.Column('author_id', sa.INTEGER(), autoincrement=False, nullable=False),
|
|
||||||
sa.Column('description', sa.VARCHAR(), autoincrement=False, nullable=True),
|
|
||||||
sa.Column('body', sa.VARCHAR(), autoincrement=False, nullable=False),
|
|
||||||
sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=False),
|
|
||||||
sa.Column('updated_at', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True),
|
|
||||||
sa.Column('published_at', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True),
|
|
||||||
sa.Column('word_count', sa.INTEGER(), autoincrement=False, nullable=True),
|
|
||||||
sa.Column('version', sa.INTEGER(), autoincrement=False, nullable=True),
|
|
||||||
sa.Column('read_time', sa.INTEGER(), autoincrement=False, nullable=True),
|
|
||||||
sa.Column('language', sa.VARCHAR(), autoincrement=False, nullable=True),
|
|
||||||
sa.Column('tags', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=True),
|
|
||||||
sa.Column('view_count', sa.INTEGER(), autoincrement=False, nullable=False),
|
|
||||||
sa.Column('like_count', sa.INTEGER(), autoincrement=False, nullable=False),
|
|
||||||
sa.PrimaryKeyConstraint('id', name=op.f('blogs_pkey'))
|
|
||||||
)
|
|
||||||
op.create_index(op.f('ix_blogs_title'), 'blogs', ['title'], unique=False)
|
|
||||||
op.create_index(op.f('ix_blogs_id'), 'blogs', ['id'], unique=False)
|
|
||||||
op.create_index(op.f('ix_blogs_author_id'), 'blogs', ['author_id'], unique=False)
|
|
||||||
op.create_table('users',
|
|
||||||
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
|
|
||||||
sa.Column('username', sa.VARCHAR(), autoincrement=False, nullable=False),
|
|
||||||
sa.Column('email', sa.VARCHAR(), autoincrement=False, nullable=False),
|
|
||||||
sa.Column('hashed_password', sa.VARCHAR(), autoincrement=False, nullable=False),
|
|
||||||
sa.Column('permissions', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=False),
|
|
||||||
sa.Column('subscriber', sa.BOOLEAN(), autoincrement=False, nullable=False),
|
|
||||||
sa.PrimaryKeyConstraint('id', name=op.f('users_pkey'))
|
|
||||||
)
|
|
||||||
op.create_index(op.f('ix_users_username'), 'users', ['username'], unique=True)
|
|
||||||
op.create_index(op.f('ix_users_id'), 'users', ['id'], unique=False)
|
|
||||||
op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True)
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+14
-67
@@ -1,75 +1,26 @@
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from app import models, schemas
|
from app import models, schemas
|
||||||
from app.utils import hash_password, verify_password
|
from app.utils import hash_password, verify_password
|
||||||
from typing import List, Optional
|
|
||||||
|
|
||||||
|
|
||||||
def get_blog(db: Session, blog_id: int) -> Optional[models.Blog]:
|
def get_item(db: Session, item_id: int):
|
||||||
return db.query(models.Blog).filter(models.Blog.id == blog_id).first()
|
return db.query(models.Item).filter(models.Item.id == item_id).first()
|
||||||
|
|
||||||
|
|
||||||
def get_blogs(
|
def get_items(db: Session, skip: int = 0, limit: int = 10):
|
||||||
db: Session,
|
return db.query(models.Item).offset(skip).limit(limit).all()
|
||||||
skip: int = 0,
|
|
||||||
limit: int = 10,
|
|
||||||
author_id: Optional[int] = None,
|
|
||||||
visibility: Optional[str] = None,
|
|
||||||
) -> List[models.Blog]:
|
|
||||||
q = db.query(models.Blog)
|
|
||||||
if author_id is not None:
|
|
||||||
q = q.filter(models.Blog.author_id == author_id)
|
|
||||||
if visibility is not None:
|
|
||||||
q = q.filter(models.Blog.visibility == visibility)
|
|
||||||
return q.offset(skip).limit(limit).all()
|
|
||||||
|
|
||||||
|
|
||||||
def create_blog(db: Session, blog_in: schemas.BlogCreate) -> models.Blog:
|
def create_item(db: Session, item: schemas.Item):
|
||||||
db_blog = models.Blog(**blog_in.model_dump())
|
db_item = models.Item(**item.model_dump())
|
||||||
db.add(db_blog)
|
db.add(db_item)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(db_blog)
|
db.refresh(db_item)
|
||||||
return db_blog
|
return db_item
|
||||||
|
|
||||||
|
|
||||||
def update_blog(
|
def delete_item(db: Session, item_id: int):
|
||||||
db: Session, blog_id: int, blog_in: schemas.BlogUpdate
|
item = db.query(models.Item).filter(models.Item.id == item_id).first()
|
||||||
) -> Optional[models.Blog]:
|
|
||||||
db_blog = get_blog(db, blog_id)
|
|
||||||
if not db_blog:
|
|
||||||
return None
|
|
||||||
update_data = blog_in.model_dump(exclude_unset=True)
|
|
||||||
for field, value in update_data.items():
|
|
||||||
setattr(db_blog, field, value)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(db_blog)
|
|
||||||
return db_blog
|
|
||||||
|
|
||||||
|
|
||||||
def delete_blog(db: Session, blog_id: int) -> Optional[models.Blog]:
|
|
||||||
db_blog = get_blog(db, blog_id)
|
|
||||||
if not db_blog:
|
|
||||||
return None
|
|
||||||
db.delete(db_blog)
|
|
||||||
db.commit()
|
|
||||||
return db_blog
|
|
||||||
|
|
||||||
|
|
||||||
def increment_view_count(db: Session, blog_id: int) -> None:
|
|
||||||
db.query(models.Blog).filter(models.Blog.id == blog_id).update(
|
|
||||||
{"view_count": models.Blog.view_count + 1}
|
|
||||||
)
|
|
||||||
db.commit()
|
|
||||||
|
|
||||||
|
|
||||||
def add_like(db: Session, blog_id: int) -> None:
|
|
||||||
db.query(models.Blog).filter(models.Blog.id == blog_id).update(
|
|
||||||
{"like_count": models.Blog.like_count + 1}
|
|
||||||
)
|
|
||||||
db.commit()
|
|
||||||
|
|
||||||
|
|
||||||
def delete_blogs(db: Session, item_id: int):
|
|
||||||
item = db.query(models.Blog).filter(models.Blog.id == item_id).first()
|
|
||||||
if item:
|
if item:
|
||||||
db.delete(item)
|
db.delete(item)
|
||||||
db.commit()
|
db.commit()
|
||||||
@@ -80,10 +31,10 @@ def delete_blogs(db: Session, item_id: int):
|
|||||||
|
|
||||||
|
|
||||||
def authenticate_user(db: Session, username: str, password: str):
|
def authenticate_user(db: Session, username: str, password: str):
|
||||||
user = get_user_by_username(db, username) or get_user_by_email(db, username)
|
user = get_user_by_username(db, username)
|
||||||
if not user:
|
if not user:
|
||||||
return None
|
return None
|
||||||
if not verify_password(password, user.hashed_password):
|
if not verify_password(password, str(user.hashed_password)):
|
||||||
return None
|
return None
|
||||||
return user
|
return user
|
||||||
|
|
||||||
@@ -99,11 +50,7 @@ def get_user_by_email(db: Session, email: str):
|
|||||||
def create_user(db: Session, user: schemas.UserCreate):
|
def create_user(db: Session, user: schemas.UserCreate):
|
||||||
hashed_pw = hash_password(user.password)
|
hashed_pw = hash_password(user.password)
|
||||||
db_user = models.User(
|
db_user = models.User(
|
||||||
username=user.username,
|
username=user.username, email=user.email, hashed_password=hashed_pw
|
||||||
email=user.email,
|
|
||||||
hashed_password=hashed_pw,
|
|
||||||
permissions=user.permissions,
|
|
||||||
subscriber=user.subscriber,
|
|
||||||
)
|
)
|
||||||
db.add(db_user)
|
db.add(db_user)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ from dotenv import load_dotenv
|
|||||||
env = load_dotenv()
|
env = load_dotenv()
|
||||||
DATABASE_URL = os.getenv("DATABASE_URL", "")
|
DATABASE_URL = os.getenv("DATABASE_URL", "")
|
||||||
|
|
||||||
print(DATABASE_URL)
|
|
||||||
engine = create_engine(DATABASE_URL)
|
engine = create_engine(DATABASE_URL)
|
||||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
Base = declarative_base()
|
Base = declarative_base()
|
||||||
|
|||||||
+22
-74
@@ -1,9 +1,6 @@
|
|||||||
from fastapi import FastAPI, Depends, HTTPException, status
|
from fastapi import FastAPI, Depends, HTTPException, status
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
|
||||||
from fastapi.middleware.trustedhost import TrustedHostMiddleware
|
|
||||||
from fastapi.security import OAuth2PasswordRequestForm
|
from fastapi.security import OAuth2PasswordRequestForm
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from app.utils import create_access_token
|
from app.utils import create_access_token
|
||||||
from . import schemas, crud
|
from . import schemas, crud
|
||||||
@@ -11,26 +8,7 @@ from .database import SessionLocal, engine, Base
|
|||||||
|
|
||||||
Base.metadata.create_all(bind=engine)
|
Base.metadata.create_all(bind=engine)
|
||||||
|
|
||||||
app = FastAPI(proxy_headers=True)
|
app = FastAPI()
|
||||||
|
|
||||||
app.add_middleware(
|
|
||||||
TrustedHostMiddleware, allowed_hosts=["site-api.muszyn.dev", "*.muszyn.dev"]
|
|
||||||
)
|
|
||||||
app.add_middleware(
|
|
||||||
CORSMiddleware,
|
|
||||||
allow_origins=[
|
|
||||||
"http://localhost:3000",
|
|
||||||
"http://localhost:8000",
|
|
||||||
"http://192.168.125.129:3000",
|
|
||||||
"https://192.168.125.129:3000",
|
|
||||||
"http://192.168.125.129:8000",
|
|
||||||
"https://192.168.125.129:8000",
|
|
||||||
"https://site.muszyn.dev",
|
|
||||||
],
|
|
||||||
allow_credentials=True,
|
|
||||||
allow_methods=["*"],
|
|
||||||
allow_headers=["*"],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# Dependency
|
# Dependency
|
||||||
@@ -47,64 +25,35 @@ def health_check():
|
|||||||
return {"Health": "Super Healthy!"}
|
return {"Health": "Super Healthy!"}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/blogs/", response_model=schemas.Blog)
|
@app.post("/items/", response_model=schemas.Item)
|
||||||
def create_blog(
|
def create_item(item: schemas.Item, db: Session = Depends(get_db)):
|
||||||
blog: schemas.BlogCreate,
|
return crud.create_item(db, item)
|
||||||
db: Session = Depends(get_db),
|
|
||||||
):
|
|
||||||
return crud.create_blog(db, blog)
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/blogs/", response_model=list[schemas.Blog])
|
@app.get("/items/", response_model=list[schemas.Item])
|
||||||
def read_blogs(
|
def read_items(skip: int = 0, limit: int = 10, db: Session = Depends(get_db)):
|
||||||
skip: int = 0,
|
return crud.get_items(db, skip, limit)
|
||||||
limit: int = 10,
|
|
||||||
author_id: Optional[int] = None,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
):
|
|
||||||
return crud.get_blogs(
|
|
||||||
db,
|
|
||||||
skip=skip,
|
|
||||||
limit=limit,
|
|
||||||
author_id=author_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/blogs/{blog_id}", response_model=schemas.Blog)
|
@app.get("/items/{item_id}", response_model=schemas.Item)
|
||||||
def read_blog(
|
def read_item(item_id: int, db: Session = Depends(get_db)):
|
||||||
blog_id: int,
|
db_item = crud.get_item(db, item_id)
|
||||||
db: Session = Depends(get_db),
|
if db_item is None:
|
||||||
):
|
raise HTTPException(status_code=404, detail="Item not found")
|
||||||
db_blog = crud.get_blog(db, blog_id)
|
return db_item
|
||||||
if not db_blog:
|
|
||||||
raise HTTPException(status_code=404, detail="Blog not found")
|
|
||||||
return db_blog
|
|
||||||
|
|
||||||
|
|
||||||
@app.put("/blogs/{blog_id}", response_model=schemas.Blog)
|
@app.delete("/items/{item_id}", response_model=schemas.Item)
|
||||||
def update_blog(
|
def delete_item(item_id: int, db: Session = Depends(get_db)):
|
||||||
blog_id: int,
|
item = crud.delete_item(db, item_id)
|
||||||
blog_in: schemas.BlogUpdate,
|
if item is None:
|
||||||
db: Session = Depends(get_db),
|
raise HTTPException(status_code=404, detail="Item not found")
|
||||||
):
|
return item
|
||||||
updated = crud.update_blog(db, blog_id, blog_in)
|
|
||||||
if not updated:
|
|
||||||
raise HTTPException(status_code=404, detail="Blog not found")
|
|
||||||
return updated
|
|
||||||
|
|
||||||
|
|
||||||
@app.delete("/blogs/{blog_id}", response_model=schemas.Blog)
|
|
||||||
def delete_blog(
|
|
||||||
blog_id: int,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
):
|
|
||||||
deleted = crud.delete_blog(db, blog_id)
|
|
||||||
if not deleted:
|
|
||||||
raise HTTPException(status_code=404, detail="Blog not found")
|
|
||||||
return deleted
|
|
||||||
|
|
||||||
|
|
||||||
# Users
|
# Users
|
||||||
|
|
||||||
|
|
||||||
@app.post("/login", response_model=schemas.Token)
|
@app.post("/login", response_model=schemas.Token)
|
||||||
def user_login(
|
def user_login(
|
||||||
form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)
|
form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)
|
||||||
@@ -117,7 +66,7 @@ def user_login(
|
|||||||
headers={"WWW-Authenticate": "Bearer"},
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
)
|
)
|
||||||
access_token = create_access_token(data={"sub": user.username})
|
access_token = create_access_token(data={"sub": user.username})
|
||||||
return {"access_token": access_token, "token_type": "bearer", "user_id": user.id}
|
return {"access_token": access_token, "token_type": "bearer"}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/register", response_model=schemas.UserOut)
|
@app.post("/register", response_model=schemas.UserOut)
|
||||||
@@ -132,5 +81,4 @@ def register_user(user: schemas.UserCreate, db: Session = Depends(get_db)):
|
|||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="Account with that email already registered",
|
detail="Account with that email already registered",
|
||||||
)
|
)
|
||||||
# Default Cases
|
|
||||||
return crud.create_user(db, user)
|
return crud.create_user(db, user)
|
||||||
|
|||||||
+5
-31
@@ -1,40 +1,14 @@
|
|||||||
from sqlalchemy import (
|
from sqlalchemy import JSON, Boolean, Column, Integer, String
|
||||||
JSON,
|
|
||||||
Boolean,
|
|
||||||
Column,
|
|
||||||
DateTime,
|
|
||||||
Integer,
|
|
||||||
String,
|
|
||||||
func,
|
|
||||||
)
|
|
||||||
from .database import Base
|
from .database import Base
|
||||||
|
|
||||||
|
|
||||||
class Blog(Base):
|
class Item(Base):
|
||||||
__tablename__ = "blogs"
|
__tablename__ = "items"
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
title = Column(String, index=True)
|
name = Column(String, index=True)
|
||||||
author_id = Column(Integer, nullable=False, index=True)
|
|
||||||
author_name = Column(String, nullable=False, index=True)
|
|
||||||
description = Column(String, nullable=True)
|
description = Column(String, nullable=True)
|
||||||
body = Column(String, nullable=False)
|
body = Column(JSON, nullable=False)
|
||||||
created_at = Column(
|
|
||||||
DateTime(timezone=True), server_default=func.now(), nullable=False
|
|
||||||
)
|
|
||||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
|
||||||
published_at = Column(DateTime(timezone=True), nullable=True)
|
|
||||||
|
|
||||||
# Document Meta Data
|
|
||||||
word_count = Column(Integer, nullable=True)
|
|
||||||
version = Column(Integer, nullable=True)
|
|
||||||
read_time = Column(Integer, nullable=True)
|
|
||||||
language = Column(String, nullable=True)
|
|
||||||
tags = Column(JSON, nullable=True)
|
|
||||||
|
|
||||||
# User Meta Data
|
|
||||||
view_count = Column(Integer, default=0, nullable=False)
|
|
||||||
like_count = Column(Integer, default=0, nullable=False)
|
|
||||||
|
|
||||||
|
|
||||||
class User(Base):
|
class User(Base):
|
||||||
|
|||||||
+9
-53
@@ -1,58 +1,17 @@
|
|||||||
from typing import Optional
|
from pydantic import BaseModel, EmailStr
|
||||||
from pydantic import AwareDatetime, BaseModel, EmailStr
|
|
||||||
|
|
||||||
|
|
||||||
class BlogBase(BaseModel):
|
# DB Schemas
|
||||||
title: str
|
|
||||||
author_id: int
|
|
||||||
author_name: str
|
class ItemBase(BaseModel):
|
||||||
description: Optional[str] = None
|
name: str
|
||||||
|
description: str | None = None
|
||||||
body: str
|
body: str
|
||||||
created_at: AwareDatetime
|
|
||||||
updated_at: AwareDatetime
|
|
||||||
published_at: AwareDatetime
|
|
||||||
|
|
||||||
# Document meta
|
|
||||||
word_count: Optional[int] = None
|
|
||||||
version: Optional[int] = None
|
|
||||||
read_time: Optional[int] = None
|
|
||||||
language: Optional[str] = None
|
|
||||||
tags: Optional[list[str]] = None
|
|
||||||
|
|
||||||
|
|
||||||
class BlogCreate(BlogBase):
|
class Item(ItemBase):
|
||||||
"""All fields required to create a new blog post."""
|
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class BlogUpdate(BaseModel):
|
|
||||||
"""All fields are optional for partial updates."""
|
|
||||||
|
|
||||||
title: Optional[str] = None
|
|
||||||
description: Optional[str] = None
|
|
||||||
author_name: Optional[str] = None
|
|
||||||
body: Optional[str] = None
|
|
||||||
created_at: Optional[str] = None
|
|
||||||
updated_at: Optional[str] = None
|
|
||||||
published_at: Optional[str] = None
|
|
||||||
|
|
||||||
word_count: Optional[int] = None
|
|
||||||
version: Optional[int] = None
|
|
||||||
read_time: Optional[int] = None
|
|
||||||
language: Optional[str] = None
|
|
||||||
tags: Optional[list[str]] = None
|
|
||||||
|
|
||||||
view_count: Optional[int] = None
|
|
||||||
like_count: Optional[int] = None
|
|
||||||
|
|
||||||
|
|
||||||
class Blog(BlogBase):
|
|
||||||
"""What’s returned in responses."""
|
|
||||||
|
|
||||||
id: int
|
id: int
|
||||||
view_count: int
|
|
||||||
like_count: int
|
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
from_attributes = True
|
from_attributes = True
|
||||||
@@ -65,15 +24,13 @@ class UserBase(BaseModel):
|
|||||||
|
|
||||||
class UserCreate(UserBase):
|
class UserCreate(UserBase):
|
||||||
password: str
|
password: str
|
||||||
permissions: dict = {}
|
|
||||||
subscriber: bool = False
|
|
||||||
|
|
||||||
|
|
||||||
class UserOut(UserBase):
|
class UserOut(UserBase):
|
||||||
id: int
|
id: int
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
from_attributes = True
|
orm_mode = True
|
||||||
|
|
||||||
|
|
||||||
# Other Schemas
|
# Other Schemas
|
||||||
@@ -82,7 +39,6 @@ class UserOut(UserBase):
|
|||||||
class Token(BaseModel):
|
class Token(BaseModel):
|
||||||
access_token: str
|
access_token: str
|
||||||
token_type: str
|
token_type: str
|
||||||
user_id: int
|
|
||||||
|
|
||||||
|
|
||||||
class TokenData(BaseModel):
|
class TokenData(BaseModel):
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
from typing import Any, Mapping
|
from typing import Any, Mapping
|
||||||
from passlib.context import CryptContext
|
from passlib.context import CryptContext
|
||||||
@@ -5,8 +6,7 @@ from datetime import UTC, datetime, timedelta
|
|||||||
from jose import JWTError, jwt
|
from jose import JWTError, jwt
|
||||||
from app.logger_config import Logger
|
from app.logger_config import Logger
|
||||||
|
|
||||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
pwd_context = CryptContext(schemas=["bcrypt"], deprecated="auto")
|
||||||
_logger = Logger().logger
|
|
||||||
|
|
||||||
|
|
||||||
def hash_password(password: str) -> str:
|
def hash_password(password: str) -> str:
|
||||||
@@ -37,5 +37,5 @@ def decode_access_token(token: str) -> Mapping[Any, Any] | None:
|
|||||||
try:
|
try:
|
||||||
return jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
return jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||||||
except JWTError:
|
except JWTError:
|
||||||
_logger.exception(msg="Failed to Decode JWT", extra={"TOKEN": token})
|
logging.exception(msg="Failed to Decode JWT", extra={"TOKEN": token})
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ dependencies = [
|
|||||||
"fastapi[standard]>=0.115.12",
|
"fastapi[standard]>=0.115.12",
|
||||||
"passlib[bcrypt]>=1.7.4",
|
"passlib[bcrypt]>=1.7.4",
|
||||||
"psycopg2-binary>=2.9.10",
|
"psycopg2-binary>=2.9.10",
|
||||||
|
"pyrefly>=0.18.1",
|
||||||
"python-jose[cryptography]>=3.5.0",
|
"python-jose[cryptography]>=3.5.0",
|
||||||
"sqlalchemy>=2.0.41",
|
"sqlalchemy>=2.0.41",
|
||||||
"uvicorn[standard]>=0.34.2",
|
"uvicorn[standard]>=0.34.2",
|
||||||
@@ -17,8 +18,3 @@ dependencies = [
|
|||||||
[tool.pyrefly]
|
[tool.pyrefly]
|
||||||
project-includes = ["**/*"]
|
project-includes = ["**/*"]
|
||||||
project-excludes = ["**/*venv/**/*"]
|
project-excludes = ["**/*venv/**/*"]
|
||||||
|
|
||||||
[dependency-groups]
|
|
||||||
dev = [
|
|
||||||
"sqlalchemy-stubs>=0.4",
|
|
||||||
]
|
|
||||||
|
|||||||
Generated
+18
-71
@@ -508,47 +508,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "mypy"
|
|
||||||
version = "1.16.1"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "mypy-extensions" },
|
|
||||||
{ name = "pathspec" },
|
|
||||||
{ name = "typing-extensions" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/81/69/92c7fa98112e4d9eb075a239caa4ef4649ad7d441545ccffbd5e34607cbb/mypy-1.16.1.tar.gz", hash = "sha256:6bd00a0a2094841c5e47e7374bb42b83d64c527a502e3334e1173a0c24437bab", size = 3324747, upload-time = "2025-06-16T16:51:35.145Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9a/61/ec1245aa1c325cb7a6c0f8570a2eee3bfc40fa90d19b1267f8e50b5c8645/mypy-1.16.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:472e4e4c100062488ec643f6162dd0d5208e33e2f34544e1fc931372e806c0cc", size = 10890557, upload-time = "2025-06-16T16:37:21.421Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6b/bb/6eccc0ba0aa0c7a87df24e73f0ad34170514abd8162eb0c75fd7128171fb/mypy-1.16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea16e2a7d2714277e349e24d19a782a663a34ed60864006e8585db08f8ad1782", size = 10012921, upload-time = "2025-06-16T16:51:28.659Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5f/80/b337a12e2006715f99f529e732c5f6a8c143bb58c92bb142d5ab380963a5/mypy-1.16.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08e850ea22adc4d8a4014651575567b0318ede51e8e9fe7a68f25391af699507", size = 11802887, upload-time = "2025-06-16T16:50:53.627Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d9/59/f7af072d09793d581a745a25737c7c0a945760036b16aeb620f658a017af/mypy-1.16.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22d76a63a42619bfb90122889b903519149879ddbf2ba4251834727944c8baca", size = 12531658, upload-time = "2025-06-16T16:33:55.002Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/82/c4/607672f2d6c0254b94a646cfc45ad589dd71b04aa1f3d642b840f7cce06c/mypy-1.16.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2c7ce0662b6b9dc8f4ed86eb7a5d505ee3298c04b40ec13b30e572c0e5ae17c4", size = 12732486, upload-time = "2025-06-16T16:37:03.301Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b6/5e/136555ec1d80df877a707cebf9081bd3a9f397dedc1ab9750518d87489ec/mypy-1.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:211287e98e05352a2e1d4e8759c5490925a7c784ddc84207f4714822f8cf99b6", size = 9479482, upload-time = "2025-06-16T16:47:37.48Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b4/d6/39482e5fcc724c15bf6280ff5806548c7185e0c090712a3736ed4d07e8b7/mypy-1.16.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:af4792433f09575d9eeca5c63d7d90ca4aeceda9d8355e136f80f8967639183d", size = 11066493, upload-time = "2025-06-16T16:47:01.683Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e6/e5/26c347890efc6b757f4d5bb83f4a0cf5958b8cf49c938ac99b8b72b420a6/mypy-1.16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:66df38405fd8466ce3517eda1f6640611a0b8e70895e2a9462d1d4323c5eb4b9", size = 10081687, upload-time = "2025-06-16T16:48:19.367Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/44/c7/b5cb264c97b86914487d6a24bd8688c0172e37ec0f43e93b9691cae9468b/mypy-1.16.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44e7acddb3c48bd2713994d098729494117803616e116032af192871aed80b79", size = 11839723, upload-time = "2025-06-16T16:49:20.912Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/15/f8/491997a9b8a554204f834ed4816bda813aefda31cf873bb099deee3c9a99/mypy-1.16.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ab5eca37b50188163fa7c1b73c685ac66c4e9bdee4a85c9adac0e91d8895e15", size = 12722980, upload-time = "2025-06-16T16:37:40.929Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/df/f0/2bd41e174b5fd93bc9de9a28e4fb673113633b8a7f3a607fa4a73595e468/mypy-1.16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb6229b2c9086247e21a83c309754b9058b438704ad2f6807f0d8227f6ebdd", size = 12903328, upload-time = "2025-06-16T16:34:35.099Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/61/81/5572108a7bec2c46b8aff7e9b524f371fe6ab5efb534d38d6b37b5490da8/mypy-1.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:1f0435cf920e287ff68af3d10a118a73f212deb2ce087619eb4e648116d1fe9b", size = 9562321, upload-time = "2025-06-16T16:48:58.823Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/28/e3/96964af4a75a949e67df4b95318fe2b7427ac8189bbc3ef28f92a1c5bc56/mypy-1.16.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ddc91eb318c8751c69ddb200a5937f1232ee8efb4e64e9f4bc475a33719de438", size = 11063480, upload-time = "2025-06-16T16:47:56.205Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f5/4d/cd1a42b8e5be278fab7010fb289d9307a63e07153f0ae1510a3d7b703193/mypy-1.16.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:87ff2c13d58bdc4bbe7dc0dedfe622c0f04e2cb2a492269f3b418df2de05c536", size = 10090538, upload-time = "2025-06-16T16:46:43.92Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c9/4f/c3c6b4b66374b5f68bab07c8cabd63a049ff69796b844bc759a0ca99bb2a/mypy-1.16.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a7cfb0fe29fe5a9841b7c8ee6dffb52382c45acdf68f032145b75620acfbd6f", size = 11836839, upload-time = "2025-06-16T16:36:28.039Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b4/7e/81ca3b074021ad9775e5cb97ebe0089c0f13684b066a750b7dc208438403/mypy-1.16.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:051e1677689c9d9578b9c7f4d206d763f9bbd95723cd1416fad50db49d52f359", size = 12715634, upload-time = "2025-06-16T16:50:34.441Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e9/95/bdd40c8be346fa4c70edb4081d727a54d0a05382d84966869738cfa8a497/mypy-1.16.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d5d2309511cc56c021b4b4e462907c2b12f669b2dbeb68300110ec27723971be", size = 12895584, upload-time = "2025-06-16T16:34:54.857Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5a/fd/d486a0827a1c597b3b48b1bdef47228a6e9ee8102ab8c28f944cb83b65dc/mypy-1.16.1-cp313-cp313-win_amd64.whl", hash = "sha256:4f58ac32771341e38a853c5d0ec0dfe27e18e27da9cdb8bbc882d2249c71a3ee", size = 9573886, upload-time = "2025-06-16T16:36:43.589Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cf/d3/53e684e78e07c1a2bf7105715e5edd09ce951fc3f47cf9ed095ec1b7a037/mypy-1.16.1-py3-none-any.whl", hash = "sha256:5fc2ac4027d0ef28d6ba69a0343737a23c4d1b83672bf38d1fe237bdc0643b37", size = 2265923, upload-time = "2025-06-16T16:48:02.366Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "mypy-extensions"
|
|
||||||
version = "1.1.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "passlib"
|
name = "passlib"
|
||||||
version = "1.7.4"
|
version = "1.7.4"
|
||||||
@@ -563,15 +522,6 @@ bcrypt = [
|
|||||||
{ name = "bcrypt" },
|
{ name = "bcrypt" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pathspec"
|
|
||||||
version = "0.12.1"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "psycopg2-binary"
|
name = "psycopg2-binary"
|
||||||
version = "2.9.10"
|
version = "2.9.10"
|
||||||
@@ -722,6 +672,22 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" },
|
{ url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyrefly"
|
||||||
|
version = "0.18.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c1/25/a01a7c1dbad97d9108f0445a75b971170b5f748781864e01e942b572161d/pyrefly-0.18.1.tar.gz", hash = "sha256:e192edc0a4916b56bc89362d569956a93f6fdd2ed60c63cbc14ea86a8d7c072b", size = 1000813, upload-time = "2025-06-07T00:19:35.917Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/21/53/ae13e448e4fab6f6106ab3b690ea36ec6d33a824f10507e5cf63515c4420/pyrefly-0.18.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:311cd1855fb991dd79aee550c26be88457ea2e547bc08568a6481dfcb8f35a4a", size = 5619666, upload-time = "2025-06-07T00:19:19.911Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0e/78/8551414e7300d4937afd74af474435d20b6e42d33962700b2c2e02c5cf85/pyrefly-0.18.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3abb545a427f105f5c743a0aa8a4bd59a8481df31c1feb6c886336804ed58395", size = 5210517, upload-time = "2025-06-07T00:19:22.192Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/23/52/edf7bda660cf7e6a7a25a75a067d46708ecf61ffa423aeb727476dcc85e8/pyrefly-0.18.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:443b4376a6c6f38cdab521e3268e3844e88c3f465eccfd757387725245640e01", size = 5415912, upload-time = "2025-06-07T00:19:24.024Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1d/4a/1b1c626a4d076c3c63d6f2fe80d6317dee61bf957766353aded6abcfb75e/pyrefly-0.18.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c5c2a3d458eae1a293ba1b3acae679ae611fd22ec486e095c02f828f2c60f06", size = 6086858, upload-time = "2025-06-07T00:19:25.944Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0f/35/995862038f8e708cab59f5f45fe830044bd85bcdd2fba330b08934e9bc2f/pyrefly-0.18.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe5801cf26fafe0cfcc99d89a2c7632bf8b1edfa05b142ad1ac18621a042de68", size = 5849624, upload-time = "2025-06-07T00:19:27.988Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/15/cb/181aada3e76fc740a6bc628b597c092042b880285c3a2fd2a465e6dfffa5/pyrefly-0.18.1-py3-none-win32.whl", hash = "sha256:31e663ca9208cfb4021026784936cd21c4350235d51bbc3cbad7c0dc97649421", size = 5243708, upload-time = "2025-06-07T00:19:29.932Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d8/3e/d1ca8927cdde89e2f202ac7b734d7cbe6970405e4c34a0329dfead0ef5a8/pyrefly-0.18.1-py3-none-win_amd64.whl", hash = "sha256:bb7da09aadd6a53bb210b8eaa4ce86471fb27222e7d2c785bb32d02254f17a66", size = 5668996, upload-time = "2025-06-07T00:19:31.88Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/15/a8/1aff0e1d26cee78fdadf86777a1ad5b305a7dec7b61e27b51d84e255f4d8/pyrefly-0.18.1-py3-none-win_arm64.whl", hash = "sha256:88220a9ff947c9f2c9ab81e06a5e334b0104b9987317e961219d218b367d9192", size = 5321319, upload-time = "2025-06-07T00:19:33.657Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-dotenv"
|
name = "python-dotenv"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
@@ -843,16 +809,12 @@ dependencies = [
|
|||||||
{ name = "fastapi", extra = ["standard"] },
|
{ name = "fastapi", extra = ["standard"] },
|
||||||
{ name = "passlib", extra = ["bcrypt"] },
|
{ name = "passlib", extra = ["bcrypt"] },
|
||||||
{ name = "psycopg2-binary" },
|
{ name = "psycopg2-binary" },
|
||||||
|
{ name = "pyrefly" },
|
||||||
{ name = "python-jose", extra = ["cryptography"] },
|
{ name = "python-jose", extra = ["cryptography"] },
|
||||||
{ name = "sqlalchemy" },
|
{ name = "sqlalchemy" },
|
||||||
{ name = "uvicorn", extra = ["standard"] },
|
{ name = "uvicorn", extra = ["standard"] },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dev-dependencies]
|
|
||||||
dev = [
|
|
||||||
{ name = "sqlalchemy-stubs" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "alembic", specifier = ">=1.16.1" },
|
{ name = "alembic", specifier = ">=1.16.1" },
|
||||||
@@ -860,14 +822,12 @@ requires-dist = [
|
|||||||
{ name = "fastapi", extras = ["standard"], specifier = ">=0.115.12" },
|
{ name = "fastapi", extras = ["standard"], specifier = ">=0.115.12" },
|
||||||
{ name = "passlib", extras = ["bcrypt"], specifier = ">=1.7.4" },
|
{ name = "passlib", extras = ["bcrypt"], specifier = ">=1.7.4" },
|
||||||
{ name = "psycopg2-binary", specifier = ">=2.9.10" },
|
{ name = "psycopg2-binary", specifier = ">=2.9.10" },
|
||||||
|
{ name = "pyrefly", specifier = ">=0.18.1" },
|
||||||
{ name = "python-jose", extras = ["cryptography"], specifier = ">=3.5.0" },
|
{ name = "python-jose", extras = ["cryptography"], specifier = ">=3.5.0" },
|
||||||
{ name = "sqlalchemy", specifier = ">=2.0.41" },
|
{ name = "sqlalchemy", specifier = ">=2.0.41" },
|
||||||
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.34.2" },
|
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.34.2" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata.requires-dev]
|
|
||||||
dev = [{ name = "sqlalchemy-stubs", specifier = ">=0.4" }]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shellingham"
|
name = "shellingham"
|
||||||
version = "1.5.4"
|
version = "1.5.4"
|
||||||
@@ -932,19 +892,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/1c/fc/9ba22f01b5cdacc8f5ed0d22304718d2c758fce3fd49a5372b886a86f37c/sqlalchemy-2.0.41-py3-none-any.whl", hash = "sha256:57df5dc6fdb5ed1a88a1ed2195fd31927e705cad62dedd86b46972752a80f576", size = 1911224, upload-time = "2025-05-14T17:39:42.154Z" },
|
{ url = "https://files.pythonhosted.org/packages/1c/fc/9ba22f01b5cdacc8f5ed0d22304718d2c758fce3fd49a5372b886a86f37c/sqlalchemy-2.0.41-py3-none-any.whl", hash = "sha256:57df5dc6fdb5ed1a88a1ed2195fd31927e705cad62dedd86b46972752a80f576", size = 1911224, upload-time = "2025-05-14T17:39:42.154Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "sqlalchemy-stubs"
|
|
||||||
version = "0.4"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "mypy" },
|
|
||||||
{ name = "typing-extensions" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/81/60/db082788267740b17eac2c00666bbea1c8c5a94b569e8b1ea76b0cf42d57/sqlalchemy-stubs-0.4.tar.gz", hash = "sha256:c665d6dd4482ef642f01027fa06c3d5e91befabb219dc71fc2a09e7d7695f7ae", size = 70682, upload-time = "2021-01-12T14:02:04.438Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/62/ae/cb215ab25b76228bc90c90444b87e323ffba58c212321a53d5bc92903098/sqlalchemy_stubs-0.4-py3-none-any.whl", hash = "sha256:5eec7aa110adf9b957b631799a72fef396b23ff99fe296df726645d01e312aa5", size = 116067, upload-time = "2021-01-12T14:02:02.723Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "starlette"
|
name = "starlette"
|
||||||
version = "0.46.2"
|
version = "0.46.2"
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
* */1 * * * /app/backup.sh
|
*/15 * * * * /app/backup.sh
|
||||||
|
|||||||
+3
-5
@@ -1,3 +1,5 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
db:
|
db:
|
||||||
image: postgres:15
|
image: postgres:15
|
||||||
@@ -35,11 +37,7 @@ services:
|
|||||||
- app-network
|
- app-network
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
build:
|
build: ./frontend
|
||||||
context: ./frontend
|
|
||||||
args:
|
|
||||||
VITE_API_URL: ${API_URL}
|
|
||||||
|
|
||||||
ports:
|
ports:
|
||||||
- "3000:80"
|
- "3000:80"
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
+10
-13
@@ -1,22 +1,19 @@
|
|||||||
FROM node:20 AS builder
|
# Build stage
|
||||||
|
FROM node:20 AS build
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package*.json ./
|
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
RUN npm install
|
RUN npm install
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
ARG VITE_API_URL
|
|
||||||
ENV VITE_API_URL=$VITE_API_URL
|
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# Stage 2: Serve with NGINX
|
# Production stage
|
||||||
FROM nginx:alpine
|
FROM nginx:alpine
|
||||||
|
|
||||||
# Clean out default config
|
COPY --from=build /app/dist /usr/share/nginx/html
|
||||||
RUN rm /etc/nginx/conf.d/default.conf
|
|
||||||
|
|
||||||
# Copy your custom nginx config
|
|
||||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
|
||||||
|
|
||||||
# Copy built files from Vite
|
|
||||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
|
||||||
|
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name localhost;
|
|
||||||
|
|
||||||
root /usr/share/nginx/html;
|
|
||||||
index index.html;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
try_files $uri /index.html;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Generated
+3
-653
@@ -8,23 +8,15 @@
|
|||||||
"name": "blogs-app",
|
"name": "blogs-app",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/free-brands-svg-icons": "^6.7.2",
|
|
||||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
|
||||||
"@tailwindcss/vite": "^4.1.7",
|
"@tailwindcss/vite": "^4.1.7",
|
||||||
"fs": "^0.0.1-security",
|
"fs": "^0.0.1-security",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-hook-form": "^7.57.0",
|
|
||||||
"react-icons": "^5.5.0",
|
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"react-router-dom": "^7.6.0",
|
"react-router-dom": "^7.6.0"
|
||||||
"rehype-raw": "^7.0.0",
|
|
||||||
"remark-gfm": "^4.0.1",
|
|
||||||
"remark-rehype": "^11.1.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.25.0",
|
"@eslint/js": "^9.25.0",
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
|
||||||
"@types/react": "^19.1.2",
|
"@types/react": "^19.1.2",
|
||||||
"@types/react-dom": "^19.1.2",
|
"@types/react-dom": "^19.1.2",
|
||||||
"@vitejs/plugin-react": "^4.4.1",
|
"@vitejs/plugin-react": "^4.4.1",
|
||||||
@@ -33,6 +25,7 @@
|
|||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.19",
|
"eslint-plugin-react-refresh": "^0.4.19",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
|
"tailwindcss": "^4.1.7",
|
||||||
"typescript": "~5.8.3",
|
"typescript": "~5.8.3",
|
||||||
"typescript-eslint": "^8.30.1",
|
"typescript-eslint": "^8.30.1",
|
||||||
"vite": "^6.3.5"
|
"vite": "^6.3.5"
|
||||||
@@ -887,53 +880,6 @@
|
|||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@fortawesome/fontawesome-common-types": {
|
|
||||||
"version": "6.7.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz",
|
|
||||||
"integrity": "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@fortawesome/fontawesome-svg-core": {
|
|
||||||
"version": "6.7.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz",
|
|
||||||
"integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@fortawesome/fontawesome-common-types": "6.7.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@fortawesome/free-brands-svg-icons": {
|
|
||||||
"version": "6.7.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.7.2.tgz",
|
|
||||||
"integrity": "sha512-zu0evbcRTgjKfrr77/2XX+bU+kuGfjm0LbajJHVIgBWNIDzrhpRxiCPNT8DW5AdmSsq7Mcf9D1bH0aSeSUSM+Q==",
|
|
||||||
"license": "(CC-BY-4.0 AND MIT)",
|
|
||||||
"dependencies": {
|
|
||||||
"@fortawesome/fontawesome-common-types": "6.7.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@fortawesome/react-fontawesome": {
|
|
||||||
"version": "0.2.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.2.tgz",
|
|
||||||
"integrity": "sha512-EnkrprPNqI6SXJl//m29hpaNzOp1bruISWaOiRtkMi/xSvHJlzc2j2JAYS7egxt/EbjSNV/k6Xy0AQI6vB2+1g==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"prop-types": "^15.8.1"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@fortawesome/fontawesome-svg-core": "~1 || ~6",
|
|
||||||
"react": ">=16.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@humanfs/core": {
|
"node_modules/@humanfs/core": {
|
||||||
"version": "0.19.1",
|
"version": "0.19.1",
|
||||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||||
@@ -1613,22 +1559,6 @@
|
|||||||
"node": ">= 10"
|
"node": ">= 10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/typography": {
|
|
||||||
"version": "0.5.16",
|
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz",
|
|
||||||
"integrity": "sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"lodash.castarray": "^4.4.0",
|
|
||||||
"lodash.isplainobject": "^4.0.6",
|
|
||||||
"lodash.merge": "^4.6.2",
|
|
||||||
"postcss-selector-parser": "6.0.10"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@tailwindcss/vite": {
|
"node_modules/@tailwindcss/vite": {
|
||||||
"version": "4.1.7",
|
"version": "4.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.7.tgz",
|
||||||
@@ -2374,19 +2304,6 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/cssesc": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
|
||||||
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"bin": {
|
|
||||||
"cssesc": "bin/cssesc"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/csstype": {
|
"node_modules/csstype": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
@@ -2481,18 +2398,6 @@
|
|||||||
"node": ">=10.13.0"
|
"node": ">=10.13.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/entities": {
|
|
||||||
"version": "6.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
|
|
||||||
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
|
|
||||||
"license": "BSD-2-Clause",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.12"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.25.4",
|
"version": "0.25.4",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz",
|
||||||
@@ -2968,64 +2873,6 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/hast-util-from-parse5": {
|
|
||||||
"version": "8.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz",
|
|
||||||
"integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/hast": "^3.0.0",
|
|
||||||
"@types/unist": "^3.0.0",
|
|
||||||
"devlop": "^1.0.0",
|
|
||||||
"hastscript": "^9.0.0",
|
|
||||||
"property-information": "^7.0.0",
|
|
||||||
"vfile": "^6.0.0",
|
|
||||||
"vfile-location": "^5.0.0",
|
|
||||||
"web-namespaces": "^2.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/unified"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/hast-util-parse-selector": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/hast": "^3.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/unified"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/hast-util-raw": {
|
|
||||||
"version": "9.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz",
|
|
||||||
"integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/hast": "^3.0.0",
|
|
||||||
"@types/unist": "^3.0.0",
|
|
||||||
"@ungap/structured-clone": "^1.0.0",
|
|
||||||
"hast-util-from-parse5": "^8.0.0",
|
|
||||||
"hast-util-to-parse5": "^8.0.0",
|
|
||||||
"html-void-elements": "^3.0.0",
|
|
||||||
"mdast-util-to-hast": "^13.0.0",
|
|
||||||
"parse5": "^7.0.0",
|
|
||||||
"unist-util-position": "^5.0.0",
|
|
||||||
"unist-util-visit": "^5.0.0",
|
|
||||||
"vfile": "^6.0.0",
|
|
||||||
"web-namespaces": "^2.0.0",
|
|
||||||
"zwitch": "^2.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/unified"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/hast-util-to-jsx-runtime": {
|
"node_modules/hast-util-to-jsx-runtime": {
|
||||||
"version": "2.3.6",
|
"version": "2.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
|
||||||
@@ -3053,35 +2900,6 @@
|
|||||||
"url": "https://opencollective.com/unified"
|
"url": "https://opencollective.com/unified"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/hast-util-to-parse5": {
|
|
||||||
"version": "8.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz",
|
|
||||||
"integrity": "sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/hast": "^3.0.0",
|
|
||||||
"comma-separated-tokens": "^2.0.0",
|
|
||||||
"devlop": "^1.0.0",
|
|
||||||
"property-information": "^6.0.0",
|
|
||||||
"space-separated-tokens": "^2.0.0",
|
|
||||||
"web-namespaces": "^2.0.0",
|
|
||||||
"zwitch": "^2.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/unified"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/hast-util-to-parse5/node_modules/property-information": {
|
|
||||||
"version": "6.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz",
|
|
||||||
"integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==",
|
|
||||||
"license": "MIT",
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/wooorm"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/hast-util-whitespace": {
|
"node_modules/hast-util-whitespace": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
|
||||||
@@ -3095,23 +2913,6 @@
|
|||||||
"url": "https://opencollective.com/unified"
|
"url": "https://opencollective.com/unified"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/hastscript": {
|
|
||||||
"version": "9.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz",
|
|
||||||
"integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/hast": "^3.0.0",
|
|
||||||
"comma-separated-tokens": "^2.0.0",
|
|
||||||
"hast-util-parse-selector": "^4.0.0",
|
|
||||||
"property-information": "^7.0.0",
|
|
||||||
"space-separated-tokens": "^2.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/unified"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/html-url-attributes": {
|
"node_modules/html-url-attributes": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
|
||||||
@@ -3122,16 +2923,6 @@
|
|||||||
"url": "https://opencollective.com/unified"
|
"url": "https://opencollective.com/unified"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/html-void-elements": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz",
|
|
||||||
"integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/wooorm"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ignore": {
|
"node_modules/ignore": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||||
@@ -3284,6 +3075,7 @@
|
|||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/js-yaml": {
|
"node_modules/js-yaml": {
|
||||||
@@ -3614,20 +3406,6 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lodash.castarray": {
|
|
||||||
"version": "4.4.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz",
|
|
||||||
"integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/lodash.isplainobject": {
|
|
||||||
"version": "4.0.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
|
||||||
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/lodash.merge": {
|
"node_modules/lodash.merge": {
|
||||||
"version": "4.6.2",
|
"version": "4.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||||
@@ -3645,18 +3423,6 @@
|
|||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/loose-envify": {
|
|
||||||
"version": "1.4.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
|
||||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"loose-envify": "cli.js"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/lru-cache": {
|
"node_modules/lru-cache": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||||
@@ -3676,44 +3442,6 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/markdown-table": {
|
|
||||||
"version": "3.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz",
|
|
||||||
"integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/wooorm"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/mdast-util-find-and-replace": {
|
|
||||||
"version": "3.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz",
|
|
||||||
"integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/mdast": "^4.0.0",
|
|
||||||
"escape-string-regexp": "^5.0.0",
|
|
||||||
"unist-util-is": "^6.0.0",
|
|
||||||
"unist-util-visit-parents": "^6.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/unified"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": {
|
|
||||||
"version": "5.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
|
|
||||||
"integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/mdast-util-from-markdown": {
|
"node_modules/mdast-util-from-markdown": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz",
|
||||||
@@ -3738,107 +3466,6 @@
|
|||||||
"url": "https://opencollective.com/unified"
|
"url": "https://opencollective.com/unified"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mdast-util-gfm": {
|
|
||||||
"version": "3.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz",
|
|
||||||
"integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"mdast-util-from-markdown": "^2.0.0",
|
|
||||||
"mdast-util-gfm-autolink-literal": "^2.0.0",
|
|
||||||
"mdast-util-gfm-footnote": "^2.0.0",
|
|
||||||
"mdast-util-gfm-strikethrough": "^2.0.0",
|
|
||||||
"mdast-util-gfm-table": "^2.0.0",
|
|
||||||
"mdast-util-gfm-task-list-item": "^2.0.0",
|
|
||||||
"mdast-util-to-markdown": "^2.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/unified"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/mdast-util-gfm-autolink-literal": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/mdast": "^4.0.0",
|
|
||||||
"ccount": "^2.0.0",
|
|
||||||
"devlop": "^1.0.0",
|
|
||||||
"mdast-util-find-and-replace": "^3.0.0",
|
|
||||||
"micromark-util-character": "^2.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/unified"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/mdast-util-gfm-footnote": {
|
|
||||||
"version": "2.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz",
|
|
||||||
"integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/mdast": "^4.0.0",
|
|
||||||
"devlop": "^1.1.0",
|
|
||||||
"mdast-util-from-markdown": "^2.0.0",
|
|
||||||
"mdast-util-to-markdown": "^2.0.0",
|
|
||||||
"micromark-util-normalize-identifier": "^2.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/unified"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/mdast-util-gfm-strikethrough": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/mdast": "^4.0.0",
|
|
||||||
"mdast-util-from-markdown": "^2.0.0",
|
|
||||||
"mdast-util-to-markdown": "^2.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/unified"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/mdast-util-gfm-table": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/mdast": "^4.0.0",
|
|
||||||
"devlop": "^1.0.0",
|
|
||||||
"markdown-table": "^3.0.0",
|
|
||||||
"mdast-util-from-markdown": "^2.0.0",
|
|
||||||
"mdast-util-to-markdown": "^2.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/unified"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/mdast-util-gfm-task-list-item": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/mdast": "^4.0.0",
|
|
||||||
"devlop": "^1.0.0",
|
|
||||||
"mdast-util-from-markdown": "^2.0.0",
|
|
||||||
"mdast-util-to-markdown": "^2.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/unified"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/mdast-util-mdx-expression": {
|
"node_modules/mdast-util-mdx-expression": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz",
|
||||||
@@ -4047,127 +3674,6 @@
|
|||||||
"micromark-util-types": "^2.0.0"
|
"micromark-util-types": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/micromark-extension-gfm": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz",
|
|
||||||
"integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"micromark-extension-gfm-autolink-literal": "^2.0.0",
|
|
||||||
"micromark-extension-gfm-footnote": "^2.0.0",
|
|
||||||
"micromark-extension-gfm-strikethrough": "^2.0.0",
|
|
||||||
"micromark-extension-gfm-table": "^2.0.0",
|
|
||||||
"micromark-extension-gfm-tagfilter": "^2.0.0",
|
|
||||||
"micromark-extension-gfm-task-list-item": "^2.0.0",
|
|
||||||
"micromark-util-combine-extensions": "^2.0.0",
|
|
||||||
"micromark-util-types": "^2.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/unified"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/micromark-extension-gfm-autolink-literal": {
|
|
||||||
"version": "2.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz",
|
|
||||||
"integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"micromark-util-character": "^2.0.0",
|
|
||||||
"micromark-util-sanitize-uri": "^2.0.0",
|
|
||||||
"micromark-util-symbol": "^2.0.0",
|
|
||||||
"micromark-util-types": "^2.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/unified"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/micromark-extension-gfm-footnote": {
|
|
||||||
"version": "2.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz",
|
|
||||||
"integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"devlop": "^1.0.0",
|
|
||||||
"micromark-core-commonmark": "^2.0.0",
|
|
||||||
"micromark-factory-space": "^2.0.0",
|
|
||||||
"micromark-util-character": "^2.0.0",
|
|
||||||
"micromark-util-normalize-identifier": "^2.0.0",
|
|
||||||
"micromark-util-sanitize-uri": "^2.0.0",
|
|
||||||
"micromark-util-symbol": "^2.0.0",
|
|
||||||
"micromark-util-types": "^2.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/unified"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/micromark-extension-gfm-strikethrough": {
|
|
||||||
"version": "2.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz",
|
|
||||||
"integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"devlop": "^1.0.0",
|
|
||||||
"micromark-util-chunked": "^2.0.0",
|
|
||||||
"micromark-util-classify-character": "^2.0.0",
|
|
||||||
"micromark-util-resolve-all": "^2.0.0",
|
|
||||||
"micromark-util-symbol": "^2.0.0",
|
|
||||||
"micromark-util-types": "^2.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/unified"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/micromark-extension-gfm-table": {
|
|
||||||
"version": "2.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz",
|
|
||||||
"integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"devlop": "^1.0.0",
|
|
||||||
"micromark-factory-space": "^2.0.0",
|
|
||||||
"micromark-util-character": "^2.0.0",
|
|
||||||
"micromark-util-symbol": "^2.0.0",
|
|
||||||
"micromark-util-types": "^2.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/unified"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/micromark-extension-gfm-tagfilter": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"micromark-util-types": "^2.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/unified"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/micromark-extension-gfm-task-list-item": {
|
|
||||||
"version": "2.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz",
|
|
||||||
"integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"devlop": "^1.0.0",
|
|
||||||
"micromark-factory-space": "^2.0.0",
|
|
||||||
"micromark-util-character": "^2.0.0",
|
|
||||||
"micromark-util-symbol": "^2.0.0",
|
|
||||||
"micromark-util-types": "^2.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/unified"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/micromark-factory-destination": {
|
"node_modules/micromark-factory-destination": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz",
|
||||||
@@ -4652,15 +4158,6 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/object-assign": {
|
|
||||||
"version": "4.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
|
||||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/optionator": {
|
"node_modules/optionator": {
|
||||||
"version": "0.9.4",
|
"version": "0.9.4",
|
||||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||||
@@ -4749,18 +4246,6 @@
|
|||||||
"integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
|
"integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/parse5": {
|
|
||||||
"version": "7.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
|
|
||||||
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"entities": "^6.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/path-exists": {
|
"node_modules/path-exists": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||||
@@ -4828,20 +4313,6 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss-selector-parser": {
|
|
||||||
"version": "6.0.10",
|
|
||||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
|
|
||||||
"integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"cssesc": "^3.0.0",
|
|
||||||
"util-deprecate": "^1.0.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/postcss-value-parser": {
|
"node_modules/postcss-value-parser": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
||||||
@@ -4859,17 +4330,6 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prop-types": {
|
|
||||||
"version": "15.8.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
|
||||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"loose-envify": "^1.4.0",
|
|
||||||
"object-assign": "^4.1.1",
|
|
||||||
"react-is": "^16.13.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/property-information": {
|
"node_modules/property-information": {
|
||||||
"version": "7.1.0",
|
"version": "7.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
|
||||||
@@ -4932,37 +4392,6 @@
|
|||||||
"react": "^19.1.0"
|
"react": "^19.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-hook-form": {
|
|
||||||
"version": "7.57.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.57.0.tgz",
|
|
||||||
"integrity": "sha512-RbEks3+cbvTP84l/VXGUZ+JMrKOS8ykQCRYdm5aYsxnDquL0vspsyNhGRO7pcH6hsZqWlPOjLye7rJqdtdAmlg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/react-hook-form"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "^16.8.0 || ^17 || ^18 || ^19"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-icons": {
|
|
||||||
"version": "5.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz",
|
|
||||||
"integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-is": {
|
|
||||||
"version": "16.13.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
|
||||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/react-markdown": {
|
"node_modules/react-markdown": {
|
||||||
"version": "10.1.0",
|
"version": "10.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz",
|
||||||
@@ -5038,39 +4467,6 @@
|
|||||||
"react-dom": ">=18"
|
"react-dom": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rehype-raw": {
|
|
||||||
"version": "7.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz",
|
|
||||||
"integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/hast": "^3.0.0",
|
|
||||||
"hast-util-raw": "^9.0.0",
|
|
||||||
"vfile": "^6.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/unified"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/remark-gfm": {
|
|
||||||
"version": "4.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz",
|
|
||||||
"integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/mdast": "^4.0.0",
|
|
||||||
"mdast-util-gfm": "^3.0.0",
|
|
||||||
"micromark-extension-gfm": "^3.0.0",
|
|
||||||
"remark-parse": "^11.0.0",
|
|
||||||
"remark-stringify": "^11.0.0",
|
|
||||||
"unified": "^11.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/unified"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/remark-parse": {
|
"node_modules/remark-parse": {
|
||||||
"version": "11.0.0",
|
"version": "11.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
|
||||||
@@ -5104,21 +4500,6 @@
|
|||||||
"url": "https://opencollective.com/unified"
|
"url": "https://opencollective.com/unified"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/remark-stringify": {
|
|
||||||
"version": "11.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz",
|
|
||||||
"integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/mdast": "^4.0.0",
|
|
||||||
"mdast-util-to-markdown": "^2.0.0",
|
|
||||||
"unified": "^11.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/unified"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/resolve-from": {
|
"node_modules/resolve-from": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||||
@@ -5632,13 +5013,6 @@
|
|||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/util-deprecate": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
|
||||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/vfile": {
|
"node_modules/vfile": {
|
||||||
"version": "6.0.3",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
|
||||||
@@ -5653,20 +5027,6 @@
|
|||||||
"url": "https://opencollective.com/unified"
|
"url": "https://opencollective.com/unified"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vfile-location": {
|
|
||||||
"version": "5.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz",
|
|
||||||
"integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/unist": "^3.0.0",
|
|
||||||
"vfile": "^6.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/unified"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/vfile-message": {
|
"node_modules/vfile-message": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz",
|
||||||
@@ -5781,16 +5141,6 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/web-namespaces": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/wooorm"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
|
|||||||
@@ -10,23 +10,15 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/free-brands-svg-icons": "^6.7.2",
|
|
||||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
|
||||||
"@tailwindcss/vite": "^4.1.7",
|
"@tailwindcss/vite": "^4.1.7",
|
||||||
"fs": "^0.0.1-security",
|
"fs": "^0.0.1-security",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-hook-form": "^7.57.0",
|
|
||||||
"react-icons": "^5.5.0",
|
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"react-router-dom": "^7.6.0",
|
"react-router-dom": "^7.6.0"
|
||||||
"rehype-raw": "^7.0.0",
|
|
||||||
"remark-gfm": "^4.0.1",
|
|
||||||
"remark-rehype": "^11.1.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.25.0",
|
"@eslint/js": "^9.25.0",
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
|
||||||
"@types/react": "^19.1.2",
|
"@types/react": "^19.1.2",
|
||||||
"@types/react-dom": "^19.1.2",
|
"@types/react-dom": "^19.1.2",
|
||||||
"@vitejs/plugin-react": "^4.4.1",
|
"@vitejs/plugin-react": "^4.4.1",
|
||||||
@@ -35,6 +27,7 @@
|
|||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.19",
|
"eslint-plugin-react-refresh": "^0.4.19",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
|
"tailwindcss": "^4.1.7",
|
||||||
"typescript": "~5.8.3",
|
"typescript": "~5.8.3",
|
||||||
"typescript-eslint": "^8.30.1",
|
"typescript-eslint": "^8.30.1",
|
||||||
"vite": "^6.3.5"
|
"vite": "^6.3.5"
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 187 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 175 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 340 KiB |
@@ -0,0 +1,42 @@
|
|||||||
|
#root {
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
height: 6em;
|
||||||
|
padding: 1.5em;
|
||||||
|
will-change: filter;
|
||||||
|
transition: filter 300ms;
|
||||||
|
}
|
||||||
|
.logo:hover {
|
||||||
|
filter: drop-shadow(0 0 2em #646cffaa);
|
||||||
|
}
|
||||||
|
.logo.react:hover {
|
||||||
|
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes logo-spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
a:nth-of-type(2) .logo {
|
||||||
|
animation: logo-spin infinite 20s linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
padding: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.read-the-docs {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
+43
-643
@@ -1,290 +1,31 @@
|
|||||||
import React, {
|
|
||||||
createContext,
|
|
||||||
useContext,
|
|
||||||
useState,
|
|
||||||
useEffect,
|
|
||||||
useRef,
|
|
||||||
} from "react";
|
|
||||||
import cruiseImg from "./images/cruise.jpg";
|
|
||||||
import discGolfImg from "./images/disc_golf_ace.jpg";
|
|
||||||
import odinImg from "./images/odin.jpg";
|
|
||||||
import {
|
import {
|
||||||
BrowserRouter as Router,
|
BrowserRouter as Router,
|
||||||
Routes,
|
Routes,
|
||||||
Route,
|
Route,
|
||||||
Link,
|
Link,
|
||||||
useNavigate,
|
useParams,
|
||||||
} from "react-router-dom";
|
} from "react-router-dom";
|
||||||
import { useForm } from "react-hook-form";
|
import { useState, useEffect } from "react";
|
||||||
|
import ReactMarkdown from "react-markdown";
|
||||||
import { BlogViewer } from "./utils/BlogViewer";
|
import { BlogViewer } from "./utils/BlogViewer";
|
||||||
|
import { BlogList } from "./utils/BlogList";
|
||||||
import { RequireAdmin } from "./utils/RouteGuard";
|
import { RequireAdmin } from "./utils/RouteGuard";
|
||||||
import { AdminPage } from "./utils/AdminPage";
|
import { AdminPage } from "./utils/AdminPage";
|
||||||
import { EditBlog } from "./utils/EditBlog";
|
|
||||||
import Unauthorized from "./utils/UnauthorizedPage";
|
import Unauthorized from "./utils/UnauthorizedPage";
|
||||||
import {
|
|
||||||
FaGithub,
|
|
||||||
FaGitAlt,
|
|
||||||
FaLinkedin,
|
|
||||||
FaTwitter,
|
|
||||||
FaSun,
|
|
||||||
FaMoon,
|
|
||||||
} from "react-icons/fa";
|
|
||||||
import type { Blog } from "./utils/types";
|
|
||||||
import { countWords } from "./utils/countWords";
|
|
||||||
import { API_URL } from "./utils/constants";
|
|
||||||
|
|
||||||
// Auth Context
|
|
||||||
interface AuthContextProps {
|
|
||||||
isAuthenticated: boolean;
|
|
||||||
login: (token: string, user_id: number) => void;
|
|
||||||
logout: () => void;
|
|
||||||
}
|
|
||||||
const AuthContext = createContext<AuthContextProps>({
|
|
||||||
isAuthenticated: false,
|
|
||||||
login: () => {},
|
|
||||||
logout: () => {},
|
|
||||||
});
|
|
||||||
export const useAuth = () => useContext(AuthContext);
|
|
||||||
|
|
||||||
const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
|
|
||||||
children,
|
|
||||||
}) => {
|
|
||||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const token = localStorage.getItem("token");
|
|
||||||
setIsAuthenticated(!!token);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const login = (token: string, user_id: number) => {
|
|
||||||
localStorage.setItem("token", token);
|
|
||||||
localStorage.setItem("user_id", user_id.toString());
|
|
||||||
setIsAuthenticated(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const logout = () => {
|
|
||||||
localStorage.removeItem("token");
|
|
||||||
localStorage.removeItem("user_id");
|
|
||||||
setIsAuthenticated(false);
|
|
||||||
navigate("/signin");
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AuthContext.Provider value={{ isAuthenticated, login, logout }}>
|
|
||||||
{children}
|
|
||||||
</AuthContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
function BlogPage() {
|
|
||||||
const { isAuthenticated } = useAuth();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const [blogs, setBlogs] = useState<Blog[]>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetch(`${API_URL}/blogs`, {
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((res) => res.json())
|
|
||||||
.then((data: Blog[]) => setBlogs(data))
|
|
||||||
.catch((err) => console.error(err));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="flex justify-end mb-4">
|
|
||||||
<button
|
|
||||||
onClick={() =>
|
|
||||||
isAuthenticated ? navigate("/create") : navigate("/signin")
|
|
||||||
}
|
|
||||||
className="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700"
|
|
||||||
>
|
|
||||||
Create
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-4">
|
|
||||||
{blogs.map((blog) => (
|
|
||||||
<Link
|
|
||||||
key={blog.id}
|
|
||||||
to={`/blog/${blog.id}`}
|
|
||||||
className="block p-4 border rounded hover:bg-gray-100 dark:hover:bg-gray-800"
|
|
||||||
>
|
|
||||||
<h3 className="text-xl font-semibold">{blog.title}</h3>
|
|
||||||
<p className="italic text-gray-600 dark:text-gray-400">
|
|
||||||
{blog.description}
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-500">By {blog.author_name}</p>
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Page for creating a blog post
|
|
||||||
function CreateBlog() {
|
|
||||||
const [fileName, setFileName] = useState<string>("");
|
|
||||||
const [title, setTitle] = useState("");
|
|
||||||
const [description, setDescription] = useState("");
|
|
||||||
const [authorName, setAuthorName] = useState("");
|
|
||||||
const [content, setContent] = useState<string>("");
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const handleFile = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const file = e.target.files?.[0];
|
|
||||||
if (!file) return;
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = () => {
|
|
||||||
const text = reader.result as string;
|
|
||||||
setContent(text);
|
|
||||||
setFileName(file.name);
|
|
||||||
const baseName = file.name.replace(/\.[^.]+$/, "");
|
|
||||||
setTitle(baseName);
|
|
||||||
setAuthorName(authorName);
|
|
||||||
};
|
|
||||||
reader.readAsText(file);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const userId = Number(localStorage.getItem("user_id"));
|
|
||||||
try {
|
|
||||||
const body = {
|
|
||||||
title,
|
|
||||||
author_id: userId, // still from localStorage
|
|
||||||
author_name: authorName, // from the form field
|
|
||||||
description: description,
|
|
||||||
body: content,
|
|
||||||
created_at: new Date().toISOString(),
|
|
||||||
updated_at: new Date().toISOString(),
|
|
||||||
published_at: new Date().toISOString(),
|
|
||||||
word_count: countWords(content),
|
|
||||||
};
|
|
||||||
await fetch(`${API_URL}/blogs`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
});
|
|
||||||
navigate("/blog");
|
|
||||||
} catch (err: any) {
|
|
||||||
alert(err.message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="max-w-xl mx-auto py-10 space-y-6">
|
|
||||||
<h2 className="text-2xl font-bold">Create Post</h2>
|
|
||||||
<div className="flex space-x-4">
|
|
||||||
<label className="px-4 py-2 bg-gray-200 dark:bg-gray-700 rounded cursor-pointer hover:bg-gray-300 dark:hover:bg-gray-600">
|
|
||||||
Upload File
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
accept=".txt,.md"
|
|
||||||
onChange={handleFile}
|
|
||||||
className="hidden"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
{fileName && (
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label htmlFor="title" className="block text-sm font-medium mb-1">
|
|
||||||
Title
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="title"
|
|
||||||
type="text"
|
|
||||||
value={title}
|
|
||||||
onChange={(e) => setTitle(e.target.value)}
|
|
||||||
className="w-full border border-gray-300 dark:border-gray-600 rounded px-3 py-2"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label htmlFor="author" className="block text-sm font-medium mb-1">
|
|
||||||
Author
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="author"
|
|
||||||
type="text"
|
|
||||||
value={authorName}
|
|
||||||
onChange={(e) => setAuthorName(e.target.value)}
|
|
||||||
className="w-full border border-gray-300 dark:border-gray-600 rounded px-3 py-2"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
htmlFor="description"
|
|
||||||
className="block text-sm font-medium mb-1"
|
|
||||||
>
|
|
||||||
Description
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="description"
|
|
||||||
type="text"
|
|
||||||
value={description}
|
|
||||||
onChange={(e) => setDescription(e.target.value)}
|
|
||||||
className="w-full border border-gray-300 dark:border-gray-600 rounded px-3 py-2"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label htmlFor="content" className="block text-sm font-medium mb-1">
|
|
||||||
Content
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
id="content"
|
|
||||||
rows={10}
|
|
||||||
value={content}
|
|
||||||
onChange={(e) => setContent(e.target.value)}
|
|
||||||
className="w-full border border-gray-300 dark:border-gray-600 rounded px-3 py-2 max-h-96 overflow-y-auto resize-none"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
|
|
||||||
>
|
|
||||||
Submit
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
// Initialize theme from localStorage or default to light
|
const [darkMode, setDarkMode] = useState(true);
|
||||||
const [darkMode, setDarkMode] = useState<boolean>(() => {
|
|
||||||
return localStorage.getItem("theme") === "dark";
|
|
||||||
});
|
|
||||||
|
|
||||||
// Sync dark mode state with <html> class and localStorage
|
|
||||||
useEffect(() => {
|
|
||||||
if (darkMode) {
|
|
||||||
document.documentElement.classList.add("dark");
|
|
||||||
localStorage.setItem("theme", "dark");
|
|
||||||
} else {
|
|
||||||
document.documentElement.classList.remove("dark");
|
|
||||||
localStorage.setItem("theme", "light");
|
|
||||||
}
|
|
||||||
}, [darkMode]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className={darkMode ? "dark" : ""}>
|
||||||
<Router>
|
<Router>
|
||||||
<AuthProvider>
|
<div className="min-h-screen bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100">
|
||||||
<div className="min-h-screen bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100 transition-colors duration-300">
|
<AppBar toggleDarkMode={() => setDarkMode(!darkMode)} />
|
||||||
<AppBar
|
|
||||||
darkMode={darkMode}
|
|
||||||
toggleDarkMode={() => setDarkMode((prev) => !prev)}
|
|
||||||
/>
|
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<LandingPage />} />
|
<Route path="/" element={<LandingPage />} />
|
||||||
<Route path="/blog" element={<BlogPage />} />
|
<Route path="/blog" element={<BlogList />} />
|
||||||
<Route path="/blog/:slug" element={<BlogViewer />} />
|
<Route path="/blog/:slug" element={<BlogViewer />} />
|
||||||
<Route path="/blog/:slug/edit" element={<EditBlog />} />
|
|
||||||
<Route path="/about" element={<About />} />
|
<Route path="/about" element={<About />} />
|
||||||
<Route path="/contact" element={<Contact />} />
|
<Route path="/contact" element={<Contact />} />
|
||||||
<Route
|
<Route
|
||||||
@@ -296,120 +37,48 @@ function App() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route path="/unauthorized" element={<Unauthorized />} />
|
<Route path="/unauthorized" element={<Unauthorized />} />
|
||||||
<Route path="/register" element={<Register />} />
|
|
||||||
<Route path="/signin" element={<SignIn />} />
|
|
||||||
<Route path="/profile" element={<Profile />} />
|
|
||||||
<Route path="/create" element={<CreateBlog />} />
|
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</AuthProvider>
|
|
||||||
</Router>
|
</Router>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AppBar({
|
function AppBar({ toggleDarkMode }: { toggleDarkMode: () => void }) {
|
||||||
darkMode,
|
|
||||||
toggleDarkMode,
|
|
||||||
}: {
|
|
||||||
darkMode: boolean;
|
|
||||||
toggleDarkMode: () => void;
|
|
||||||
}) {
|
|
||||||
const { isAuthenticated, logout } = useAuth();
|
|
||||||
const [menuOpen, setMenuOpen] = useState(false);
|
|
||||||
const menuRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
|
||||||
if (
|
|
||||||
menuOpen &&
|
|
||||||
menuRef.current &&
|
|
||||||
!menuRef.current.contains(event.target as Node)
|
|
||||||
) {
|
|
||||||
setMenuOpen(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
document.addEventListener("mousedown", handleClickOutside);
|
|
||||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
||||||
}, [menuOpen]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="bg-white dark:bg-gray-800 shadow p-4 flex items-center relative">
|
<nav className="bg-white dark:bg-gray-800 shadow p-4 flex space-x-4 items-center">
|
||||||
<Link to="/" className="font-bold text-blue-600 dark:text-blue-400">
|
<Link to="/" className="font-bold text-blue-600 dark:text-blue-400">
|
||||||
Home
|
Home
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
to="/blog"
|
to="/blog"
|
||||||
className="ml-4 text-gray-800 dark:text-gray-200 hover:text-blue-600 dark:hover:text-blue-400"
|
className="text-gray-800 dark:text-gray-200 hover:text-blue-600 dark:hover:text-blue-400"
|
||||||
>
|
>
|
||||||
Blog
|
Blog
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
to="/about"
|
to="/about"
|
||||||
className="ml-4 text-gray-800 dark:text-gray-200 hover:text-blue-600 dark:hover:text-blue-400"
|
className="text-gray-800 dark:text-gray-200 hover:text-blue-600 dark:hover:text-blue-400"
|
||||||
>
|
>
|
||||||
About Me
|
About Me
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
to="/contact"
|
to="/contact"
|
||||||
className="ml-4 text-gray-800 dark:text-gray-200 hover:text-blue-600 dark:hover:text-blue-400"
|
className="text-gray-800 dark:text-gray-200 hover:text-blue-600 dark:hover:text-blue-400"
|
||||||
>
|
>
|
||||||
Contact
|
Contact
|
||||||
</Link>
|
</Link>
|
||||||
<div className="ml-auto flex items-center space-x-4">
|
|
||||||
{!isAuthenticated ? (
|
|
||||||
<>
|
|
||||||
<Link
|
|
||||||
to="/register"
|
|
||||||
className="px-3 py-1 bg-blue-600 text-white rounded hover:bg-blue-700"
|
|
||||||
>
|
|
||||||
Register
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
to="/signin"
|
|
||||||
className="px-3 py-1 bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200 rounded hover:bg-gray-300 dark:hover:bg-gray-600"
|
|
||||||
>
|
|
||||||
Sign In
|
|
||||||
</Link>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="relative" ref={menuRef}>
|
|
||||||
<button
|
|
||||||
onClick={() => setMenuOpen(!menuOpen)}
|
|
||||||
className="w-8 h-8 rounded-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center"
|
|
||||||
>
|
|
||||||
<span className="text-gray-700 dark:text-gray-200">U</span>
|
|
||||||
</button>
|
|
||||||
{menuOpen && (
|
|
||||||
<div className="absolute right-0 mt-2 w-32 bg-white dark:bg-gray-700 shadow rounded">
|
|
||||||
<Link
|
|
||||||
to="/profile"
|
|
||||||
className="block px-4 py-2 text-gray-800 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-600"
|
|
||||||
>
|
|
||||||
Profile
|
|
||||||
</Link>
|
|
||||||
<button
|
|
||||||
onClick={logout}
|
|
||||||
className="w-full text-left block px-4 py-2 text-gray-800 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-600"
|
|
||||||
>
|
|
||||||
Sign Out
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<button
|
<button
|
||||||
onClick={toggleDarkMode}
|
onClick={toggleDarkMode}
|
||||||
className="text-xl p-1 bg-gray-200 dark:bg-gray-700 rounded-full"
|
className="ml-auto text-sm bg-gray-200 dark:bg-gray-700 px-2 py-1 rounded"
|
||||||
>
|
>
|
||||||
{darkMode ? <FaMoon /> : <FaSun />}
|
Toggle Dark Mode
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
</nav>
|
</nav>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pages and forms
|
|
||||||
function LandingPage() {
|
function LandingPage() {
|
||||||
return (
|
return (
|
||||||
<div className="text-center py-10">
|
<div className="text-center py-10">
|
||||||
@@ -421,310 +90,41 @@ function LandingPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const BlogPost = () => {
|
||||||
|
const { slug } = useParams<{ slug: string }>();
|
||||||
|
const [content, setContent] = useState<string>("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!slug) return;
|
||||||
|
|
||||||
|
fetch(`../public/blogs/${slug}.md`)
|
||||||
|
.then((res) => res.text())
|
||||||
|
.then((text) => setContent(text));
|
||||||
|
}, [slug]);
|
||||||
|
|
||||||
|
if (!content) return <div>Loading...</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article className="prose dark:prose-invert max-w-none p-4">
|
||||||
|
<ReactMarkdown>{content}</ReactMarkdown>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
function About() {
|
function About() {
|
||||||
return (
|
return (
|
||||||
<div className="py-10">
|
<div>
|
||||||
<div className="flex justify-center items-end space-x-4 flex-wrap">
|
<h2 className="text-2xl font-bold mb-4">About Me</h2>
|
||||||
{[cruiseImg, discGolfImg, odinImg].map((src, i) => (
|
<p>This is a placeholder for information about me.</p>
|
||||||
<img
|
|
||||||
key={i}
|
|
||||||
src={src}
|
|
||||||
alt={["Cruise", "Disc golf ace", "Odin"][i]}
|
|
||||||
className="
|
|
||||||
rounded-full
|
|
||||||
aspect-square
|
|
||||||
object-cover
|
|
||||||
w-1/3 max-w-[120px] /* small screens: 33% of container, up to 120px */
|
|
||||||
sm:w-1/4 sm:max-w-[150px] /* ≥640px: 25% up to 150px */
|
|
||||||
md:w-1/6 md:max-w-[200px] /* ≥768px: ~16% up to 200px */
|
|
||||||
lg:w-1/8 lg:max-w-[250px] /* ≥1024px: 12.5% up to 250px */
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="text-center mt-8 px-4">
|
|
||||||
<h2 className="text-3xl font-bold mb-4">About Me</h2>
|
|
||||||
<p className="text-lg text-gray-700 dark:text-gray-300 max-w-2xl mx-auto">
|
|
||||||
I’m Alex, a full-stack engineer at Whisker, working on backend one
|
|
||||||
minute, mobile tweaks the next, and firmware the day after. Learning
|
|
||||||
new tech is something I'm passionate about, and I’m all about
|
|
||||||
spreading that knowledge fast. Off-duty you’ll catch me reading a
|
|
||||||
bunch, writing (ironically, on my blog, which you are currently
|
|
||||||
viewing), exploring the outdoors with my dogs (and my girlfriend,
|
|
||||||
trust me she’s real!), or rolling dice as a D&D sorcerer. Here
|
|
||||||
you’ll find book thoughts, code experiments, and random shower
|
|
||||||
thoughts. Thanks for stopping by!
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="mt-8 flex flex-col items-center space-y-2">
|
|
||||||
<a
|
|
||||||
href="https://github.com/amuszyn"
|
|
||||||
className="flex items-center space-x-2 text-blue-600 hover:underline"
|
|
||||||
>
|
|
||||||
<FaGithub />
|
|
||||||
<span>github.com/amuszyn</span>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="https://gitea.muszyn.dev/"
|
|
||||||
className="flex items-center space-x-2 text-blue-600 hover:underline"
|
|
||||||
>
|
|
||||||
<FaGitAlt />
|
|
||||||
<span>gitea.muszyn.dev/</span>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="https://www.linkedin.com/in/almuszynski/"
|
|
||||||
className="flex items-center space-x-2 text-blue-600 hover:underline"
|
|
||||||
>
|
|
||||||
<FaLinkedin />
|
|
||||||
<span>linkedin.com/in/almuszynski</span>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="https://x.com/Muszynlol"
|
|
||||||
className="flex items-center space-x-2 text-blue-600 hover:underline"
|
|
||||||
>
|
|
||||||
<FaTwitter />
|
|
||||||
<span>x.com/Muszynlol</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Contact() {
|
function Contact() {
|
||||||
return (
|
return (
|
||||||
<div className="text-center py-10">
|
<div>
|
||||||
<h2 className="text-2xl font-bold mb-4">Contact</h2>
|
<h2 className="text-2xl font-bold mb-4">Contact</h2>
|
||||||
<p>Contact page content coming soon.</p>
|
<p>This is a placeholder for contact information or a form.</p>
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Profile() {
|
|
||||||
return (
|
|
||||||
<div className="text-center py-10">
|
|
||||||
<h2 className="text-2xl font-bold mb-4">Profile</h2>
|
|
||||||
<p>Profile page coming soon.</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Registration form with API integration
|
|
||||||
interface RegisterForm {
|
|
||||||
username: string;
|
|
||||||
email: string;
|
|
||||||
password: string;
|
|
||||||
confirmPassword: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function Register() {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
watch,
|
|
||||||
formState: { errors },
|
|
||||||
} = useForm<RegisterForm>();
|
|
||||||
|
|
||||||
const onSubmit = async (data: RegisterForm) => {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${API_URL}/register`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
username: data.username,
|
|
||||||
email: data.email,
|
|
||||||
password: data.password,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
const err = await response.json();
|
|
||||||
throw new Error(err.detail || "Registration failed");
|
|
||||||
}
|
|
||||||
navigate("/signin");
|
|
||||||
} catch (err: any) {
|
|
||||||
alert(err.message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="max-w-md mx-auto py-10">
|
|
||||||
<h2 className="text-2xl font-bold mb-6 text-center">Register</h2>
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label htmlFor="username" className="block text-sm font-medium mb-1">
|
|
||||||
Username
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="username"
|
|
||||||
{...register("username", { required: true })}
|
|
||||||
className="w-full border border-gray-300 dark:border-gray-600 rounded px-3 py-2"
|
|
||||||
/>
|
|
||||||
{errors.username && (
|
|
||||||
<span className="text-red-600 text-sm">Username is required</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label htmlFor="email" className="block text-sm font-medium mb-1">
|
|
||||||
Email
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="email"
|
|
||||||
type="email"
|
|
||||||
{...register("email", {
|
|
||||||
required: true,
|
|
||||||
pattern: /^\S+@\S+\.\S+$/,
|
|
||||||
})}
|
|
||||||
className="w-full border border-gray-300 dark:border-gray-600 rounded px-3 py-2"
|
|
||||||
/>
|
|
||||||
{errors.email && (
|
|
||||||
<span className="text-red-600 text-sm">
|
|
||||||
Valid email is required
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label htmlFor="password" className="block text-sm font-medium mb-1">
|
|
||||||
Password
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="password"
|
|
||||||
type="password"
|
|
||||||
{...register("password", {
|
|
||||||
required: true,
|
|
||||||
pattern: /(?=.*[A-Z])(?=.*\d).+/,
|
|
||||||
})}
|
|
||||||
className="w-full border border-gray-300 dark:border-gray-600 rounded px-3 py-2"
|
|
||||||
/>
|
|
||||||
{errors.password && (
|
|
||||||
<span className="text-red-600 text-sm">
|
|
||||||
Password must include uppercase and number
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
htmlFor="confirmPassword"
|
|
||||||
className="block text-sm font-medium mb-1"
|
|
||||||
>
|
|
||||||
Confirm Password
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="confirmPassword"
|
|
||||||
type="password"
|
|
||||||
{...register("confirmPassword", {
|
|
||||||
validate: (value) =>
|
|
||||||
value === watch("password") || "Passwords do not match",
|
|
||||||
})}
|
|
||||||
className="w-full border border-gray-300 dark:border-gray-600 rounded px-3 py-2"
|
|
||||||
/>
|
|
||||||
{errors.confirmPassword && (
|
|
||||||
<span className="text-red-600 text-sm">
|
|
||||||
{errors.confirmPassword.message}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="w-full bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
|
|
||||||
>
|
|
||||||
Register
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sign-in form with API integration
|
|
||||||
interface SignInForm {
|
|
||||||
identifier: string;
|
|
||||||
password: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function SignIn() {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
formState: { errors },
|
|
||||||
} = useForm<SignInForm>();
|
|
||||||
const { login } = useAuth();
|
|
||||||
|
|
||||||
const onSubmit = async (data: SignInForm) => {
|
|
||||||
try {
|
|
||||||
const body = new URLSearchParams();
|
|
||||||
body.append("username", data.identifier);
|
|
||||||
body.append("password", data.password);
|
|
||||||
|
|
||||||
const response = await fetch(`${API_URL}/login`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
||||||
body: body.toString(),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const err = await response.json();
|
|
||||||
throw new Error(err.detail || "Login failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
const tokenData = await response.json();
|
|
||||||
alert(response);
|
|
||||||
login(tokenData.access_token, tokenData.user_id);
|
|
||||||
navigate("/");
|
|
||||||
} catch (err: any) {
|
|
||||||
alert(err.message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="max-w-md mx-auto py-10">
|
|
||||||
<h2 className="text-2xl font-bold mb-6 text-center">Sign In</h2>
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
htmlFor="identifier"
|
|
||||||
className="block text-sm font-medium mb-1"
|
|
||||||
>
|
|
||||||
Username or Email
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="identifier"
|
|
||||||
{...register("identifier", { required: true })}
|
|
||||||
className="w-full border border-gray-300 dark:border-gray-600 rounded px-3 py-2"
|
|
||||||
/>
|
|
||||||
{errors.identifier && (
|
|
||||||
<span className="text-red-600 text-sm">This field is required</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
htmlFor="signinPassword"
|
|
||||||
className="block text-sm font-medium mb-1"
|
|
||||||
>
|
|
||||||
Password
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="signinPassword"
|
|
||||||
type="password"
|
|
||||||
{...register("password", { required: true })}
|
|
||||||
className="w-full border border-gray-300 dark:border-gray-600 rounded px-3 py-2"
|
|
||||||
/>
|
|
||||||
{errors.password && (
|
|
||||||
<span className="text-red-600 text-sm">Password is required</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="w-full bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
|
|
||||||
>
|
|
||||||
Sign In
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 187 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 175 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 340 KiB |
@@ -1,5 +1,10 @@
|
|||||||
import { createRoot } from "react-dom/client";
|
import { StrictMode } from 'react'
|
||||||
import "./index.css";
|
import { createRoot } from 'react-dom/client'
|
||||||
import App from "./App.tsx";
|
import './index.css'
|
||||||
|
import App from './App.tsx'
|
||||||
|
|
||||||
createRoot(document.getElementById("root")!).render(<App />);
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,169 +0,0 @@
|
|||||||
/* markdown.css - Custom wrapper for Markdown content */
|
|
||||||
|
|
||||||
.markdown-body {
|
|
||||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
||||||
font-size: 1rem;
|
|
||||||
line-height: 1.7;
|
|
||||||
color: #1e293b;
|
|
||||||
/* slate-800 */
|
|
||||||
background-color: #ffffff;
|
|
||||||
/* white */
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
||||||
padding: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark mode override */
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
.markdown-body {
|
|
||||||
color: #cbd5e1;
|
|
||||||
/* slate-300 */
|
|
||||||
background-color: #0f172a;
|
|
||||||
/* slate-900 */
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Headings */
|
|
||||||
.markdown-body h1,
|
|
||||||
.markdown-body h2,
|
|
||||||
.markdown-body h3,
|
|
||||||
.markdown-body h4 {
|
|
||||||
margin-top: 1.5em;
|
|
||||||
margin-bottom: 0.5em;
|
|
||||||
font-weight: 600;
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-body h1 {
|
|
||||||
font-size: 2.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-body h2 {
|
|
||||||
font-size: 1.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-body h3 {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-body h4 {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Paragraphs */
|
|
||||||
.markdown-body p {
|
|
||||||
margin: 1em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Links */
|
|
||||||
.markdown-body a {
|
|
||||||
color: #3b82f6;
|
|
||||||
/* blue-500 */
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-body a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
.markdown-body a {
|
|
||||||
color: #60a5fa;
|
|
||||||
/* blue-400 */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Lists */
|
|
||||||
.markdown-body ul,
|
|
||||||
.markdown-body ol {
|
|
||||||
margin: 1em 0 1em 1.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Blockquotes */
|
|
||||||
.markdown-body blockquote {
|
|
||||||
margin: 1em 0;
|
|
||||||
padding: 0.75em 1em;
|
|
||||||
border-left: 4px solid #e2e8f0;
|
|
||||||
/* slate-200 */
|
|
||||||
background-color: #f1f5f9;
|
|
||||||
/* slate-100 */
|
|
||||||
color: #475569;
|
|
||||||
/* slate-600 */
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
.markdown-body blockquote {
|
|
||||||
border-color: #334155;
|
|
||||||
/* slate-700 */
|
|
||||||
background-color: #1e293b;
|
|
||||||
/* slate-800 */
|
|
||||||
color: #cbd5e1;
|
|
||||||
/* slate-300 */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Inline code */
|
|
||||||
.markdown-body code {
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
color: #db2777;
|
|
||||||
/* rose-600 */
|
|
||||||
padding: 0.2em 0.4em;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
|
|
||||||
font-size: 0.95em;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
.markdown-body code {
|
|
||||||
background-color: #1e293b;
|
|
||||||
/* slate-800 */
|
|
||||||
color: #f472b6;
|
|
||||||
/* pink-400 */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Code blocks */
|
|
||||||
.markdown-body pre {
|
|
||||||
background-color: #0f172a;
|
|
||||||
/* slate-900 */
|
|
||||||
color: #e2e8f0;
|
|
||||||
/* slate-200 */
|
|
||||||
padding: 1em;
|
|
||||||
overflow: auto;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tables */
|
|
||||||
.markdown-body table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
margin: 1em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-body th,
|
|
||||||
.markdown-body td {
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
/* slate-200 */
|
|
||||||
padding: 0.75em 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-body th {
|
|
||||||
background-color: #f1f5f9;
|
|
||||||
/* slate-100 */
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
|
|
||||||
.markdown-body th,
|
|
||||||
.markdown-body td {
|
|
||||||
border-color: #334155;
|
|
||||||
/* slate-700 */
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-body th {
|
|
||||||
background-color: #334155;
|
|
||||||
/* slate-700 */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +1,9 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { API_URL } from "./constants";
|
|
||||||
|
|
||||||
export function BlogList() {
|
export function BlogList() {
|
||||||
const [content, setContent] = useState("");
|
const [content, setContent] = useState("");
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch(`${API_URL}/get-blogs`, {
|
fetch(`localhost:8000/get-blogs`)
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((res) => res.text())
|
.then((res) => res.text())
|
||||||
.then(setContent);
|
.then(setContent);
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
@@ -1,80 +1,15 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
|
||||||
import { API_URL } from "./constants";
|
|
||||||
import type { Blog } from "./types";
|
|
||||||
import Markdown from "react-markdown";
|
import Markdown from "react-markdown";
|
||||||
import remarkGfm from "remark-gfm";
|
import { useParams } from "react-router-dom";
|
||||||
import remarkRehype from "remark-rehype";
|
|
||||||
import rehypeRaw from "rehype-raw";
|
|
||||||
import "../styles/markdown.css";
|
|
||||||
|
|
||||||
export function BlogViewer() {
|
export function BlogViewer() {
|
||||||
const { slug } = useParams<{ slug: string }>();
|
const [content, setContent] = useState("");
|
||||||
const [blog, setBlog] = useState<Blog | null>(null);
|
const { slug } = useParams();
|
||||||
const navigate = useNavigate();
|
|
||||||
const me = localStorage.getItem("user_id");
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!slug) return;
|
fetch(`localhost:8000/get-blogs/${slug}`)
|
||||||
fetch(`${API_URL}/blogs/${slug}`, {
|
.then((res) => res.text())
|
||||||
method: "GET",
|
.then(setContent);
|
||||||
headers: {
|
}, []);
|
||||||
"Content-Type": "application/json",
|
return <Markdown>{content}</Markdown>;
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((res) => res.json())
|
|
||||||
.then((data: Blog) => setBlog(data))
|
|
||||||
.catch((err) => console.error(err));
|
|
||||||
}, [slug]);
|
|
||||||
|
|
||||||
if (!blog) return <p>Loading…</p>;
|
|
||||||
|
|
||||||
const isAuthor = me === String(blog.author_id);
|
|
||||||
|
|
||||||
const handleDelete = async () => {
|
|
||||||
if (!window.confirm(`Are you sure you want to delete ${blog.title}`))
|
|
||||||
return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await fetch(`${API_URL}/blogs/${slug}`, {
|
|
||||||
method: "DELETE",
|
|
||||||
headers: { Accept: "application/json" },
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error(await res.text());
|
|
||||||
navigate("/");
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Delete failed:", err);
|
|
||||||
alert("Could not delete post. See console for details.");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="max-w-2xl mx-auto py-10 space-y-6">
|
|
||||||
{isAuthor && (
|
|
||||||
<div className="flex space-x-4">
|
|
||||||
<button
|
|
||||||
onClick={() => navigate(`/blog/${slug}/edit`)}
|
|
||||||
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
|
|
||||||
>
|
|
||||||
Edit
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={handleDelete}
|
|
||||||
className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<h1 className="text-3xl font-bold">{blog.title.toUpperCase()}</h1>
|
|
||||||
<p className="text-sm text-gray-500">By {blog.author_name}</p>
|
|
||||||
<div className="markdown-body mx-auto p-4">
|
|
||||||
<Markdown
|
|
||||||
remarkPlugins={[remarkGfm, remarkRehype]}
|
|
||||||
rehypePlugins={[rehypeRaw]}
|
|
||||||
children={blog.body}
|
|
||||||
></Markdown>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,111 +0,0 @@
|
|||||||
import { useEffect, useState, type FormEvent } from "react";
|
|
||||||
|
|
||||||
import { useParams, useNavigate } from "react-router-dom";
|
|
||||||
import { API_URL } from "./constants";
|
|
||||||
import type { Blog } from "./types";
|
|
||||||
import { countWords } from "./countWords";
|
|
||||||
|
|
||||||
export function EditBlog() {
|
|
||||||
const { slug } = useParams<{ slug: string }>();
|
|
||||||
const [blog, setBlog] = useState<Blog | null>(null);
|
|
||||||
const [title, setTitle] = useState("");
|
|
||||||
const [description, setDescription] = useState("");
|
|
||||||
const [body, setBody] = useState("");
|
|
||||||
const [authorName, setAuthorName] = useState("");
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!slug) return;
|
|
||||||
fetch(`${API_URL}/blogs/${slug}`, {
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((res) => res.json())
|
|
||||||
.then((data: Blog) => {
|
|
||||||
setBlog(data);
|
|
||||||
setTitle(data.title);
|
|
||||||
setDescription(data.description);
|
|
||||||
setBody(data.body);
|
|
||||||
setAuthorName(data.author_name);
|
|
||||||
})
|
|
||||||
.catch(console.error);
|
|
||||||
}, [slug]);
|
|
||||||
|
|
||||||
if (!blog) return <p>Loading…</p>;
|
|
||||||
|
|
||||||
async function handleSubmit(e: FormEvent) {
|
|
||||||
e.preventDefault();
|
|
||||||
const updatedBlog = {
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
body,
|
|
||||||
author_name: authorName, // from the form field
|
|
||||||
updated_at: new Date().toISOString(),
|
|
||||||
word_count: countWords(body),
|
|
||||||
version: blog ? blog.version + 1 : 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
const res = await fetch(`${API_URL}/blogs/${slug}`, {
|
|
||||||
method: "PUT", // or "PATCH" if your API prefers
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify(updatedBlog),
|
|
||||||
});
|
|
||||||
if (res.ok) {
|
|
||||||
navigate(`/blogs/${slug}`); // go back to the viewer
|
|
||||||
} else {
|
|
||||||
console.error("Update failed:", await res.text());
|
|
||||||
alert("Could not update the post. See console for details.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="max-w-2xl mx-auto py-10">
|
|
||||||
<h2 className="text-2xl font-bold mb-4">Edit Blog Post</h2>
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label className="block font-medium">Title</label>
|
|
||||||
<input
|
|
||||||
value={title}
|
|
||||||
onChange={(e) => setTitle(e.target.value)}
|
|
||||||
className="w-full border px-2 py-1 rounded"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block font-medium">Description</label>
|
|
||||||
<textarea
|
|
||||||
value={description}
|
|
||||||
onChange={(e) => setDescription(e.target.value)}
|
|
||||||
className="w-full border px-2 py-1 rounded"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block font-medium">Author Name</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={authorName}
|
|
||||||
onChange={(e) => setAuthorName(e.target.value)}
|
|
||||||
className="w-full border px-2 py-1 rounded"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block font-medium">Body (Markdown)</label>
|
|
||||||
<textarea
|
|
||||||
value={body}
|
|
||||||
onChange={(e) => setBody(e.target.value)}
|
|
||||||
rows={10}
|
|
||||||
className="w-full border px-2 py-1 rounded font-mono"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700"
|
|
||||||
>
|
|
||||||
Save Changes
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export const API_URL = import.meta.env.VITE_API_URL || "http://localhost:8000";
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
export function countWords(text: string): number {
|
|
||||||
// Trim leading/trailing whitespace, then split on one-or-more whitespace characters
|
|
||||||
const words = text.trim().split(/\s+/);
|
|
||||||
// If the string was empty or only whitespace, split() returns [''], so handle that
|
|
||||||
if (words.length === 1 && words[0] === "") {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return words.length;
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
export interface Blog {
|
|
||||||
id: number;
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
body: string;
|
|
||||||
author_id: number;
|
|
||||||
author_name: string;
|
|
||||||
created_at: string;
|
|
||||||
updated_at: string;
|
|
||||||
published_at: string;
|
|
||||||
word_count: number;
|
|
||||||
version: number;
|
|
||||||
read_time: number;
|
|
||||||
language: string;
|
|
||||||
tags: string[];
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,16 @@
|
|||||||
|
|
||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
content: [
|
||||||
darkMode: "class",
|
"./index.html",
|
||||||
|
"./src/**/*.{js,ts,jsx,tsx}"
|
||||||
|
],
|
||||||
|
darkMode: 'class',
|
||||||
theme: {
|
theme: {
|
||||||
extend: {},
|
extend: {},
|
||||||
},
|
},
|
||||||
plugins: [require("@tailwindcss/typography"), require("@tailwindcss/vite")],
|
plugins: [
|
||||||
|
require('@tailwindcss/typography'),
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user