Compare commits

..

3 Commits

Author SHA1 Message Date
13596f7cb6 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
2025-07-21 20:23:54 -07:00
Hayden Hargreaves
0017d1a3d5 (FEAT): Created workflow.
This should work, pretty simple, just building and pushing to Dockerhub.
The server will handle listening and watching for docker changes.
2025-07-21 20:22:44 -07:00
Hayden Hargreaves
eec8ed00c3 (FEAT): Application has been dockerized.
The application can now be built, tailwind is built, AND templ
components are generated! Which means, we can no longer push the
tailwind generated CSS file OR the templ generated go files!

Plus, this is the first step towards deployment and CI/CD!
2025-07-21 20:14:03 -07:00
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 # Fetch stage
# Need a real way to build tailwind and build the templ files FROM golang:latest AS fetch-stage
FROM golang:1.24
COPY go.mod go.sum /app
WORKDIR /app WORKDIR /app
COPY . .
RUN go mod download 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 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 { func (s *Server) ConfigureAuth() *Server {
err := godotenv.Load(".env") err := godotenv.Load(".env")
if err != nil { 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") redirect_domain := os.Getenv("DOMAIN")
@ -80,7 +80,7 @@ func (s *Server) ConfigureAuth() *Server {
func (s *Server) ConnectDatabase() *Server { func (s *Server) ConnectDatabase() *Server {
err := godotenv.Load(".env") err := godotenv.Load(".env")
if err != nil { 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") var connUrl string = os.Getenv("DATABASE_URL")
@ -107,7 +107,7 @@ func (s *Server) Start() {
func (s *Server) Setup() *Server { func (s *Server) Setup() *Server {
err := godotenv.Load(".env") err := godotenv.Load(".env")
if err != nil { 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")) jwtSecret := []byte(os.Getenv("JWT_SECRET"))

View File

@ -4,39 +4,48 @@ import "strings"
import "github.com/haydenhargreaves/Potion/internal/domain/server" import "github.com/haydenhargreaves/Potion/internal/domain/server"
templ navLink(current, name, url string) { templ navLink(current, name, url string) {
<a href={ templ.SafeURL(url) } if strings.ToLower(current)==strings.ToLower(name) { <a
class="text-gray-700 border-b-2 border-blue-500 px-1 cursor-pointer" } else { href={ templ.SafeURL(url) }
class="text-gray-700 border-b-2 hover:border-blue-400 px-1 cursor-pointer border-white duration-150" }> 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 } { name }
</a> </a>
} }
templ dropdownLink(name, url string) { templ dropdownLink(name, url string) {
<a class="py-2" href={ templ.SafeURL(url) }> <a class="py-2" href={ templ.SafeURL(url) }>
{ name } { name }
</a> </a>
} }
templ listIcon(current, name, url string) { templ listIcon(current, name, url string) {
<a href={ templ.SafeURL(url) }> <a href={ templ.SafeURL(url) }>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512" <svg
if strings.ToLower(current) == strings.ToLower(name) { 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" class="h-4 text-blue-500"
} else { } else {
class="h-4 text-gray-700 hover:text-blue-400 duration-150" class="h-4 text-gray-700 hover:text-blue-400 duration-150"
} }
> >
<path fill="currentColor" <path
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"> fill="currentColor"
</path> 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> </svg>
</a> </a>
} }
templ Navbar(current string) { templ Navbar(current string) {
<nav class="block md:fixed w-full z-10"> <nav class="block md:fixed w-full z-10">
<div <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"> 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> <div>
<a href={ domain.WEB_HOME }> <a href={ domain.WEB_HOME }>
<p class="select-none">Potion</p> <p class="select-none">Potion</p>
@ -52,27 +61,32 @@ templ Navbar(current string) {
<div class="md:hidden grid place-content-center"> <div class="md:hidden grid place-content-center">
<button onclick="toggleMenu()" class="p-2"> <button onclick="toggleMenu()" class="p-2">
// carot // carot
<svg id="mobile-menu-button-carot" class="hidden size-5" xmlns="http://www.w3.org/2000/svg" <svg
viewBox="0 0 320 512"> id="mobile-menu-button-carot"
class="hidden size-5"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 320 512"
>
<path <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"> 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> ></path>
</svg> </svg>
// bars // bars
<svg id="mobile-menu-button-bars" class="size-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"> <svg id="mobile-menu-button-bars" class="size-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
<path fill="currentColor" <path
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"> fill="currentColor"
</path> 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> </svg>
</button> </button>
</div> </div>
@hamburgerMenu() @hamburgerMenu()
</div> </div>
</nav> </nav>
} }
templ hamburgerMenu() { templ hamburgerMenu() {
<script> <script>
function toggleMenu() { function toggleMenu() {
const menu = document.getElementById("mobile-menu-content"); const menu = document.getElementById("mobile-menu-content");
const carotButton = document.getElementById("mobile-menu-button-carot"); const carotButton = document.getElementById("mobile-menu-button-carot");
@ -93,13 +107,14 @@ templ hamburgerMenu() {
} }
} }
</script> </script>
<div id="mobile-menu-content" <div
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"> 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("Home", domain.WEB_HOME)
@dropdownLink("Favorites", domain.WEB_FAVORITES) @dropdownLink("Favorites", domain.WEB_FAVORITES)
@dropdownLink("Create", domain.WEB_CREATE) @dropdownLink("Create", domain.WEB_CREATE)
@dropdownLink("Profile", domain.WEB_PROFIlE) @dropdownLink("Profile", domain.WEB_PROFIlE)
@dropdownLink("Shopping List", domain.WEB_LIST) @dropdownLink("Shopping List", domain.WEB_LIST)
</div> </div>
} }

View File

@ -63,7 +63,7 @@ func navLink(current, name, url string) templ.Component {
var templ_7745c5c3_Var3 string var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(name) templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(name)
if templ_7745c5c3_Err != nil { 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@ -114,7 +114,7 @@ func dropdownLink(name, url string) templ.Component {
var templ_7745c5c3_Var6 string var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(name) templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(name)
if templ_7745c5c3_Err != nil { 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@ -211,7 +211,7 @@ func Navbar(current string) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }

View File

@ -5,13 +5,15 @@ import "github.com/haydenhargreaves/Potion/internal/domain/server"
import domainRecipe "github.com/haydenhargreaves/Potion/internal/domain/recipe" import domainRecipe "github.com/haydenhargreaves/Potion/internal/domain/recipe"
templ introSection() { templ introSection() {
<section class="w-full h-fit mb-16"> <section class="w-full h-fit mb-16">
<div class="relative"> <div class="relative">
<video class="" autoplay loop muted playsinline> <video class="" autoplay loop muted playsinline>
<source src="/v1/web/static/img/salmon_video.mp4" type="video/mp4" /> <source src="/v1/web/static/img/salmon_video.mp4" type="video/mp4"/>
</video> </video>
<h1 class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-center <h1
text-white text-3xl w-4/5 font-bold z-10"> 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 Discover Your Next Favorite Meal
</h1> </h1>
</div> </div>
@ -21,21 +23,21 @@ templ introSection() {
all at your fingertips. Find exactly what you're craving with our powerful search and intuitive filters, or 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. browse our trending dishes for fresh ideas.
</p> </p>
</section> </section>
} }
templ searchSection() { templ searchSection() {
<section class="w-full flex flex-col items-center justify-center my-8 py-4"> <section class="w-full flex flex-col items-center justify-center my-8 py-4">
@components.BannerText("Craving Something Specific?") @components.BannerText("Craving Something Specific?")
<div class="w-full md:w-3/4"> <div class="w-full md:w-3/4">
@components.SearchBar(domainRecipe.SearchFilters{}, true, false, false) @components.SearchBar(domainRecipe.SearchFilters{}, true, false, false)
</div> </div>
<div class="hidden" id="result-list"></div> <div class="hidden" id="result-list"></div>
</section> </section>
} }
templ highlightSection(liked bool) { templ highlightSection(liked bool) {
<section class="w-full flex flex-col items-center justify-center my-8 py-4"> <section class="w-full flex flex-col items-center justify-center my-8 py-4">
@components.BannerText("Recipe of the Week!") @components.BannerText("Recipe of the Week!")
<p class="leading-relaxed p-4 my-8"> <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 'Recipe of the Week' is the cream of the crop! We handpick it by looking at what recipes
@ -47,11 +49,11 @@ templ highlightSection(liked bool) {
<div class="flex items-center justify-center w-full"> <div class="flex items-center justify-center w-full">
@components.RecipeCardLarge(false) @components.RecipeCardLarge(false)
</div> </div>
</section> </section>
} }
templ listsSection(loggedIn bool, viewed, made []domainRecipe.Recipe) { templ listsSection(loggedIn bool, viewed, made []domainRecipe.Recipe) {
<section class="w-full flex flex-col items-center justify-center my-8 py-4"> <section class="w-full flex flex-col items-center justify-center my-8 py-4">
@components.BannerText("Take Another Look.") @components.BannerText("Take Another Look.")
<div class="w-full"> <div class="w-full">
<h3 class="text-lg mt-8 mx-4">Recently viewed</h3> <h3 class="text-lg mt-8 mx-4">Recently viewed</h3>
@ -85,12 +87,13 @@ templ listsSection(loggedIn bool, viewed, made []domainRecipe.Recipe) {
</div> </div>
} }
</div> </div>
</section> </section>
} }
templ ctaSection() { templ ctaSection() {
<section <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"> 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"> <h2 class="text-2xl md:text-3xl font-extrabold text-gray-800 mb-6 px-4">
Unleash Your Inner Chef! Unleash Your Inner Chef!
</h2> </h2>
@ -98,19 +101,22 @@ templ ctaSection() {
Have a unique recipe idea? Want to share your culinary masterpiece with the world? Have a unique recipe idea? Want to share your culinary masterpiece with the world?
It's time to bring your creations to life! It's time to bring your creations to life!
</p> </p>
<a href={ domain.WEB_CREATE } class="flex items-center justify-center <a
href={ domain.WEB_CREATE }
class="flex items-center justify-center
bg-gradient-to-r from-blue-400 to-blue-600 text-white bg-gradient-to-r from-blue-400 to-blue-600 text-white
px-12 py-5 rounded-full shadow-sm hover:shadow-md px-12 py-5 rounded-full shadow-sm hover:shadow-md
transition-all duration-300 ease-in-out shadow-blue-700 transition-all duration-300 ease-in-out shadow-blue-700
text-lg md:text-2xl font-bold uppercase tracking-wide"> text-lg md:text-2xl font-bold uppercase tracking-wide"
>
Create Your Recipe! Create Your Recipe!
</a> </a>
</section> </section>
} }
templ HomePage(loggedIn bool, viewed, made []domainRecipe.Recipe) { templ HomePage(loggedIn bool, viewed, made []domainRecipe.Recipe) {
@components.Navbar("home") @components.Navbar("home")
<div class="w-full h-fit flex justify-center"> <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"> <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() @introSection()
@searchSection() @searchSection()
@ -118,5 +124,5 @@ templ HomePage(loggedIn bool, viewed, made []domainRecipe.Recipe) {
@listsSection(loggedIn, viewed, made) @listsSection(loggedIn, viewed, made)
@ctaSection() @ctaSection()
</div> </div>
</div> </div>
} }

View File

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