Merge pull request '(CI/CD): Workflow and Docker implementation.' (#26) from dev into master
Some checks failed
Deploy application with Docker / build_and_deploy (push) Failing after 1m34s

Reviewed-on: #26
This commit is contained in:
Hayden Hargreaves 2025-07-21 20:23:54 -07:00
commit 13596f7cb6
7 changed files with 267 additions and 184 deletions

28
.github/workflows/deploy.yml vendored Normal file
View File

@ -0,0 +1,28 @@
name: Deploy application with Docker
on:
push:
branches:
- master
jobs:
build_and_deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v3
with:
context: .
file: ./Dockerfile
push: true
tags: azpect3120/option.gophernest:latest

View File

@ -1,15 +1,54 @@
# TEMPORARY SOLUTION
# Need a real way to build tailwind and build the templ files
FROM golang:1.24
# Fetch stage
FROM golang:latest AS fetch-stage
COPY go.mod go.sum /app
WORKDIR /app
COPY . .
RUN go mod download
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/app /app/cmd/web/main.go
# Generate stage
FROM ghcr.io/a-h/templ:latest AS generate-stage
COPY --chown=65532:65532 . /app
WORKDIR /app
RUN ["templ", "generate"]
# Generate stage two
FROM node:lts-alpine AS tailwind-build-stage
COPY --from=generate-stage /app /app
WORKDIR /app
RUN npm install tailwindcss @tailwindcss/cli && npx @tailwindcss/cli -i ./web/static/css/main.css -o ./web/static/css/tailwind.css --minify -c ./tailwind.config.js
# Build stage
FROM golang:1.24 AS build-stage
COPY --from=tailwind-build-stage /app /app
WORKDIR /app
RUN CGO_ENABLED=0 GOOS=linux go build -o /entrypoint /app/cmd/web/main.go
# Deploy.
FROM gcr.io/distroless/static-debian11 AS release-stage
WORKDIR /
COPY --from=build-stage /entrypoint /entrypoint
COPY --from=build-stage /app/web/static /web/static
EXPOSE 3000
CMD [ "/app/app" ]
USER nonroot:nonroot
ENTRYPOINT ["/entrypoint"]

View File

@ -56,7 +56,7 @@ func Init(port int) *Server {
func (s *Server) ConfigureAuth() *Server {
err := godotenv.Load(".env")
if err != nil {
panic("Could not load env file")
fmt.Printf("No .env file found or error loading .env: %v. Relying on system environment variables.", err)
}
redirect_domain := os.Getenv("DOMAIN")
@ -80,7 +80,7 @@ func (s *Server) ConfigureAuth() *Server {
func (s *Server) ConnectDatabase() *Server {
err := godotenv.Load(".env")
if err != nil {
panic("Could not load env file")
fmt.Printf("No .env file found or error loading .env: %v. Relying on system environment variables.", err)
}
var connUrl string = os.Getenv("DATABASE_URL")
@ -107,7 +107,7 @@ func (s *Server) Start() {
func (s *Server) Setup() *Server {
err := godotenv.Load(".env")
if err != nil {
panic("Could not load env file")
fmt.Printf("No .env file found or error loading .env: %v. Relying on system environment variables.", err)
}
jwtSecret := []byte(os.Getenv("JWT_SECRET"))

View File

@ -4,75 +4,89 @@ import "strings"
import "github.com/haydenhargreaves/Potion/internal/domain/server"
templ navLink(current, name, url string) {
<a href={ templ.SafeURL(url) } if strings.ToLower(current)==strings.ToLower(name) {
class="text-gray-700 border-b-2 border-blue-500 px-1 cursor-pointer" } else {
class="text-gray-700 border-b-2 hover:border-blue-400 px-1 cursor-pointer border-white duration-150" }>
{ name }
</a>
<a
href={ templ.SafeURL(url) }
if strings.ToLower(current)==strings.ToLower(name) {
class="text-gray-700 border-b-2 border-blue-500 px-1 cursor-pointer"
} else {
class="text-gray-700 border-b-2 hover:border-blue-400 px-1 cursor-pointer border-white duration-150"
}
>
{ name }
</a>
}
templ dropdownLink(name, url string) {
<a class="py-2" href={ templ.SafeURL(url) }>
{ name }
</a>
<a class="py-2" href={ templ.SafeURL(url) }>
{ name }
</a>
}
templ listIcon(current, name, url string) {
<a href={ templ.SafeURL(url) }>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"
if strings.ToLower(current) == strings.ToLower(name) {
class="h-4 text-blue-500"
} else {
class="h-4 text-gray-700 hover:text-blue-400 duration-150"
}
>
<path fill="currentColor"
d="M0 24C0 10.7 10.7 0 24 0L69.5 0c22 0 41.5 12.8 50.6 32l411 0c26.3 0 45.5 25 38.6 50.4l-41 152.3c-8.5 31.4-37 53.3-69.5 53.3l-288.5 0 5.4 28.5c2.2 11.3 12.1 19.5 23.6 19.5L488 336c13.3 0 24 10.7 24 24s-10.7 24-24 24l-288.3 0c-34.6 0-64.3-24.6-70.7-58.5L77.4 54.5c-.7-3.8-4-6.5-7.9-6.5L24 48C10.7 48 0 37.3 0 24zM128 464a48 48 0 1 1 96 0 48 48 0 1 1 -96 0zm336-48a48 48 0 1 1 0 96 48 48 0 1 1 0-96z">
</path>
</svg>
</a>
<a href={ templ.SafeURL(url) }>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 576 512"
if strings.ToLower(current)==strings.ToLower(name) {
class="h-4 text-blue-500"
} else {
class="h-4 text-gray-700 hover:text-blue-400 duration-150"
}
>
<path
fill="currentColor"
d="M0 24C0 10.7 10.7 0 24 0L69.5 0c22 0 41.5 12.8 50.6 32l411 0c26.3 0 45.5 25 38.6 50.4l-41 152.3c-8.5 31.4-37 53.3-69.5 53.3l-288.5 0 5.4 28.5c2.2 11.3 12.1 19.5 23.6 19.5L488 336c13.3 0 24 10.7 24 24s-10.7 24-24 24l-288.3 0c-34.6 0-64.3-24.6-70.7-58.5L77.4 54.5c-.7-3.8-4-6.5-7.9-6.5L24 48C10.7 48 0 37.3 0 24zM128 464a48 48 0 1 1 96 0 48 48 0 1 1 -96 0zm336-48a48 48 0 1 1 0 96 48 48 0 1 1 0-96z"
></path>
</svg>
</a>
}
templ Navbar(current string) {
<nav class="block md:fixed w-full z-10">
<div
class="relative w-full px-8 md:px-44 p-4 border-b border-gray-300 shadow-sm shadow-gray-300 bg-white flex justify-between items-center">
<div>
<a href={ domain.WEB_HOME }>
<p class="select-none">Potion</p>
</a>
</div>
<div class="hidden md:flex lg:flex items-center gap-8 select-none">
@navLink(current, "Home", domain.WEB_HOME)
@navLink(current, "Favorites", domain.WEB_FAVORITES)
@navLink(current, "Create", domain.WEB_CREATE)
@navLink(current, "Profile", domain.WEB_PROFIlE)
@listIcon(current, "List", domain.WEB_LIST)
</div>
<div class="md:hidden grid place-content-center">
<button onclick="toggleMenu()" class="p-2">
// carot
<svg id="mobile-menu-button-carot" class="hidden size-5" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 320 512">
<path
d="M182.6 137.4c-12.5-12.5-32.8-12.5-45.3 0l-128 128c-9.2 9.2-11.9 22.9-6.9 34.9s16.6 19.8 29.6 19.8l256 0c12.9 0 24.6-7.8 29.6-19.8s2.2-25.7-6.9-34.9l-128-128z">
</path>
</svg>
// bars
<svg id="mobile-menu-button-bars" class="size-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
<path fill="currentColor"
d="M0 96C0 78.3 14.3 64 32 64l384 0c17.7 0 32 14.3 32 32s-14.3 32-32 32L32 128C14.3 128 0 113.7 0 96zM0 256c0-17.7 14.3-32 32-32l384 0c17.7 0 32 14.3 32 32s-14.3 32-32 32L32 288c-17.7 0-32-14.3-32-32zM448 416c0 17.7-14.3 32-32 32L32 448c-17.7 0-32-14.3-32-32s14.3-32 32-32l384 0c17.7 0 32 14.3 32 32z">
</path>
</svg>
</button>
</div>
@hamburgerMenu()
</div>
</nav>
<nav class="block md:fixed w-full z-10">
<div
class="relative w-full px-8 md:px-44 p-4 border-b border-gray-300 shadow-sm shadow-gray-300 bg-white flex justify-between items-center"
>
<div>
<a href={ domain.WEB_HOME }>
<p class="select-none">Potion</p>
</a>
</div>
<div class="hidden md:flex lg:flex items-center gap-8 select-none">
@navLink(current, "Home", domain.WEB_HOME)
@navLink(current, "Favorites", domain.WEB_FAVORITES)
@navLink(current, "Create", domain.WEB_CREATE)
@navLink(current, "Profile", domain.WEB_PROFIlE)
@listIcon(current, "List", domain.WEB_LIST)
</div>
<div class="md:hidden grid place-content-center">
<button onclick="toggleMenu()" class="p-2">
// carot
<svg
id="mobile-menu-button-carot"
class="hidden size-5"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 320 512"
>
<path
d="M182.6 137.4c-12.5-12.5-32.8-12.5-45.3 0l-128 128c-9.2 9.2-11.9 22.9-6.9 34.9s16.6 19.8 29.6 19.8l256 0c12.9 0 24.6-7.8 29.6-19.8s2.2-25.7-6.9-34.9l-128-128z"
></path>
</svg>
// bars
<svg id="mobile-menu-button-bars" class="size-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
<path
fill="currentColor"
d="M0 96C0 78.3 14.3 64 32 64l384 0c17.7 0 32 14.3 32 32s-14.3 32-32 32L32 128C14.3 128 0 113.7 0 96zM0 256c0-17.7 14.3-32 32-32l384 0c17.7 0 32 14.3 32 32s-14.3 32-32 32L32 288c-17.7 0-32-14.3-32-32zM448 416c0 17.7-14.3 32-32 32L32 448c-17.7 0-32-14.3-32-32s14.3-32 32-32l384 0c17.7 0 32 14.3 32 32z"
></path>
</svg>
</button>
</div>
@hamburgerMenu()
</div>
</nav>
}
templ hamburgerMenu() {
<script>
<script>
function toggleMenu() {
const menu = document.getElementById("mobile-menu-content");
const carotButton = document.getElementById("mobile-menu-button-carot");
@ -93,13 +107,14 @@ templ hamburgerMenu() {
}
}
</script>
<div id="mobile-menu-content"
class="hidden w-full flex-col items-center absolute top-[100%] left-0 py-2 bg-white border-b border-gray-300 shadow-sm shadow-gray-300 z-20">
@dropdownLink("Home", domain.WEB_HOME)
@dropdownLink("Favorites", domain.WEB_FAVORITES)
@dropdownLink("Create", domain.WEB_CREATE)
@dropdownLink("Profile", domain.WEB_PROFIlE)
@dropdownLink("Shopping List", domain.WEB_LIST)
</div>
<div
id="mobile-menu-content"
class="hidden w-full flex-col items-center absolute top-[100%] left-0 py-2 bg-white border-b border-gray-300 shadow-sm shadow-gray-300 z-20"
>
@dropdownLink("Home", domain.WEB_HOME)
@dropdownLink("Favorites", domain.WEB_FAVORITES)
@dropdownLink("Create", domain.WEB_CREATE)
@dropdownLink("Profile", domain.WEB_PROFIlE)
@dropdownLink("Shopping List", domain.WEB_LIST)
</div>
}

View File

@ -63,7 +63,7 @@ func navLink(current, name, url string) templ.Component {
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/components/navbar.templ`, Line: 10, Col: 8}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/components/navbar.templ`, Line: 15, Col: 8}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
@ -114,7 +114,7 @@ func dropdownLink(name, url string) templ.Component {
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/components/navbar.templ`, Line: 16, Col: 8}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/components/navbar.templ`, Line: 21, Col: 8}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
@ -211,7 +211,7 @@ func Navbar(current string) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\"><p class=\"select-none\">Potion</p></a></div><div class=\"hidden md:flex lg:flex items-center gap-8 select-none\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\"><p class=\"select-none text-red-800\">Potion</p></a></div><div class=\"hidden md:flex lg:flex items-center gap-8 select-none\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}

View File

@ -5,118 +5,124 @@ import "github.com/haydenhargreaves/Potion/internal/domain/server"
import domainRecipe "github.com/haydenhargreaves/Potion/internal/domain/recipe"
templ introSection() {
<section class="w-full h-fit mb-16">
<div class="relative">
<video class="" autoplay loop muted playsinline>
<source src="/v1/web/static/img/salmon_video.mp4" type="video/mp4" />
</video>
<h1 class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-center
text-white text-3xl w-4/5 font-bold z-10">
Discover Your Next Favorite Meal
</h1>
</div>
<p class="leading-relaxed p-4 my-8">
Welcome to your ultimate recipe hub! Whether you're a seasoned chef or just starting your culinary adventure,
we're here to inspire. Explore thousands of delicious recipes, from quick weeknight dinners to gourmet delights,
all at your fingertips. Find exactly what you're craving with our powerful search and intuitive filters, or
browse our trending dishes for fresh ideas.
</p>
</section>
<section class="w-full h-fit mb-16">
<div class="relative">
<video class="" autoplay loop muted playsinline>
<source src="/v1/web/static/img/salmon_video.mp4" type="video/mp4"/>
</video>
<h1
class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-center
text-white text-3xl w-4/5 font-bold z-10"
>
Discover Your Next Favorite Meal
</h1>
</div>
<p class="leading-relaxed p-4 my-8">
Welcome to your ultimate recipe hub! Whether you're a seasoned chef or just starting your culinary adventure,
we're here to inspire. Explore thousands of delicious recipes, from quick weeknight dinners to gourmet delights,
all at your fingertips. Find exactly what you're craving with our powerful search and intuitive filters, or
browse our trending dishes for fresh ideas.
</p>
</section>
}
templ searchSection() {
<section class="w-full flex flex-col items-center justify-center my-8 py-4">
@components.BannerText("Craving Something Specific?")
<div class="w-full md:w-3/4">
@components.SearchBar(domainRecipe.SearchFilters{}, true, false, false)
</div>
<div class="hidden" id="result-list"></div>
</section>
<section class="w-full flex flex-col items-center justify-center my-8 py-4">
@components.BannerText("Craving Something Specific?")
<div class="w-full md:w-3/4">
@components.SearchBar(domainRecipe.SearchFilters{}, true, false, false)
</div>
<div class="hidden" id="result-list"></div>
</section>
}
templ highlightSection(liked bool) {
<section class="w-full flex flex-col items-center justify-center my-8 py-4">
@components.BannerText("Recipe of the Week!")
<p class="leading-relaxed p-4 my-8">
Our 'Recipe of the Week' is the cream of the crop! We handpick it by looking at what recipes
our community loves most. This isn't just about how many people view a recipe; it's also about
how many times it's been made, liked, reviewed, and its average rating, all combined to find
the true fan favorite of the week. It's our way of highlighting the best recipes that truly
resonate with our users!
</p>
<div class="flex items-center justify-center w-full">
@components.RecipeCardLarge(false)
</div>
</section>
<section class="w-full flex flex-col items-center justify-center my-8 py-4">
@components.BannerText("Recipe of the Week!")
<p class="leading-relaxed p-4 my-8">
Our 'Recipe of the Week' is the cream of the crop! We handpick it by looking at what recipes
our community loves most. This isn't just about how many people view a recipe; it's also about
how many times it's been made, liked, reviewed, and its average rating, all combined to find
the true fan favorite of the week. It's our way of highlighting the best recipes that truly
resonate with our users!
</p>
<div class="flex items-center justify-center w-full">
@components.RecipeCardLarge(false)
</div>
</section>
}
templ listsSection(loggedIn bool, viewed, made []domainRecipe.Recipe) {
<section class="w-full flex flex-col items-center justify-center my-8 py-4">
@components.BannerText("Take Another Look.")
<div class="w-full">
<h3 class="text-lg mt-8 mx-4">Recently viewed</h3>
if loggedIn {
<div class="flex overflow-x-auto gap-x-4 mx-4 my-4">
for _, recipe := range viewed {
@components.RecipeCardSmall(recipe)
}
@components.ContentCardSmall("View full history...", "/v1/web/history")
</div>
} else {
<div class="my-2 mx-4 text-gray-800">
<a class="underline" href={ domain.WEB_LOGIN }>
<p class="text-sm">Log in to view metrics.</p>
</a>
</div>
}
<h3 class="text-lg mt-8 mx-4">Make again</h3>
if loggedIn {
<div class="flex overflow-x-auto gap-x-4 mx-4 my-4">
for _, recipe := range made {
@components.RecipeCardSmall(recipe)
}
@components.ContentCardSmall("View full history...", "/v1/web/history")
</div>
} else {
<div class="my-2 mx-4 text-gray-800">
<a class="underline" href={ domain.WEB_LOGIN }>
<p class="text-sm">Log in to view metrics.</p>
</a>
</div>
}
</div>
</section>
<section class="w-full flex flex-col items-center justify-center my-8 py-4">
@components.BannerText("Take Another Look.")
<div class="w-full">
<h3 class="text-lg mt-8 mx-4">Recently viewed</h3>
if loggedIn {
<div class="flex overflow-x-auto gap-x-4 mx-4 my-4">
for _, recipe := range viewed {
@components.RecipeCardSmall(recipe)
}
@components.ContentCardSmall("View full history...", "/v1/web/history")
</div>
} else {
<div class="my-2 mx-4 text-gray-800">
<a class="underline" href={ domain.WEB_LOGIN }>
<p class="text-sm">Log in to view metrics.</p>
</a>
</div>
}
<h3 class="text-lg mt-8 mx-4">Make again</h3>
if loggedIn {
<div class="flex overflow-x-auto gap-x-4 mx-4 my-4">
for _, recipe := range made {
@components.RecipeCardSmall(recipe)
}
@components.ContentCardSmall("View full history...", "/v1/web/history")
</div>
} else {
<div class="my-2 mx-4 text-gray-800">
<a class="underline" href={ domain.WEB_LOGIN }>
<p class="text-sm">Log in to view metrics.</p>
</a>
</div>
}
</div>
</section>
}
templ ctaSection() {
<section
class="w-full flex flex-col items-center justify-center mt-16 py-8 md:py-12 bg-gradient-to-br from-blue-100 to-purple-100 text-center">
<h2 class="text-2xl md:text-3xl font-extrabold text-gray-800 mb-6 px-4">
Unleash Your Inner Chef!
</h2>
<p class="text-md md:text-lg text-gray-700 max-w-2xl mb-10 px-4 leading-relaxed">
Have a unique recipe idea? Want to share your culinary masterpiece with the world?
It's time to bring your creations to life!
</p>
<a href={ domain.WEB_CREATE } class="flex items-center justify-center
<section
class="w-full flex flex-col items-center justify-center mt-16 py-8 md:py-12 bg-gradient-to-br from-blue-100 to-purple-100 text-center"
>
<h2 class="text-2xl md:text-3xl font-extrabold text-gray-800 mb-6 px-4">
Unleash Your Inner Chef!
</h2>
<p class="text-md md:text-lg text-gray-700 max-w-2xl mb-10 px-4 leading-relaxed">
Have a unique recipe idea? Want to share your culinary masterpiece with the world?
It's time to bring your creations to life!
</p>
<a
href={ domain.WEB_CREATE }
class="flex items-center justify-center
bg-gradient-to-r from-blue-400 to-blue-600 text-white
px-12 py-5 rounded-full shadow-sm hover:shadow-md
transition-all duration-300 ease-in-out shadow-blue-700
text-lg md:text-2xl font-bold uppercase tracking-wide">
Create Your Recipe!
</a>
</section>
text-lg md:text-2xl font-bold uppercase tracking-wide"
>
Create Your Recipe!
</a>
</section>
}
templ HomePage(loggedIn bool, viewed, made []domainRecipe.Recipe) {
@components.Navbar("home")
<div class="w-full h-fit flex justify-center">
<div class="mx-2 md:mx-0 w-full md:w-1/2 md:pt-14 h-full border-l border-r border-gray-300 bg-white">
@introSection()
@searchSection()
@highlightSection(false)
@listsSection(loggedIn, viewed, made)
@ctaSection()
</div>
</div>
@components.Navbar("home")
<div class="w-full h-fit flex justify-center">
<div class="mx-2 md:mx-0 w-full md:w-1/2 md:pt-14 h-full border-l border-r border-gray-300 bg-white">
@introSection()
@searchSection()
@highlightSection(false)
@listsSection(loggedIn, viewed, made)
@ctaSection()
</div>
</div>
}

View File

@ -9,6 +9,7 @@
monospace;
--color-red-100: oklch(93.6% 0.032 17.717);
--color-red-500: oklch(63.7% 0.237 25.331);
--color-red-800: oklch(44.4% 0.177 26.899);
--color-green-500: oklch(72.3% 0.219 149.579);
--color-blue-50: oklch(97% 0.014 254.604);
--color-blue-100: oklch(93.2% 0.032 255.585);
@ -259,18 +260,12 @@
.z-20 {
z-index: 20;
}
.m-4 {
margin: calc(var(--spacing) * 4);
}
.mx-2 {
margin-inline: calc(var(--spacing) * 2);
}
.mx-4 {
margin-inline: calc(var(--spacing) * 4);
}
.mx-8 {
margin-inline: calc(var(--spacing) * 8);
}
.mx-auto {
margin-inline: auto;
}
@ -662,9 +657,6 @@
.bg-red-100 {
background-color: var(--color-red-100);
}
.bg-red-500 {
background-color: var(--color-red-500);
}
.bg-white {
background-color: var(--color-white);
}
@ -881,6 +873,9 @@
.text-red-500 {
color: var(--color-red-500);
}
.text-red-800 {
color: var(--color-red-800);
}
.text-white {
color: var(--color-white);
}