diff --git a/.air.toml b/.air.toml new file mode 100644 index 0000000..bd9409d --- /dev/null +++ b/.air.toml @@ -0,0 +1,5 @@ +[build] +kill_delay = "10s" +delay = 500 +exclude_dir = ["public", "tmp", "vendor", "node_modules"] + diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..9baa39b --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,18 @@ +name: Release + +on: + push: + branches: + - main + +jobs: + deploy: + name: Deploy app + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: superfly/flyctl-actions/setup-flyctl@master + - run: flyctl deploy --remote-only + env: + FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..78eb3a6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules +.env +.DS_Store +tmp diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..6021861 Binary files /dev/null and b/bun.lockb differ diff --git a/db/db_client.go b/db/db_client.go new file mode 100644 index 0000000..e111ca9 --- /dev/null +++ b/db/db_client.go @@ -0,0 +1,29 @@ +package db + +import ( + "os" + + "github.com/jmoiron/sqlx" + _ "github.com/lib/pq" +) + +var Client *sqlx.DB + +func InitializeConnection() { + connectionString := os.Getenv("DATABASE_URL") + db, err := sqlx.Connect("postgres", connectionString) + + if err != nil { + panic(err) + } + + // Check if connection is alive + err = db.Ping() + + if err != nil { + panic(err) + } + + // Set the global DBClient variable to the db connection + Client = db +} diff --git a/fly.toml b/fly.toml new file mode 100644 index 0000000..86b9b02 --- /dev/null +++ b/fly.toml @@ -0,0 +1,22 @@ +# fly.toml app configuration file generated for movies-go on 2023-09-21T13:59:11+02:00 +# +# See https://fly.io/docs/reference/configuration/ for information about how to use this file. +# + +app = "movies-go" +primary_region = "arn" + +[build] + builder = "paketobuildpacks/builder:base" + buildpacks = ["gcr.io/paketo-buildpacks/go"] + +[env] + PORT = "8080" + +[http_service] + internal_port = 8080 + force_https = true + auto_stop_machines = true + auto_start_machines = true + min_machines_running = 0 + processes = ["app"] diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..98f93ba --- /dev/null +++ b/go.mod @@ -0,0 +1,26 @@ +module believer/movies + +go 1.21.0 + +require ( + github.com/gofiber/fiber/v2 v2.49.2 + github.com/gofiber/template/html/v2 v2.0.5 + github.com/jmoiron/sqlx v1.3.5 + github.com/lib/pq v1.10.9 +) + +require ( + github.com/andybalholm/brotli v1.0.5 // indirect + github.com/gofiber/template v1.8.2 // indirect + github.com/gofiber/utils v1.1.0 // indirect + github.com/google/uuid v1.3.1 // indirect + github.com/klauspost/compress v1.17.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/rivo/uniseg v0.4.4 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.50.0 // indirect + github.com/valyala/tcplisten v1.0.0 // indirect + golang.org/x/sys v0.12.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..1f643ed --- /dev/null +++ b/go.sum @@ -0,0 +1,43 @@ +github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= +github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/gofiber/fiber/v2 v2.49.2 h1:ONEN3/Vc+dUCxxDgZZwpqvhISgHqb+bu+isBiEyKEQs= +github.com/gofiber/fiber/v2 v2.49.2/go.mod h1:gNsKnyrmfEWFpJxQAV0qvW6l70K1dZGno12oLtukcts= +github.com/gofiber/template v1.8.2 h1:PIv9s/7Uq6m+Fm2MDNd20pAFFKt5wWs7ZBd8iV9pWwk= +github.com/gofiber/template v1.8.2/go.mod h1:bs/2n0pSNPOkRa5VJ8zTIvedcI/lEYxzV3+YPXdBvq8= +github.com/gofiber/template/html/v2 v2.0.5 h1:BKLJ6Qr940NjntbGmpO3zVa4nFNGDCi/IfUiDB9OC20= +github.com/gofiber/template/html/v2 v2.0.5/go.mod h1:RCF14eLeQDCSUPp0IGc2wbSSDv6yt+V54XB/+Unz+LM= +github.com/gofiber/utils v1.1.0 h1:vdEBpn7AzIUJRhe+CiTOJdUcTg4Q9RK+pEa0KPbLdrM= +github.com/gofiber/utils v1.1.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0= +github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= +github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= +github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= +github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.50.0 h1:H7fweIlBm0rXLs2q0XbalvJ6r0CUPFWK3/bB4N13e9M= +github.com/valyala/fasthttp v1.50.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA= +github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/main.go b/main.go new file mode 100644 index 0000000..227b3f2 --- /dev/null +++ b/main.go @@ -0,0 +1,27 @@ +package main + +import ( + "believer/movies/db" + "believer/movies/routes" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/template/html/v2" +) + +func main() { + db.InitializeConnection() + + engine := html.New("./views", ".html") + + app := fiber.New(fiber.Config{ + Views: engine, + ViewsLayout: "layouts/main", + }) + + app.Get("/", routes.FeedHandler) + app.Get("/movies/:id", routes.MovieHandler) + + app.Static("/public", "./public") + + app.Listen(":8080") +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..ec9a9a0 --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "movies", + "version": "1.0.0", + "description": "", + "main": "tailwind.config.js", + "scripts": { + "css": "tailwindcss -i ./styles.css -o ./public/styles.css --watch --minify", + "start": "godotenv air" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/believer/movies-go.git" + }, + "keywords": [], + "author": "Rickard Natt och Dag", + "license": "ISC", + "bugs": { + "url": "https://github.com/believer/movies-go/issues" + }, + "homepage": "https://github.com/believer/movies-go#readme", + "devDependencies": { + "tailwindcss": "^3.3.3" + } +} diff --git a/public/styles.css b/public/styles.css new file mode 100644 index 0000000..fa3cea2 --- /dev/null +++ b/public/styles.css @@ -0,0 +1 @@ +/*! tailwindcss v3.3.3 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.row-span-2{grid-row:span 2/span 2}.mx-auto{margin-left:auto;margin-right:auto}.mb-2{margin-bottom:.5rem}.mr-2{margin-right:.5rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.grid{display:grid}.aspect-auto{aspect-ratio:auto}.h-full{height:100%}.h-6{height:1.5rem}.w-full{width:100%}.w-6{width:1.5rem}.max-w-6xl{max-width:72rem}.max-w-4xl{max-width:56rem}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.grid-cols-\[300px_1fr\]{grid-template-columns:300px 1fr}.grid-rows-\[subgrid\]{grid-template-rows:subgrid}.flex-col{flex-direction:column}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-5{gap:1.25rem}.gap-8{gap:2rem}.gap-4{gap:1rem}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rounded-lg{border-radius:.5rem}.rounded-full{border-radius:9999px}.bg-slate-100{--tw-bg-opacity:1;background-color:rgb(241 245 249/var(--tw-bg-opacity))}.fill-current{fill:currentColor}.object-cover{-o-object-fit:cover;object-fit:cover}.object-center{-o-object-position:center;object-position:center}.py-8{padding-top:2rem;padding-bottom:2rem}.px-3{padding-left:.75rem;padding-right:.75rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.font-semibold{font-weight:600}.font-bold{font-weight:700}.text-slate-500{--tw-text-opacity:1;color:rgb(100 116 139/var(--tw-text-opacity))}.text-slate-700{--tw-text-opacity:1;color:rgb(51 65 85/var(--tw-text-opacity))}.text-slate-800{--tw-text-opacity:1;color:rgb(30 41 59/var(--tw-text-opacity))}.text-\[\#F5C518\]{--tw-text-opacity:1;color:rgb(245 197 24/var(--tw-text-opacity))}.shadow-lg{--tw-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a;--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.movie-poster-animation{view-transition-name:var(--poster-transition)} \ No newline at end of file diff --git a/routes/routes.go b/routes/routes.go new file mode 100644 index 0000000..09df3d0 --- /dev/null +++ b/routes/routes.go @@ -0,0 +1,87 @@ +package routes + +import ( + "believer/movies/db" + "database/sql" + "fmt" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/lib/pq" +) + +type CastAndCrew struct { + Name string `db:"name"` + Job string `db:"job"` +} + +type Movie struct { + Cast []CastAndCrew `db:"cast"` + CreatedAt time.Time `db:"created_at"` + Genres pq.StringArray `db:"genres"` + Id int `db:"id"` + ImdbId string `db:"imdb_id"` + ImdbRating sql.NullFloat64 `db:"imdb_rating"` + Overview string `db:"overview"` + Poster string `db:"poster"` + ReleaseDate time.Time `db:"release_date"` + Runtime int `db:"runtime"` + Tagline string `db:"tagline"` + Title string `db:"title"` + UpdatedAt time.Time `db:"updated_at"` + WatchedAt time.Time `db:"watched_at"` +} + +// Format runtime in hours and minutes from minutes +func (m Movie) RuntimeFormatted() string { + hours := m.Runtime / 60 + minutes := m.Runtime % 60 + + return fmt.Sprintf("%dh %dm", hours, minutes) +} + +func FeedHandler(c *fiber.Ctx) error { + var movies []Movie + + err := db.Client.Select(&movies, ` +SELECT m.id, m.title, m.poster, m.release_date, s.date AS watched_at +FROM public.seen AS s + INNER JOIN public.movie AS m ON m.id = s.movie_id +WHERE + user_id = 1 + AND EXTRACT(YEAR FROM s.date) = EXTRACT(YEAR FROM CURRENT_DATE) +ORDER BY s.date DESC +`) + + if err != nil { + panic(err) + } + + return c.Render("index", fiber.Map{ + "Movies": movies, + }) +} + +func MovieHandler(c *fiber.Ctx) error { + var movie Movie + + err := db.Client.Get(&movie, ` +SELECT + m.*, + ARRAY_AGG(g.name) AS genres +FROM + public.movie AS m + INNER JOIN public.movie_genre AS mg ON mg.movie_id = m.id + INNER JOIN public.genre AS g ON g.id = mg.genre_id +WHERE m.id = $1 +GROUP BY 1 +`, c.Params("id")) + + if err != nil { + panic(err) + } + + return c.Render("movie", fiber.Map{ + "Movie": movie, + }) +} diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..525219f --- /dev/null +++ b/styles.css @@ -0,0 +1,7 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +.movie-poster-animation { + view-transition-name: var(--poster-transition); +} diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..73bce1d --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,8 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ["./views/**/*.html", "./**/*.go"], + theme: { + extend: {}, + }, + plugins: [], +}; diff --git a/views/index.html b/views/index.html new file mode 100644 index 0000000..4893d72 --- /dev/null +++ b/views/index.html @@ -0,0 +1,24 @@ +
+
    + {{ range .Movies }} +
  1. + + {{ .Title }} poster + +
    +

    + {{ .Title }} +

    +

    + {{ .ReleaseDate.Format "2006" }} - {{ .WatchedAt.Format "Jan 02" }} +

    +
    +
  2. + {{ end }} +
+
diff --git a/views/layouts/main.html b/views/layouts/main.html new file mode 100644 index 0000000..28a055a --- /dev/null +++ b/views/layouts/main.html @@ -0,0 +1,17 @@ + + + + + Movies + + + + + +
{{embed}}
+ + diff --git a/views/movie.html b/views/movie.html new file mode 100644 index 0000000..a2a98fe --- /dev/null +++ b/views/movie.html @@ -0,0 +1,46 @@ +
+ Back to movies +
+ {{ .Movie.Title }} poster +
+

{{ .Movie.Title }}

+
    +
  • {{ .Movie.ReleaseDate.Format "2006-01-02" }}
  • +
  • {{ .Movie.RuntimeFormatted }}
  • +
+ +

{{ .Movie.Overview }}

+ +
    + {{ range .Movie.Genres }} +
  • + {{ . }} +
  • + {{end}} +
+ + + + + + +
+
+