diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml
index 72582e7..3320ed2 100644
--- a/.github/workflows/ruff.yml
+++ b/.github/workflows/ruff.yml
@@ -1,28 +1,21 @@
-name: Ruff check
+name: ruff check
on:
- push:
- workflow_dispatch:
+ push:
+ pull_request:
jobs:
- test:
- runs-on: ubuntu-latest
- strategy:
- matrix:
- python-version: ["3.10", "3.11", "3.12", "3.13"]
+ ruff_check:
+ runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
+ steps:
+ - uses: actions/checkout@v6
+ - uses: actions/setup-python@v6
+ with:
+ python-version: "3.13"
- - name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v4
- with:
- python-version: ${{ matrix.python-version }}
+ - run: python -m venv .venv && source .venv/bin/activate
+ - run: python -m pip install --group dev
- - name: Install dependencies
- run: |
- pip install -r requirements.txt
- pip install -r requirements-dev.txt
-
- - name: Run ruff check
- run: ruff check .
+ - name: Run ruff check
+ run: ruff check --output-format=github .
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index b065d9d..47ef0c1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,7 +6,10 @@ __pycache__
node_modules
package-lock.json
.env
+.venv
jikan4snek_cache
config.xml
.ruff_cache
-config.toml
\ No newline at end of file
+config.toml
+*.egg-info
+package.json
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
deleted file mode 100644
index 78f39a4..0000000
--- a/.vscode/settings.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "xml.fileAssociations": [
- {
- "pattern": "config.xml",
- "systemId": "scheme.xsd"
- }
- ]
-}
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index 8d5db45..99bf66c 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,6 @@
-FROM python:3.11-slim-bookworm
+FROM python:3.13.12-slim-bookworm
+
+COPY --from=ghcr.io/astral-sh/uv:0.11.2 /uv /uvx /bin/
USER root
@@ -10,15 +12,18 @@ COPY /templates ./templates
COPY /markdown ./markdown
COPY input.css .
-COPY requirements.txt .
+COPY pyproject.toml .
COPY static_config.toml .
COPY tailwind.config.js .
RUN apt-get update && apt-get install -y git
-RUN pip install -r requirements.txt
+ENV UV_NO_DEV=1
+
+COPY uv.lock .
+RUN uv sync --locked
EXPOSE 8000
ENV LISTEN_PORT=8000
-CMD ["uvicorn", "app.main:app", "--host=0.0.0.0", "--proxy-headers"]
+CMD ["uv", "run", "uvicorn", "app.main:app", "--host=0.0.0.0", "--proxy-headers"]
\ No newline at end of file
diff --git a/Makefile b/Makefile
deleted file mode 100644
index 9647bd7..0000000
--- a/Makefile
+++ /dev/null
@@ -1,20 +0,0 @@
-build: install-deps
-
-PIP = pip
-
-install-deps:
- ${PIP} install -r requirements.txt
-
-PYTHON = python
-
-docker-build:
- ${PYTHON} scripts/docker_build.py
-
-docker-compose:
- docker compose up
-
-test:
- ruff check .
-
-clean:
- rm ./web/output.css
\ No newline at end of file
diff --git a/README.md b/README.md
index 7838a3e..3d9c932 100644
--- a/README.md
+++ b/README.md
@@ -10,9 +10,8 @@
## Prerequisites
- ~~[NodeJS](https://nodejs.org/en)~~ (no longer needed)
-- ~~[Python 3.8+](https://www.python.org/)~~ (Might still work on 3.8 but versions below 3.9 are now depreacted)
-- [Python 3.9+](https://www.python.org/)
-- [Make](https://www.gnu.org/software/make/) (Optional)
+- ~~[Python 3.8+](https://www.python.org/)~~ (is now constrained to one major python version)
+- [Python 3.13](https://www.python.org/)
## Run Locally
> [!WARNING]
@@ -50,4 +49,4 @@ cp config.template.toml config.toml
6. RUN!!!!
```sh
fastapi dev
-```
+```
\ No newline at end of file
diff --git a/app/__init__.py b/app/__init__.py
index 6eb3fcb..b182303 100644
--- a/app/__init__.py
+++ b/app/__init__.py
@@ -1 +1 @@
-__version__ = "1.3.17"
\ No newline at end of file
+__version__ = "1.3.30"
\ No newline at end of file
diff --git a/app/anime.py b/app/anime.py
deleted file mode 100644
index 4810bbf..0000000
--- a/app/anime.py
+++ /dev/null
@@ -1,92 +0,0 @@
-from __future__ import annotations
-from typing import TYPE_CHECKING
-
-if TYPE_CHECKING:
- from typing import Dict, Tuple, List
-
-from datetime import datetime, timedelta
-
-from .async_http_client import get_http_client
-
-__all__ = ("Anime",)
-
-class Anime():
- def __init__(self, username: str) -> None:
- self.username = username
-
- self.__history_cache: Tuple[float, Dict[str, str]] = (0, {})
- self.__updates_cache: Tuple[float, Dict[str, str]] = (0, {})
-
- self.cache_expire = timedelta(minutes = 5)
-
- async def get_anime_status(self) -> List[dict]:
- anime_list = []
-
- mal_history = await self.__get_history()
- mal_updates = await self.__get_updates()
-
- for anime_update in mal_updates["data"]["anime"]:
- action = ""
-
- status = anime_update["status"].lower()
-
- if status == "watching":
- continue
-
- elif status == "plan to watch":
- action = "Planned to watch"
-
- elif status == "completed":
- action = "Completed"
-
- anime_list.append(
- {
- "action": action,
- "url": anime_update["entry"]["url"],
- "title": anime_update["entry"]["title"],
- "date": datetime.fromisoformat(anime_update["date"]).strftime("%b %d %Y"),
- "date_timestamp": datetime.fromisoformat(anime_update["date"]).timestamp()
- }
- )
-
- anime_list += [
- {
- "action": f"Watched episode {anime['increment']} of",
- "url": anime["entry"]["url"],
- "title": anime["entry"]["name"],
- "date": datetime.fromisoformat(anime["date"]).strftime("%b %d %Y"),
- "date_timestamp": datetime.fromisoformat(anime["date"]).timestamp()
- } for anime in mal_history["data"][:20]
- ]
-
- anime_list.sort(key = lambda x: x["date_timestamp"], reverse = True)
-
- return anime_list
-
- async def __get_history(self) -> Dict[str, str]:
- current_timestamp = datetime.now().timestamp()
-
- if current_timestamp > self.__history_cache[0]:
- session = await get_http_client()
- r = await session.get(f"https://api.jikan.moe/v4/users/{self.username}/history")
- data = await r.json() if r.ok else {}
-
- self.__history_cache = (
- current_timestamp + self.cache_expire.seconds, data
- )
-
- return self.__history_cache[1]
-
- async def __get_updates(self) -> Dict[str, str]:
- current_timestamp = datetime.now().timestamp()
-
- if current_timestamp > self.__updates_cache[0]:
- session = await get_http_client()
- r = await session.get(f"https://api.jikan.moe/v4/users/{self.username}/userupdates")
- data = await r.json() if r.ok else {}
-
- self.__updates_cache = (
- current_timestamp + self.cache_expire.seconds, data
- )
-
- return self.__updates_cache[1]
\ No newline at end of file
diff --git a/app/anime_api.py b/app/anime_api.py
new file mode 100644
index 0000000..5576095
--- /dev/null
+++ b/app/anime_api.py
@@ -0,0 +1,102 @@
+import typing
+
+import logging
+from datetime import datetime, timedelta
+
+from .http_client import HTTPClient
+
+__all__ = ()
+
+logger = logging.getLogger(__name__)
+
+class AnimeAPI():
+ def __init__(self, username: str) -> None:
+ self.username = username
+
+ self.__history_cache: tuple[float, dict[str, str]] = (0, {})
+ self.__updates_cache: tuple[float, dict[str, str]] = (0, {})
+
+ self.cache_expire = timedelta(minutes = 5)
+
+ async def get_anime_status(self, http_client: HTTPClient) -> list[dict]:
+ anime_list = []
+
+ mal_history = await self.__get_history(http_client)
+ mal_updates = await self.__get_updates(http_client)
+
+ if not mal_updates == {}:
+
+ for anime_update in mal_updates["data"]["anime"]:
+ action = ""
+
+ status = anime_update["status"].lower()
+
+ if status == "watching":
+ continue
+
+ elif status == "plan to watch":
+ action = "Planned to watch"
+
+ elif status == "completed":
+ action = "Completed"
+
+ anime_list.append(
+ {
+ "action": action,
+ "url": anime_update["entry"]["url"],
+ "title": anime_update["entry"]["title"],
+ "date": datetime.fromisoformat(anime_update["date"]).strftime("%b %d %Y"),
+ "date_timestamp": datetime.fromisoformat(anime_update["date"]).timestamp()
+ }
+ )
+
+ if not mal_history == {}:
+ anime_list += [
+ {
+ "action": f"Watched episode {anime['increment']} of",
+ "url": anime["entry"]["url"],
+ "title": anime["entry"]["name"],
+ "date": datetime.fromisoformat(anime["date"]).strftime("%b %d %Y"),
+ "date_timestamp": datetime.fromisoformat(anime["date"]).timestamp()
+ } for anime in mal_history["data"][:20]
+ ]
+
+ anime_list.sort(key = lambda x: x["date_timestamp"], reverse = True)
+
+ return anime_list
+
+ async def __get_history(self, http_client: HTTPClient) -> dict[str, str]:
+ current_timestamp = datetime.now().timestamp()
+
+ if current_timestamp > self.__history_cache[0]:
+ http_session = await http_client.get_http_session()
+
+ async with http_session.get(f"https://api.jikan.moe/v4/users/{self.username}/history") as response:
+ if not response.ok:
+ logger.error(f"Received unsuccessful response from jikan! Response: {response}")
+
+ data = typing.cast(dict, val = await response.json() if response.ok else {})
+
+ self.__history_cache = (
+ current_timestamp + self.cache_expire.seconds, data
+ )
+
+ return self.__history_cache[1]
+
+ async def __get_updates(self, http_client: HTTPClient) -> dict[str, str]:
+ current_timestamp = datetime.now().timestamp()
+
+ if current_timestamp > self.__updates_cache[0]:
+ http_session = await http_client.get_http_session()
+
+ async with http_session.get(f"https://api.jikan.moe/v4/users/{self.username}/userupdates") as response:
+ if not response.ok:
+ logger.error(f"Received unsuccessful response from jikan! Response: {response}")
+
+ data = typing.cast(dict, val = await response.json() if response.ok else {})
+
+ self.__updates_cache = (
+ current_timestamp + self.cache_expire.seconds, data
+ )
+
+ return self.__updates_cache[1]
\ No newline at end of file
diff --git a/app/async_http_client.py b/app/async_http_client.py
deleted file mode 100644
index fc0f584..0000000
--- a/app/async_http_client.py
+++ /dev/null
@@ -1,13 +0,0 @@
-from aiohttp import ClientSession
-
-__all__ = ()
-
-__client_session = None
-
-async def get_http_client() -> ClientSession:
- global __client_session
-
- if __client_session is None:
- __client_session = ClientSession()
-
- return __client_session
\ No newline at end of file
diff --git a/app/goldy_exe_api.py b/app/blog_api.py
similarity index 60%
rename from app/goldy_exe_api.py
rename to app/blog_api.py
index 85e198c..cb0b178 100644
--- a/app/goldy_exe_api.py
+++ b/app/blog_api.py
@@ -6,16 +6,16 @@
from datetime import datetime
-from . import constants
-from .async_http_client import get_http_client
+from .http_client import HTTPClient
+from .constants import BLOG_CDN_URL, BLOG_API_URL
__all__ = ()
-class GoldyEXEAPI():
+class BlogAPI():
def __init__(self) -> None:
- self.blogs_data: Tuple[int, list] = (0.0, [])
+ self.blogs_data: Tuple[float, list] = (0.0, [])
- async def get_blog_posts(self, limit: Optional[int] = None) -> List[dict]:
+ async def get_blog_posts(self, http_client: HTTPClient, limit: Optional[int] = None) -> List[dict]:
params = {}
if limit is not None:
@@ -24,15 +24,15 @@ async def get_blog_posts(self, limit: Optional[int] = None) -> List[dict]:
now = datetime.now().timestamp()
if now > self.blogs_data[0] + 60 * 60 * 12: # 12 hours
- http_client = await get_http_client()
+ http_session = await http_client.get_http_session()
- async with http_client.request("GET", constants.BLOG_API_URL + "/posts", params = params) as r:
+ async with http_session.request("GET", BLOG_API_URL + "/posts", params = params) as r:
if r.ok:
blog_posts = [
{
"id": post["id"],
"name": post["name"],
- "thumbnail_url": constants.BLOG_CDN_URL + post["thumbnail"] if post["thumbnail"] is not None else None,
+ "thumbnail_url": BLOG_CDN_URL + post["thumbnail"] if post["thumbnail"] is not None else None,
"date_added": datetime.fromisoformat(post["date_added"]).strftime("%b %d %Y")
} for post in await r.json()
]
diff --git a/app/config.py b/app/config.py
index 9efaf13..9731e4b 100644
--- a/app/config.py
+++ b/app/config.py
@@ -1,16 +1,15 @@
from __future__ import annotations
-from typing import TYPE_CHECKING, TypedDict
+from typing import TYPE_CHECKING, TypedDict, Optional
if TYPE_CHECKING:
- from typing import List, Tuple
- from typing_extensions import NotRequired
+ from typing import List, Tuple, NotRequired
-import toml
+import tomllib
from pathlib import Path
from aiofiles import open
from datetime import datetime
-__all__ = ("Config",)
+__all__ = ()
class ProjectData(TypedDict):
name: str
@@ -28,7 +27,7 @@ class LinkerData(TypedDict):
url: str
class ConfigData(TypedDict):
- status: str
+ status: NotRequired[str]
projects: NotRequired[List[ProjectData]]
linkers: NotRequired[List[LinkerData]]
@@ -43,15 +42,15 @@ async def get_config(self) -> ConfigData:
now = datetime.now().timestamp()
if now > self.config_data[0] + 120:
- merged_config_data: ConfigData = None
+ merged_config_data: Optional[ConfigData] = None
async with open(self.static_config_path, mode = "r", encoding = "utf+8") as file:
- merged_config_data = toml.loads(await file.read())["config"]
+ merged_config_data = tomllib.loads(await file.read())["config"]
if self.local_config_path.exists() is True:
async with open(self.local_config_path, mode = "r", encoding = "utf+8") as file:
- local_config_data = toml.loads(await file.read())["config"]
+ local_config_data = tomllib.loads(await file.read())["config"]
merged_config_data.update(local_config_data)
self.config_data = (now, merged_config_data)
diff --git a/app/constants.py b/app/constants.py
index 9701301..9b47150 100644
--- a/app/constants.py
+++ b/app/constants.py
@@ -1,19 +1,11 @@
from decouple import config
-__all__ = (
- "MAX_DESCRIPTION_LENGTH",
- "CONFIG_PATH",
- "MAL_USERNAME",
- "SOURCE_CODE_URL",
- "CHANGE_LOG_URL",
- "LICENSE_URL",
- "BLOG_CDN_URL",
- "BLOG_API_URL",
- "DEFAULT_HOME_MODE"
-)
+__all__ = ()
MAX_DESCRIPTION_LENGTH = 200
+DEBUG = config("DEBUG", False, cast = bool)
+
CONFIG_PATH = config("CONFIG_PATH", "./config.toml")
MAL_USERNAME = config("MAL_USERNAME", "thegoldenpro")
diff --git a/app/context_builder.py b/app/context_builder.py
index 1d9f300..fb7d0bb 100644
--- a/app/context_builder.py
+++ b/app/context_builder.py
@@ -8,9 +8,7 @@
from . import constants
-__all__ = (
- "PageContextBuilder",
-)
+__all__ = ()
class PageContextData(TypedDict):
request: Request
@@ -26,12 +24,12 @@ class PageContextData(TypedDict):
class PageContextBuilder():
def __init__(
- self,
+ self,
request: Request,
- name: str,
- description: str,
- image_url: Optional[str] = None,
- site_name: Optional[str] = "Goldy",
+ name: str,
+ description: str,
+ image_url: Optional[str] = None,
+ site_name: Optional[str] = "Goldy",
divider: Optional[str] = " ⢠",
theme_colour: str = "#fbc689"
) -> None:
diff --git a/app/http_client.py b/app/http_client.py
new file mode 100644
index 0000000..fcfdbb0
--- /dev/null
+++ b/app/http_client.py
@@ -0,0 +1,20 @@
+from typing import Optional
+
+import logging
+from aiohttp import ClientSession
+
+__all__ = ()
+
+logger = logging.getLogger(__name__)
+
+class HTTPClient():
+ def __init__(self):
+ self.__http_session: Optional[ClientSession] = None
+
+ async def get_http_session(self) -> ClientSession:
+ if self.__http_session is None:
+ logging.debug("Initializing aiohttp client session...")
+
+ self.__http_session = ClientSession()
+
+ return self.__http_session
\ No newline at end of file
diff --git a/app/main.py b/app/main.py
index 2ab177f..6caf843 100644
--- a/app/main.py
+++ b/app/main.py
@@ -1,23 +1,24 @@
-from __future__ import annotations
+import typing
from typing import Literal
import os
from pyromark import Markdown
from fastapi_tailwind import tailwind
from contextlib import asynccontextmanager
-from meow_inator_5000.woutews import nya_service
from fastapi import FastAPI
from fastapi.requests import Request
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
-from starlette.exceptions import HTTPException
from fastapi.responses import RedirectResponse
+from starlette.exceptions import HTTPException
-from .anime import Anime
+from .config import Config
+from .blog_api import BlogAPI
+from .anime_api import AnimeAPI
from .routers import blogs, linkers
-from .goldy_exe_api import GoldyEXEAPI
-from .config import Config, ProjectData
+from .http_client import HTTPClient
+from .markdown import MarkdownSections
from .context_builder import PageContextBuilder
from .CAIPIRINHA_CAIPIRINHA_WHOOOO_YEEEAAAAHHH import CAIPIRINHA_CAIPIRINHA_WHOOOO_YEEEAAAAHHH_or_http_exception
@@ -33,8 +34,9 @@
async def lifespan(_: FastAPI):
# Compile tailwind css.
popen = tailwind.compile(
- output_stylesheet_path = static_files.directory + "/output.css",
- tailwind_stylesheet_path = "./input.css"
+ output_stylesheet_path = str(static_files.directory) + "/output.css",
+ tailwind_stylesheet_path = "./input.css",
+ watch = constants.DEBUG,
)
yield
@@ -48,27 +50,23 @@ async def lifespan(_: FastAPI):
version = __version__,
lifespan = lifespan
)
-app.include_router(nya_service.router)
app.include_router(blogs.router)
app.include_router(linkers.router)
-projects_placeholder: ProjectData = {
- "name": "Wait what",
- "description": "there seems to be nothing here :(",
- "git": "https://cdn.devgoldy.xyz/ricky.webm"
-}
-
+http_client = HTTPClient()
basic_markdown = Markdown()
-goldy_exe_api = GoldyEXEAPI()
-anime = Anime(constants.MAL_USERNAME)
config = Config(constants.CONFIG_PATH)
templates = Jinja2Templates(directory = "./templates")
+markdown_sections = MarkdownSections(basic_markdown, debug_mode = constants.DEBUG)
+
+blog_api = BlogAPI()
+anime_api = AnimeAPI(username = constants.MAL_USERNAME)
@app.get("/")
async def index(request: Request, mode: Literal["legacy", "stable"] = constants.DEFAULT_HOME_MODE):
config_data = await config.get_config()
- blog_posts = await goldy_exe_api.get_blog_posts(8)
+ blog_posts = await blog_api.get_blog_posts(http_client, limit = 8)
context = PageContextBuilder(
request,
@@ -77,41 +75,48 @@ async def index(request: Request, mode: Literal["legacy", "stable"] = constants.
image_url = "/images/image.webp"
)
- with open("./markdown/about_me.md") as file:
- about_me_content = basic_markdown.html(file.read())
-
status_msg = None
status = config_data.get("status")
- if not status == "" and status is not None:
+ if status is not None:
status_msg = basic_markdown.html(status)
- projects = config_data.get("projects", [projects_placeholder])
-
- for index, project in enumerate(projects):
- url = project["link"]
+ projects = config_data.get(
+ "projects",
+ [
+ {
+ "name": "Wait what",
+ "description": "there seems to be nothing here :(",
+ "git": "https://cdn.devgoldy.xyz/ricky.webm"
+ }
+ ]
+ )
- project_image_url = projects[index].get("image")
+ for index in range(len(projects)):
+ url = projects[index]["link"]
+ image_url = projects[index].get("image")
- if project_image_url is None and "https://github.com" in url:
+ if image_url is None and "https://github.com" in url:
git_url = url
split_git_url = git_url.split("/")
git_user = split_git_url[-2]
repo_name = split_git_url[-1]
- project_image_url = "https://opengraph.githubassets.com/d6e56308869b44ec6a37a53b7735b6d5bdd7131635f70cae050baf0197620f3a" \
+ image_url = "https://opengraph.githubassets.com/d6e56308869b44ec6a37a53b7735b6d5bdd7131635f70cae050baf0197620f3a" \
f"/{git_user}/{repo_name}"
- projects[index]["image"] = project_image_url
+ projects[index]["image"] = typing.cast(str, image_url)
+
+ about_me_content = markdown_sections.get_section_html("about_me")
return templates.TemplateResponse(
"legacy_index.html" if mode == "legacy" else "index.html", {
"status": status_msg,
"about_me_content": about_me_content,
"blog_posts": blog_posts,
- "anime_list": await anime.get_anime_status() if mode == "legacy" else [],
+ "anime_list": await anime_api.get_anime_status(http_client) if mode == "legacy" else [],
"open_source_projects": projects,
**context.data
@@ -163,6 +168,12 @@ async def mopping_girl(request: Request):
async def favicon():
return RedirectResponse("./images/rikka.png") # You saw it, didn't you...
+@app.get("/nya") # ðº meow
+async def status(request: Request):
+ return {
+ "version": __version__
+ }
+
@app.exception_handler(HTTPException)
async def custom_http_exception_handler(request: Request, exception: HTTPException):
return await CAIPIRINHA_CAIPIRINHA_WHOOOO_YEEEAAAAHHH_or_http_exception(request, exception)
diff --git a/app/markdown.py b/app/markdown.py
new file mode 100644
index 0000000..6db9fd7
--- /dev/null
+++ b/app/markdown.py
@@ -0,0 +1,33 @@
+import logging
+from pathlib import Path
+from pyromark import Markdown
+
+__all__ = ()
+
+logger = logging.getLogger(__name__)
+
+class MarkdownSections():
+ def __init__(self, basic_markdown: Markdown, debug_mode: bool):
+ self.__debug_mode = debug_mode
+ self.__basic_markdown = basic_markdown
+
+ self.__parsed_markdown_sections: dict[str, str] = {}
+
+ def get_section_html(self, section_id: str) -> str:
+ if self.__parsed_markdown_sections == {} or self.__debug_mode:
+ self.__parse_all_markdown()
+
+ return self.__parsed_markdown_sections[section_id]
+
+ def __parse_all_markdown(self) -> None:
+ logger.debug("Parsing all markdown files...")
+
+ markdown_folder_path = Path("./markdown")
+
+ for markdown_path in markdown_folder_path.glob("*.md"):
+ logger.debug(f"Opening and parsing '{markdown_path.name}' markdown...")
+
+ with markdown_path.open("r") as file:
+ parsed_markdown = self.__basic_markdown.html(file.read())
+
+ self.__parsed_markdown_sections[markdown_path.stem] = parsed_markdown
\ No newline at end of file
diff --git a/app/routers/blogs.py b/app/routers/blogs.py
index 1d16a69..f4846c2 100644
--- a/app/routers/blogs.py
+++ b/app/routers/blogs.py
@@ -1,19 +1,21 @@
from __future__ import annotations
+from bs4 import BeautifulSoup
from datetime import datetime
from pyromark import Markdown, Options
-from bs4 import BeautifulSoup
from fastapi import APIRouter, Request
from fastapi.templating import Jinja2Templates
from fastapi.exceptions import HTTPException
from fastapi.responses import HTMLResponse, RedirectResponse
-from ..goldy_exe_api import GoldyEXEAPI
-from ..async_http_client import get_http_client
+from ..blog_api import BlogAPI
+from ..http_client import HTTPClient
from ..context_builder import PageContextBuilder
from ..constants import BLOG_API_URL, BLOG_CDN_URL, MAX_DESCRIPTION_LENGTH
+__all__ = ()
+
router = APIRouter(prefix = "/blogs")
markdown = Markdown(
@@ -21,19 +23,19 @@
| Options.ENABLE_STRIKETHROUGH
| Options.ENABLE_TABLES
)
-goldy_exe_api = GoldyEXEAPI()
-#markdown = Markdown(extensions = ["fenced_code", "sane_lists", "pymdownx.tilde"])
+goldy_exe_api = BlogAPI()
+http_client = HTTPClient()
templates = Jinja2Templates(directory = "templates")
@router.get("")
@router.get("/")
async def index(request: Request):
- posts = await goldy_exe_api.get_blog_posts()
+ posts = await goldy_exe_api.get_blog_posts(http_client)
context = PageContextBuilder(
- request,
- name = "Home",
- description = "Where you can read my articles, tutorials and rants on technology.",
+ request,
+ name = "Home",
+ description = "Where you can read my articles, tutorials and rants on technology.",
site_name = "Goldy.exe",
theme_colour = "#090b11"
)
@@ -49,54 +51,60 @@ async def index(request: Request):
@router.get("/post/{id}", response_class = HTMLResponse)
async def read_post(request: Request, id: int):
- post = {}
+ post_data = {}
content: str = ""
- http_client = await get_http_client()
+ http_session = await http_client.get_http_session()
- async with http_client.get(BLOG_API_URL + f"/post/{id}") as r:
- if not r.ok:
- raise HTTPException(404, "Hey what you doing here, there's no such post! Stop lurking, smh")
+ async with http_session.get(BLOG_API_URL + f"/post/{id}") as response:
+ if not response.ok:
+ raise HTTPException(
+ status_code = 404,
+ detail = "Hey what you doing here, there's no such post! Stop lurking, smh"
+ )
- post = await r.json()
+ post_data = await response.json()
content_url = BLOG_CDN_URL + f"/{id}/content.md"
- thumbnail_url = BLOG_CDN_URL + post["thumbnail"]
+ thumbnail_url = BLOG_CDN_URL + post_data["thumbnail"]
- async with http_client.get(content_url) as r:
+ async with http_session.get(content_url) as r:
if not r.ok:
raise HTTPException(404, "SHIT WE MESSED UP! HOW DID THIS HAPPEN!!!!!")
data = await r.text("utf-8")
content = markdown.html(data)
- description = BeautifulSoup(content).find("p").get_text()
+ first_paragraph_tag = BeautifulSoup(content, features="html.parser").find("p")
+
+ description = first_paragraph_tag.get_text() if first_paragraph_tag is not None else ""
- content = content.replace('src="./', f'src="./{id}/') # Redirects all html elements linking to root to the blog's cdn redirect.
+ # Redirects all html elements linking to root to the blog's cdn redirect.
+ content = content.replace('src="./', f'src="./{id}/')
if len(description) >= MAX_DESCRIPTION_LENGTH:
description = description[:MAX_DESCRIPTION_LENGTH] + "..."
context = PageContextBuilder(
- request,
- name = post["name"],
- description = description,
- image_url = thumbnail_url,
- site_name = None,
- divider = None,
+ request,
+ name = post_data["name"],
+ description = description,
+ image_url = thumbnail_url,
+ site_name = None,
+ divider = None,
theme_colour = PageContextBuilder.convert_rgb_to_hex(
# Adding fallback here as the old api doesn't have accent_colour.
- tuple([int(value) for value in post.get("accent_colour", "9, 11, 17").split(",")])
+ tuple([int(value) for value in post_data.get("accent_colour", "9, 11, 17").split(",")]) # ty:ignore[invalid-argument-type]
)
)
return templates.TemplateResponse(
"blogs/post.html", {
"id": id,
- "blog_name": post["name"],
- "blog_date_added": datetime.fromisoformat(post["date_added"]).strftime("%b %d %Y"),
- "blog_content": content,
- "blog_thumbnail_url": thumbnail_url,
+ "blog_name": post_data["name"],
+ "blog_date_added": datetime.fromisoformat(post_data["date_added"]).strftime("%b %d %Y"),
+ "blog_content": content,
+ "blog_thumbnail_url": thumbnail_url,
**context.data
}
diff --git a/app/routers/linkers.py b/app/routers/linkers.py
index 1a2a959..4eab247 100644
--- a/app/routers/linkers.py
+++ b/app/routers/linkers.py
@@ -9,6 +9,8 @@
from .. import constants
from ..config import Config
+__all__ = ()
+
router = APIRouter()
templates = Jinja2Templates(directory = "./templates")
diff --git a/input.css b/input.css
index 8c3fdfd..390b913 100644
--- a/input.css
+++ b/input.css
@@ -1,64 +1,130 @@
-@tailwind base;
-@tailwind components;
-@tailwind utilities;
-
-/* ð fonts */
-/* ---------- */
-@font-face {
- font-family: "Dosis";
- src: url("./fonts/Dosis.ttf");
-}
+@import 'tailwindcss';
-@font-face {
- font-family: "atwriter";
- src: url("./fonts/atwriter.ttf");
-}
+/* I'm still migrating some stuff from v3 hence why I'm using config... */
+@config './tailwind.config.js';
-@font-face {
- font-family: "Hacked-KerX";
- src: url("./fonts/Hacked-KerX.ttf");
-}
+/*
+ The default border color has changed to `currentcolor` in Tailwind CSS v4,
+ so we've added these compatibility styles to make sure everything still
+ looks the same as it did with Tailwind CSS v3.
-@font-face {
- font-family: "YanoneKaffeesatz";
- src: url("./fonts/YanoneKaffeesatz.ttf");
+ If we ever want to remove these styles, we need to add an explicit border
+ color utility to any element that depends on these defaults.
+*/
+@layer base {
+ *,
+ ::after,
+ ::before,
+ ::backdrop,
+ ::file-selector-button {
+ border-color: var(--color-gray-200, currentcolor);
+ }
}
-/* âš custom devgoldy.xyz scrollbar */
-/* --------------------------------- */
-::-webkit-scrollbar {
- width: 10px;
-}
+@utility links {
+ & li {
+ @apply inline-block px-3;
+ }
-::-webkit-scrollbar-track {
- background: transparent;
- border-radius: 5px;
-}
+ & a {
+ @apply text-gray-300 hover:text-white transition-all;
+ }
-::-webkit-scrollbar-thumb {
- background: #f1f1f1;
- border-radius: 10px;
+ & i {
+ @apply relative text-3xl top-1;
+ }
}
-/* firefox scrollbar support */
-html {
- scrollbar-color: #f1f1f1 transparent;
+@layer utilities {
+ /* ð fonts */
+ @font-face {
+ font-family: 'Dosis';
+ src: url('./fonts/Dosis.ttf');
+ }
+
+ @font-face {
+ font-family: 'atwriter';
+ src: url('./fonts/atwriter.ttf');
+ }
+
+ @font-face {
+ font-family: 'Hacked-KerX';
+ src: url('./fonts/Hacked-KerX.ttf');
+ }
+
+ @font-face {
+ font-family: 'YanoneKaffeesatz';
+ src: url('./fonts/YanoneKaffeesatz.ttf');
+ }
+
+ /* âš custom devgoldy.xyz scrollbar */
+ ::-webkit-scrollbar {
+ width: 10px;
+ }
+
+ ::-webkit-scrollbar-track {
+ background: transparent;
+ border-radius: 5px;
+ }
+
+ ::-webkit-scrollbar-thumb {
+ background: #f1f1f1;
+ border-radius: 10px;
+ }
+
+ /* firefox scrollbar support */
+ html {
+ scrollbar-color: #f1f1f1 transparent;
+ }
+
+ /* --------------------------------- */
}
-/* --------------------------------- */
+@theme {
+ /* custom colours */
+ --color-exe-black: #090b11;
+ --color-exe-gray: #9ca3af;
+ --color-goldy-pink: #fb89ab;
-@layer base {
+ /* dark colours */
+ --color-goldy-darky: #090B0D;
+ --color-goldy-darky-500: #0a0b0d;
+
+ --color-goldy-darky-orange: #100C0B;
+
+ --color-goldy-greyy: #222930;
+ --color-goldy-greyy-100: #222930;
+ --color-goldy-greyy-300: #2a2b2c;
+ --color-goldy-red: #fb89ab;
+
+ --color-goldy-cream: #fbc689;
+ --color-goldy-cream-200: var(--color-goldy-cream);
+
+ --color-goldy-white: #f1f1f1;
+
+ --color-goldy-orangy: #f5671b;
+ --color-goldy-orangy-100: #f5671b;
+ --color-goldy-orangy-300: #f57d3d;
+ --color-goldy-orangy-800: #f5be3d;
+
+ --color-goldy-green: #d0f54c;
+
+ /* custom drop shadow colours */
+ --color-pinky-drop: 3px 0.5px 1px #fb89ab;
+}
+
+@layer base {
h1, h2, h3, h4, h5, h6, p, li {
@apply font-medium;
}
strong {
- @apply font-[900];
+ @apply font-black;
}
a {
- @apply text-goldyOrangy-300 font-bold transition duration-300 hover:text-goldyOrangy-800
+ @apply text-goldy-orangy-300 font-bold transition duration-300 hover:text-goldy-orangy-800
}
html {
@@ -66,49 +132,42 @@ html {
}
body {
- @apply selection:bg-goldyCream selection:text-exeBlack;
+ @apply selection:bg-goldy-cream selection:text-exe-black;
}
}
@layer components {
- .links li {
- @apply inline-block px-3;
- }
-
- .links a {
- @apply text-gray-300 hover:text-white transition-all;
- }
-
- .links i {
- @apply relative text-3xl top-1;
- }
-
#post-content * {
@apply mb-4;
}
#post-content h1 {
- @apply font-extrabold text-5xl mobile:text-2xl mt-6 !mb-1.5 underline;
+ @apply font-extrabold text-5xl mobile:text-2xl mt-6 mb-1.5! underline;
}
#post-content h2 {
- @apply font-bold text-4xl mobile:text-xl mt-5 !mb-0;
+ @apply font-bold text-4xl mobile:text-xl mt-5 mb-0!;
}
#post-content h3 {
- @apply text-3xl mobile:text-xl underline mt-3 !mb-0;
+ @apply text-3xl mobile:text-xl underline mt-3 mb-0!;
}
#post-content h4 {
- @apply text-2xl mobile:text-lg underline !mb-0;
+ @apply text-2xl mobile:text-lg underline mb-0!;
}
- #post-content h1, #post-content h2, #post-content h3, #post-content h4 {
- @apply text-goldyCream;
+ #post-content h1,
+ #post-content h2,
+ #post-content h3,
+ #post-content h4 {
+ @apply text-goldy-cream;
}
- #post-content h5, #post-content h6, #post-content p {
+ #post-content h5,
+ #post-content h6,
+ #post-content p {
@apply text-xl mobile:text-base;
}
@@ -116,15 +175,18 @@ html {
@apply rounded-3xl my-3;
}
- #post-content ul, #about-me-div ul {
+ #post-content ul,
+ #about-me-div ul {
@apply list-disc pl-8;
}
- #post-content ol, #about-me-div ol {
+ #post-content ol,
+ #about-me-div ol {
@apply list-decimal pl-8;
}
- #post-content ul *, #post-content ol * {
+ #post-content ul *,
+ #post-content ol * {
@apply mb-0;
}
@@ -133,15 +195,15 @@ html {
}
#post-content blockquote {
- @apply p-4 my-4 border-l-4 border-exeGray;
+ @apply p-4 my-4 border-l-4 border-exe-gray;
}
#post-content blockquote p {
- @apply !m-0;
+ @apply m-0!;
}
#post-content p code {
- @apply bg-black px-2 py-1 pb-0.5 text-base rounded-xl mobile:bg-goldyGreyy;
+ @apply bg-black px-2 py-1 pb-0.5 text-base rounded-xl mobile:bg-goldy-greyy;
}
#post-content pre code {
diff --git a/markdown/about_me.md b/markdown/about_me.md
index 161421a..52ce59b 100644
--- a/markdown/about_me.md
+++ b/markdown/about_me.md
@@ -1,14 +1,11 @@
-
Computer geek based in the
-
[UK](https://en.wikipedia.org/wiki/United_Kingdom), loves to [shill](https://www.urbandictionary.com/define.php?term=shill) |
-
[open-source](https://opensource.com/resources/what-open-source)
-and
[Linux](https://en.wikipedia.org/wiki/Linux).
-
Programmer by night, known for [mov-cli](https://github.com/mov-cli/mov-cli), [roseate](https://github.com/cloudy-org/roseate), [aghpb-api](https://github.com/THEGOLDENPRO/aghpb_api), [fastapi-tailwind](https://github.com/THEGOLDENPRO/fastapi-tailwind), [anmoku](https://github.com/THEGOLDENPRO/anmoku) and [more](#projects-div).
+
Software Developer based in the
+
[UK](https://en.wikipedia.org/wiki/United_Kingdom), working towards building â¡ **fast**, ð **transparent** and ð® **efficient** software, driven by **purpose** not **money**.
+
[Linux
[open-source](https://opensource.com/resources/what-open-source) ***[SHILL](https://www.urbandictionary.com/define.php?term=shill)*** or from spreading
[Linux](https://en.wikipedia.org/wiki/Linux) proaganda on the
[internet](https://www.youtube.com/@GoldyTGP), or maybe from [one of these cool projects](#projects-div). *...who am I kidding you have no idea who I am...*
+
+
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..2d04198
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,32 @@
+[project]
+name = "devgoldy-xyz"
+version = "0.1.0"
+description = "My main Website"
+readme = "README.md"
+requires-python = "~=3.13.0"
+dependencies = [
+ "fastapi[standard]~=0.136.0",
+ "aiofiles~=25.1.0",
+ "jinja2~=3.1.6",
+ "aiohttp~=3.13.5",
+ "python-decouple~=3.8",
+ "pyromark~=0.9.10",
+ "beautifulsoup4~=4.14.3",
+ "fastapi-tailwind~=2.0.4a1",
+]
+
+[build-system]
+requires = ["setuptools >= 77.0.3"]
+build-backend = "setuptools.build_meta"
+
+[tool.setuptools.dynamic]
+version = { attr = "app.__version__" }
+
+[tool.setuptools.packages.find]
+include = ["app*"]
+
+[dependency-groups]
+dev = [
+ "ruff~=0.15.11",
+ "ty~=0.0.31",
+]
diff --git a/requirements-dev.txt b/requirements-dev.txt
deleted file mode 100644
index ede3eb4..0000000
--- a/requirements-dev.txt
+++ /dev/null
@@ -1 +0,0 @@
-ruff
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index 2b5e748..0000000
--- a/requirements.txt
+++ /dev/null
@@ -1,12 +0,0 @@
-fastapi[standard]
-aiofiles
-jinja2
-aiohttp
-python-decouple
-typing-extensions
-meow-inator-5000 @ git+https://github.com/THEGOLDENPRO/meow-inator-5000
-pyromark
-toml
-beautifulsoup4
-# Need to hold onto tailwindcss v3 for now until I refactor my codebase to support v4
-fastapi-tailwind==1.0.2b1
\ No newline at end of file
diff --git a/static/audio/mouse_click.mp3 b/static/audio/mouse_click.mp3
new file mode 100644
index 0000000..433f6aa
Binary files /dev/null and b/static/audio/mouse_click.mp3 differ
diff --git a/static/images/gfe_showcase.png b/static/images/gfe_showcase.png
deleted file mode 100644
index 167fe95..0000000
Binary files a/static/images/gfe_showcase.png and /dev/null differ
diff --git a/static/images/github_ani.gif b/static/images/github_ani.gif
new file mode 100644
index 0000000..435b83a
Binary files /dev/null and b/static/images/github_ani.gif differ
diff --git a/static/images/thinking_girl.png b/static/images/thinking_girl.png
new file mode 100644
index 0000000..a012442
Binary files /dev/null and b/static/images/thinking_girl.png differ
diff --git a/static/scripts/index.js b/static/scripts/index.js
index 0c9b3a6..bc5f9d8 100644
--- a/static/scripts/index.js
+++ b/static/scripts/index.js
@@ -68,8 +68,8 @@ function toggleSlideshowImage(slideshow, index) {
}
for (let button of slideshow_buttons) {
- if (button.classList.contains("!bg-goldyCream-200")) {
- button.classList.remove("!bg-goldyCream-200");
+ if (button.classList.contains("!bg-goldy-cream-200")) {
+ button.classList.remove("!bg-goldy-cream-200");
}
}
@@ -81,20 +81,20 @@ function toggleSlideshowImage(slideshow, index) {
// show the current slideshow image and brighten it's button.
slideshow_images[index].classList.remove("hidden");
- slideshow_buttons[index].classList.add("!bg-goldyCream-200");
+ slideshow_buttons[index].classList.add("!bg-goldy-cream-200");
slideshow_titles[index].classList.remove("hidden");
// show title on hover.
let hover_callback = (e) => {
if (e.type == "mouseover") {
- slideshow_images[index].classList.add("blur-sm");
+ slideshow_images[index].classList.add("blur-xs");
//slideshow_images[index].classList.add("saturate-50");
slideshow_images[index].classList.add("brightness-50");
slideshow_images[index].classList.add("sepia");
slideshow_titles[index].classList.remove("opacity-0");
}
else {
- slideshow_images[index].classList.remove("blur-sm");
+ slideshow_images[index].classList.remove("blur-xs");
//slideshow_images[index].classList.remove("saturate-50");
slideshow_images[index].classList.remove("brightness-50");
slideshow_images[index].classList.remove("sepia");
diff --git a/static/scripts/index.ts b/static/scripts/index.ts
index 120cb86..be9d8f7 100644
--- a/static/scripts/index.ts
+++ b/static/scripts/index.ts
@@ -71,8 +71,8 @@ function toggleSlideshowImage(slideshow: HTMLElement, index: number) {
}
for (let button of slideshow_buttons) {
- if (button.classList.contains("!bg-goldyCream-200")) {
- button.classList.remove("!bg-goldyCream-200");
+ if (button.classList.contains("!bg-goldy-cream-200")) {
+ button.classList.remove("!bg-goldy-cream-200");
}
}
@@ -84,20 +84,20 @@ function toggleSlideshowImage(slideshow: HTMLElement, index: number) {
// show the current slideshow image and brighten it's button.
slideshow_images[index].classList.remove("hidden");
- slideshow_buttons[index].classList.add("!bg-goldyCream-200");
+ slideshow_buttons[index].classList.add("!bg-goldy-cream-200");
slideshow_titles[index].classList.remove("hidden");
// show title on hover.
let hover_callback = (e: MouseEvent) => {
if (e.type == "mouseover") {
- slideshow_images[index].classList.add("blur-sm");
+ slideshow_images[index].classList.add("blur-xs");
//slideshow_images[index].classList.add("saturate-50");
slideshow_images[index].classList.add("brightness-50");
slideshow_images[index].classList.add("sepia");
slideshow_titles[index].classList.add("opacity-100");
} else {
- slideshow_images[index].classList.remove("blur-sm");
+ slideshow_images[index].classList.remove("blur-xs");
//slideshow_images[index].classList.remove("saturate-50");
slideshow_images[index].classList.remove("brightness-50");
slideshow_images[index].classList.remove("sepia");
diff --git a/static/scripts/mouse_click.js b/static/scripts/mouse_click.js
new file mode 100644
index 0000000..e960b5a
--- /dev/null
+++ b/static/scripts/mouse_click.js
@@ -0,0 +1,20 @@
+const mouseClickAudio = new Audio("/audio/mouse_click.mp3");
+
+window.addEventListener("click", (event) => {
+ const target = event.target;
+
+ const shouldClick = event.isPrimary && (
+ target.closest("a") ||
+ target.tagName == "BUTTON" ||
+ target.hasAttribute("data-click")
+ );
+
+ if (shouldClick) {
+ console.debug("Playing mouse click sound (target: " + target + ")...")
+
+ mouseClickAudio.pause();
+ mouseClickAudio.currentTime = 0;
+
+ mouseClickAudio.play();
+ }
+});
\ No newline at end of file
diff --git a/static/scripts/yt_music.js b/static/scripts/yt_music.js
new file mode 100644
index 0000000..55bb039
--- /dev/null
+++ b/static/scripts/yt_music.js
@@ -0,0 +1,82 @@
+const musicPlayerDiv = document.getElementById("music-player-div");
+const musicPlayerButton = document.getElementById("music-player-play-button");
+const musicPlayerDisplayText = document.getElementById("music-player-display-text");
+
+const songIDs = [
+ "vUU-Yl2Dprk", // LAMP - Kokoro no Madobe ni...
+ "ELSAPf-z9VI", // LAMP - Futari no ita fukei
+ "iSt0Sit6j00", // Takayuki Negishi - On The Special Day
+ "JM09NNyEpdY", // ALI PROJECT - S嬢ã®ç§ããããªææš
+];
+
+var player;
+
+function onYouTubeIframeAPIReady() {
+ const randomSongID = songIDs[Math.floor(Math.random() * songIDs.length)];
+
+ console.debug("Picked song id '" + randomSongID + "' from list.");
+
+ player = new YT.Player("yt-music-embed", {
+ height: "0",
+ width: "0",
+ videoId: randomSongID,
+ host: "https://www.youtube-nocookie.com",
+ playerVars: {
+ "playsinline": 1,
+ "origin": window.location.origin,
+ },
+ events: {
+ "onReady": onPlayerReady,
+ }
+ });
+}
+
+function onPlayerReady(_event) {
+ player.setVolume(30);
+
+ musicPlayerDisplayText.innerText = "Press play again to play music.";
+
+ ytEmbedLoaded = true;
+}
+
+var ytEmbedLoaded = false;
+
+musicPlayerButton.addEventListener("click", (event) => {
+ if (ytEmbedLoaded) {
+ return;
+ }
+
+ const asyncScriptTag = document.createElement("script");
+ asyncScriptTag.src = "https://www.youtube.com/iframe_api";
+
+ const scriptTag = document.getElementById("yt-music-script");
+ scriptTag.parentNode.insertBefore(asyncScriptTag, scriptTag);
+
+ console.debug("Loading youtube embed for playing music...");
+ musicPlayerDisplayText.innerText = "Loading YouTube embed...";
+});
+
+var musicTitle = null;
+var musicPlaying = false;
+
+musicPlayerButton.addEventListener("click", (event) => {
+ if (!ytEmbedLoaded) {
+ return;
+ }
+
+ console.debug("Playing music with youtube embed API...");
+
+ (!musicPlaying) ? player.playVideo() : player.stopVideo();
+
+ if (musicTitle == null) {
+ const videoData = player.getVideoData();
+
+ musicTitle = videoData.title;
+ }
+
+ musicPlayerDisplayText.innerText = (!musicPlaying) ? "â¶ïž PLAYING - " + musicTitle : "⌠STOPPED";
+
+ musicPlayerButton.innerText = (!musicPlaying) ? "âŒ" : "â¶ïž";
+
+ musicPlaying = !musicPlaying;
+});
\ No newline at end of file
diff --git a/static_config.toml b/static_config.toml
index 69406b2..b5f58c4 100644
--- a/static_config.toml
+++ b/static_config.toml
@@ -41,37 +41,11 @@ link = "https://github.com/THEGOLDENPRO/snakelings"
image = "https://raw.githubusercontent.com/THEGOLDENPRO/snakelings/main/assets/preview.png"
[[config.projects]]
-name = "ð GFE"
-description = "Goldy's first editor. Built in Rust ðŠ with the ICED ð§ tool kit."
-link = "https://github.com/THEGOLDENPRO/gfe"
-image = "./images/gfe_showcase.png"
+name = "ðº http-cat-cli"
+link = "https://github.com/THEGOLDENPRO/http-cat-cli"
+image = "https://raw.githubusercontent.com/THEGOLDENPRO/http-cat-cli/main/assets/http_cat_showcase.png"
[[config.projects]]
name = "Anmoku å®é»"
description = "A peaceful and fully typed MyAnimeList / Jikan Python API wrapper with caching and proper rate limiting."
-link = "https://github.com/THEGOLDENPRO/anmoku"
-
-[[config.projects]]
-name = "ð£ JS:QP Core"
-description = "WIP open source minecraft package management library."
-link = "https://github.com/JS-Quick-Pack/jsqp-core"
-
-[[config.projects]]
-name = "â« aghpb.c ð"
-description = "Anime girls holding programming books API wrapper for â« C."
-link = "https://github.com/THEGOLDENPRO/aghpb.c"
-
-[[config.projects]]
-name = "ð£ koff"
-description = "ð£ Temporary KDE osu! full screen fix."
-link = "https://github.com/THEGOLDENPRO/kde-osu-fullscreen-fix"
-
-[[config.projects]]
-name = "ðŠ aghpb.rs ð"
-description = "Anime girls holding programming books API wrapper for ðŠ Rust."
-link = "https://github.com/THEGOLDENPRO/aghpb.rs"
-
-[[config.projects]]
-name = "ðŸ hello world debloated af"
-description = "Hello world in intel assembly."
-link = "https://github.com/THEGOLDENPRO/hello-world-debloated-af"
+link = "https://github.com/THEGOLDENPRO/anmoku"
\ No newline at end of file
diff --git a/tailwind.config.js b/tailwind.config.js
index d154dc8..9ce1f57 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -4,39 +4,6 @@ module.exports = {
plugins: [],
theme: {
extend: {
- colors: {
- exeBlack: "#090b11",
- exeGray: "#9ca3af",
- goldyPink: "#fb89ab",
- goldyDarky: {
- DEFAULT: "#0e1114",
- 200: "#0e1114",
- 300: "#0b0d0f",
- 500: "#0a0b0d",
- },
- goldyGreyy: "#222930",
- goldyGreyy: {
- DEFAULT: "#222930",
- 100: "#222930",
- 300: "#2A2B2C"
- },
- goldyCream: {
- DEFAULT: "#fbc689",
- 200: "#fbc689",
- 800: "theme(colors.orange.50)"
- },
- goldyWhite: "#f1f1f1",
- goldyOrangy: {
- DEFAULT: "#f5671b",
- 100: "#f5671b",
- 300: "#f57d3d",
- 800: "#f5be3d"
- },
- goldyGreen: "#d0f54c"
- },
- dropShadow: {
- "pinky-drop": "3px 0.5px 1px #fb89ab",
- },
animation: {
"flicker": "flicker 0.00001s infinite ease-in",
"fade-in": "fadeIn ease 5s",
@@ -71,7 +38,7 @@ module.exports = {
"desktop": {"max": "1280px"}
},
fontFamily: {
- "YanoneKaffeesatz": ["YanoneKaffeesatz"],
+ "yanone-kaffeesatz": ["YanoneKaffeesatz"],
"hacked": ["Hacked-KerX"],
"typewriter": ["atwriter"],
"dosis": ["Dosis"]
diff --git a/templates/base.html b/templates/base.html
index bd98795..0b74139 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -18,12 +18,15 @@
+
{% endblock %}
{% block body %}
{% endblock %}
+
+
-
+ More Projects
+