Skip to content

Commit

Permalink
feat: blog and projects update (#4)
Browse files Browse the repository at this point in the history
Features:
- Database and Ecto setup
  • Add Ecto repositories (04aea36)
  • Base Ecto repo setup (cdec157)
  • Add dev database (cbac664)
- Project structure and routing
  • Base projects setup (a0be400, f20f705)
  • Update routers (33964e6)
  • Add projects as another controller (b31ca91)
- Blog functionality
  • Update blog setup (bf76f16)
  • Setup proper blog posts and schema (e2e10f7)
- Frontend enhancements
  • Add Tailwind typography (c27c0e4)
  • Tailwind node updates (9263f9e)
  • Add static assets (21cd893)
- Performance improvements
  • Add cache (9de8b0b)
- Development tools
  • Add migrations (e31a4d9)
  • Add packages (35d80ce)
  • Tests buildout (abdbd85)

Bug Fixes:
- Improve post rendering
  • Fix how posts are rendered (e7bd8a2)
  • Actually set HTML from Markdown (5d87f32)
- Resolve sticky footer issue (6b2c8f6)
- Disable robots temporarily (9f45891)

Other Changes:
- Add node modules to .gitignore (ab9aaf9)
  • Loading branch information
RemoteRabbit authored Nov 6, 2024
1 parent 4575631 commit 3fe1dc8
Show file tree
Hide file tree
Showing 53 changed files with 2,448 additions and 111 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,4 @@ portfolio-*.tar
# In case you use Node.js/npm, you want to ignore these.
npm-debug.log
/assets/node_modules/

/node_modules
1 change: 1 addition & 0 deletions assets/tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ module.exports = {
},
plugins: [
require("@tailwindcss/forms"),
require("@tailwindcss/typography"),
// Allows prefixing tailwind classes with LiveView classes to add rules
// only when LiveView classes are applied, for example:
//
Expand Down
2 changes: 2 additions & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
# General application configuration
import Config

config :portfolio, ecto_repos: [Portfolio.Repo]

# Configures the endpoint
config :portfolio, PortfolioWeb.Endpoint,
url: [host: "localhost"],
Expand Down
9 changes: 9 additions & 0 deletions config/dev.exs
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import Config

config :portfolio, Portfolio.Repo,
username: "postgres",
password: "postgres",
hostname: "localhost",
database: "portfolio_dev",
stacktrace: true,
show_sensitive_data_on_connection_error: true,
pool_size: 10

# For development, we disable any cache and enable
# debugging and code reloading.
#
Expand Down
13 changes: 7 additions & 6 deletions lib/portfolio/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@ defmodule Portfolio.Application do
@impl true
def start(_type, _args) do
children = [
Portfolio.Repo,
PortfolioWeb.Telemetry,
{Phoenix.PubSub, name: Portfolio.PubSub},
{Finch, name: Portfolio.Finch},
PortfolioWeb.Endpoint,
{ConCache, [
name: :blog_cache,
ttl_check_interval: :timer.seconds(1),
global_ttl: :timer.hours(1)
]}
{ConCache,
[
name: :blog_cache,
ttl_check_interval: :timer.seconds(1),
global_ttl: :timer.hours(1)
]}
]

opts = [strategy: :one_for_one, name: Portfolio.Supervisor]
Expand All @@ -26,4 +28,3 @@ defmodule Portfolio.Application do
:ok
end
end

13 changes: 7 additions & 6 deletions lib/portfolio/blog.ex
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ defmodule Portfolio.Blog do
"""
def list_posts do
case ConCache.get_or_store(:blog_cache, :posts, fn ->
posts = Portfolio.Blog.Post.fetch_posts("remoterabbit", "blog-posts")
%{value: posts, ttl: @cache_ttl}
end) do
posts = Portfolio.Blog.Post.fetch_posts("remoterabbit", "blog-posts")
%{value: posts, ttl: @cache_ttl}
end) do
%{value: posts} -> posts
posts -> posts
end
Expand All @@ -40,9 +40,9 @@ defmodule Portfolio.Blog do
Returns the blog post struct if found, or `nil` if not found.
"""
def get_post_by_slug(slug) do
list_posts()
|> Enum.find(&(&1.slug == slug))
def get_post_by_slug!(slug) do
posts = list_posts()
Enum.find(posts, fn post -> post.slug == slug end)
end

@doc """
Expand All @@ -58,3 +58,4 @@ defmodule Portfolio.Blog do
list_posts()
end
end

69 changes: 32 additions & 37 deletions lib/portfolio/blog/post.ex
Original file line number Diff line number Diff line change
@@ -1,78 +1,73 @@
defmodule Portfolio.Blog.Post do
@moduledoc """
This module defines a struct to represent a blog post and provides functions
to fetch and parse blog posts from a GitHub repository.
use Ecto.Schema
import Ecto.Changeset

## Struct Fields
schema "posts" do
field(:context, :string)
field(:title, :string)
field(:slug, :string)
field(:published_at, :naive_datetime)
field(:content, :string, virtual: true)
field(:date, :date, virtual: true)
field(:description, :string, virtual: true)
field(:status, :string, virtual: true)

- `title`: The title of the blog post.
- `content`: The HTML content of the blog post.
- `date`: The date the blog post was published.
- `slug`: The slug (URL-friendly version of the title) for the blog post.
- `description`: A short description of the blog post.
- `status`: The status of the blog post (e.g., "published", "draft").
## Functions
- `fetch_posts/2`: Fetches blog posts from a GitHub repository.
- `parse_content/2`: Parses the content of a blog post file.
"""
defstruct [:title, :content, :date, :slug, :description, :status]

@doc """
Fetches blog posts from a GitHub repository.
This function retrieves the contents of the specified `path` (defaulting to "posts/")
in the given `owner` and `repo`. It filters the contents to only include Markdown files,
parses the content of each file, and returns a list of `%Portfolio.Blog.Post{}` structs
representing the published blog posts, sorted in descending order by date.
## Examples
iex> Portfolio.Blog.Post.fetch_posts("owner", "repo")
[%Portfolio.Blog.Post{...}, ...]
timestamps()
end

iex> Portfolio.Blog.Post.fetch_posts("owner", "repo", "custom/path/")
[%Portfolio.Blog.Post{...}, ...]
@doc false
def changeset(post, attrs) do
post
|> cast(attrs, [:title, :slug, :context, :published_at])
|> validate_required([:title, :slug, :context, :published_at])
end

"""
def fetch_posts(owner, repo, path \\ "posts/") do
case Tentacat.Contents.find(owner, repo, path) do
{200, contents, _} ->
contents
|> Enum.filter(&(&1["type"] == "file" && String.ends_with?(&1["name"], ".md")))
|> Enum.map(fn file ->
|> Enum.map(fn file ->
{200, content, _} = Tentacat.Contents.find(owner, repo, file["path"])

content["content"]
|> String.replace("\n", "")
|> Base.decode64!()
|> parse_content(file["path"])
end)
|> Enum.filter(& &1.status == "published")
|> Enum.filter(&(&1.status == "published"))
|> Enum.sort_by(& &1.date, {:desc, Date})

{404, _, _} ->
[]

{status, body, _} ->
IO.puts "GitHub API returned status #{status}: #{inspect(body)}"
IO.puts("GitHub API returned status #{status}: #{inspect(body)}")
[]
end
end

defp parse_content(content, path) do
case YamlFrontMatter.parse(content) do
{:ok, metadata, markdown_content} ->
# Clean up any potential whitespace or formatting issues
cleaned_content =
markdown_content
|> String.trim()
|> String.replace(~r/\r\n?/, "\n")

%__MODULE__{
title: metadata["title"],
date: Date.from_iso8601!(metadata["date"]),
description: metadata["description"],
content: Earmark.as_html!(markdown_content),
content: cleaned_content,
slug: Path.basename(path, ".md"),
status: metadata["status"] || "published"
}

_ ->
raise "Invalid blog post format"
end
end
end

104 changes: 104 additions & 0 deletions lib/portfolio/projects.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
defmodule Portfolio.Projects do
@moduledoc """
The Projects context.
"""

import Ecto.Query, warn: false
alias Portfolio.Repo

alias Portfolio.Projects.Project

@doc """
Returns the list of projects.
## Examples
iex> list_projects()
[%Project{}, ...]
"""
def list_projects do
Repo.all(Project)
end

@doc """
Gets a single project.
Raises `Ecto.NoResultsError` if the Project does not exist.
## Examples
iex> get_project!(123)
%Project{}
iex> get_project!(456)
** (Ecto.NoResultsError)
"""
def get_project!(id), do: Repo.get!(Project, id)

@doc """
Creates a project.
## Examples
iex> create_project(%{field: value})
{:ok, %Project{}}
iex> create_project(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_project(attrs \\ %{}) do
%Project{}
|> Project.changeset(attrs)
|> Repo.insert()
end

@doc """
Updates a project.
## Examples
iex> update_project(project, %{field: new_value})
{:ok, %Project{}}
iex> update_project(project, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_project(%Project{} = project, attrs) do
project
|> Project.changeset(attrs)
|> Repo.update()
end

@doc """
Deletes a project.
## Examples
iex> delete_project(project)
{:ok, %Project{}}
iex> delete_project(project)
{:error, %Ecto.Changeset{}}
"""
def delete_project(%Project{} = project) do
Repo.delete(project)
end

@doc """
Returns an `%Ecto.Changeset{}` for tracking project changes.
## Examples
iex> change_project(project)
%Ecto.Changeset{data: %Project{}}
"""
def change_project(%Project{} = project, attrs \\ %{}) do
Project.changeset(project, attrs)
end
end
21 changes: 21 additions & 0 deletions lib/portfolio/projects/project.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
defmodule Portfolio.Projects.Project do
use Ecto.Schema
import Ecto.Changeset

schema "projects" do
field :description, :string
field :title, :string
field :url, :string
field :tech_stack, {:array, :string}
field :featured, :boolean, default: false

timestamps()
end

@doc false
def changeset(project, attrs) do
project
|> cast(attrs, [:title, :description, :url, :tech_stack, :featured])
|> validate_required([:title, :description, :url, :tech_stack, :featured])
end
end
5 changes: 5 additions & 0 deletions lib/portfolio/repo.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
defmodule Portfolio.Repo do
use Ecto.Repo,
otp_app: :portfolio,
adapter: Ecto.Adapters.Postgres
end
Loading

0 comments on commit 3fe1dc8

Please sign in to comment.