This commit is contained in:
Martin Donnelly 2024-03-19 11:44:11 +00:00
commit 491be9c1b1
21 changed files with 1700 additions and 0 deletions

200
.gitignore vendored Normal file
View File

@ -0,0 +1,200 @@
### GoLand+all template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### Go template
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
### GoLand+iml template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
# AWS User-specific
# Generated files
# Sensitive or high-churn files
# Gradle
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
# Mongo Explorer plugin
# File-based project format
# IntelliJ
# mpeltonen/sbt-idea plugin
# JIRA plugin
# Cursive Clojure plugin
# SonarLint plugin
# Crashlytics plugin (for Android Studio and IntelliJ)
# Editor-based Rest Client
# Android studio 3.1+ serialized cache file
### GoLand template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
# AWS User-specific
# Generated files
# Sensitive or high-churn files
# Gradle
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
# Mongo Explorer plugin
# File-based project format
# IntelliJ
# mpeltonen/sbt-idea plugin
# JIRA plugin
# Cursive Clojure plugin
# SonarLint plugin
# Crashlytics plugin (for Android Studio and IntelliJ)
# Editor-based Rest Client
# Android studio 3.1+ serialized cache file

35
Makefile Normal file
View File

@ -0,0 +1,35 @@
PROJECT = menuserver
VERSION = 3.0
ECR_REPO = git.caliban.io/martin
# APP_IMAGE = 482681734622.dkr.ecr.eu-west-1.amazonaws.com/$(PROJECT):$(VERSION)
APP_IMAGE = $(ECR_REPO)/$(PROJECT):$(VERSION)
# APP_IMAGE = $(PROJECT):$(VERSION)
NO_CACHE = true
.PHONY: build
build:
#CC=/usr/local/musl/bin/musl-gcc go build --ldflags '-linkmode external -extldflags "-static"' server.go
GCO_ENABLED=0 GOOS=linux go build -o ${PROJECT} server.go
# docker build ./docker/. -t $(APP_IMAGE) --build-arg VERSION=$(VERSION) --no-cache=$(NO_CACHE) --compress=true
docker build --platform linux/amd64 --no-cache -force-rm --tag ${APP_IMAGE} --file ./docker/Dockerfile .
#push docker image to registry
.PHONY: push
push: build
docker push $(APP_IMAGE)
#push docker image to registry
.PHONY: run
run: build
docker run $(APP_IMAGE)
ver:
@echo '$(VERSION)'
#echo $ERSION
.PHONY: ver

BIN
db/menu.db Normal file

Binary file not shown.

30
dbconnection/menu.go Normal file
View File

@ -0,0 +1,30 @@
package dbconnection
type Menu struct {
_ID int64 `json:"_id"`
Name string `json:"name"`
Url string `json:"url"`
Md string `json:"md"`
Short string `json:"short"`
Hash string `json:"hash"`
Meat int64 `json:"meat"`
Mealtype int64 `json:"mealtype"`
Lastused int64 `json:"lastused"`
}
type MenuStrings struct {
_ID int64 `json:"_id"`
Name string `json:"name"`
Url string `json:"url"`
Md string `json:"md"`
Short string `json:"short"`
Hash string `json:"hash"`
Meat string `json:"meat"`
Mealtype string `json:"mealtype"`
Lastused int64 `json:"lastused"`
}
type InsertedType struct {
Msg string `json:"msg"`
_ID string `json:"_id"`
}

View File

@ -0,0 +1,253 @@
package dbconnection
import (
"context"
"database/sql"
"log"
"strconv"
)
type SQLiteRepository struct {
db *sql.DB
}
func NewSQLiteRepository(db *sql.DB) *SQLiteRepository {
return &SQLiteRepository{
db: db,
}
}
func (r *SQLiteRepository) All() ([]Menu, error) {
rows, err := r.db.Query("SELECT _id, short, hash, name, meat, mealtype FROM menu")
if err != nil {
return nil, err
}
defer rows.Close()
var all []Menu
for rows.Next() {
var menu Menu
if err := rows.Scan(&menu._ID, &menu.Short, &menu.Hash, &menu.Name, &menu.Meat, &menu.Mealtype); err != nil {
return nil, err
}
all = append(all, menu)
}
return all, nil
}
func (r *SQLiteRepository) GetOneShort(ShortCode string) (Menu, error) {
rows, err := r.db.Query("select md from menu where short = ?", ShortCode)
var empty Menu
if err != nil {
return empty, err
}
defer rows.Close()
var item Menu
for rows.Next() {
var menu Menu
if err := rows.Scan(&menu.Md); err != nil {
return empty, err
}
item = menu
}
return item, nil
}
func (r *SQLiteRepository) GetOneHash(Hash string) (Menu, error) {
log.Printf("Get one hash: %s\n", Hash)
rows, err := r.db.Query("select * from menu where hash = ?", Hash)
var empty Menu
if err != nil {
log.Println("GetOneHash query error")
return empty, err
}
defer rows.Close()
var item Menu
for rows.Next() {
var menu Menu
if err := rows.Scan(&menu._ID, &menu.Name, &menu.Url, &menu.Md, &menu.Short, &menu.Hash, &menu.Meat, &menu.Mealtype, &menu.Lastused); err != nil {
return empty, err
}
item = menu
}
log.Printf("%+v\n", item)
return item, nil
}
func (r *SQLiteRepository) InsertOne(newitem MenuStrings) (InsertedType, error) {
log.Printf("Insert One: %s\n", newitem)
stmt, err := r.db.Prepare("INSERT INTO menu(name, url, md, short, hash, meat, mealtype, lastused) VALUES (?,?,?,?,?,?,?,?)")
if err != nil {
log.Fatal(err)
}
defer stmt.Close()
meat, _ := strconv.ParseInt(newitem.Meat, 10, 64)
mealtype, _ := strconv.ParseInt(newitem.Meat, 10, 64)
lastused, _ := strconv.ParseInt(newitem.Meat, 10, 64)
b, err := stmt.Exec(newitem.Name, newitem.Url, newitem.Md, newitem.Short, newitem.Hash, meat, mealtype, lastused)
if err != nil {
log.Println(err)
}
var returnVal InsertedType
lastID, err := b.LastInsertId()
if err == nil {
returnVal = InsertedType{
Msg: "Row inserted",
_ID: strconv.FormatInt(lastID, 10),
}
log.Printf("B:%+v\n", b)
}
return returnVal, err
}
func (r *SQLiteRepository) GetRandom(timestamp int64) ([]Menu, error) {
ts := strconv.FormatInt(timestamp, 10)
rows, err := r.db.Query("SELECT _id, short, hash, name, meat, mealtype FROM menu where mealtype =1 and lastused<? order by RANDOM()", ts)
if err != nil {
return nil, err
}
defer rows.Close()
var all []Menu
for rows.Next() {
var menu Menu
if err := rows.Scan(&menu._ID, &menu.Short, &menu.Hash, &menu.Name, &menu.Meat, &menu.Mealtype); err != nil {
return nil, err
}
all = append(all, menu)
}
return all, nil
}
func (r *SQLiteRepository) GetRandomSoup(timestamp int64) (Menu, error) {
var empty Menu
ts := strconv.FormatInt(timestamp, 10)
rows, err := r.db.Query("SELECT _id, short, hash, name, meat, mealtype FROM menu where mealtype =2 and lastused<? order by RANDOM() LIMIT 1", ts)
if err != nil {
return empty, err
}
defer rows.Close()
var item Menu
for rows.Next() {
var menu Menu
if err := rows.Scan(&menu._ID, &menu.Short, &menu.Hash, &menu.Name, &menu.Meat, &menu.Mealtype); err != nil {
return empty, err
}
item = menu
}
return item, nil
}
func (r *SQLiteRepository) UpdateTimeStamps(updateRecs []Menu, timestamp int64) error {
log.Printf("Updating timestamps to : %+v", timestamp)
stmt, err := r.db.Prepare("UPDATE menu SET lastused = ? WHERE _id = ?")
if err != nil {
log.Fatal(err)
}
defer stmt.Close()
for _, item := range updateRecs {
b, err := stmt.Exec(timestamp, item._ID)
if err != nil {
log.Println(err)
}
rowsAffected, err := b.RowsAffected()
if err != nil {
log.Printf("Rows affected:%+v\n", rowsAffected)
}
}
return nil
}
func (r *SQLiteRepository) UpdateTimeStampsTransaction(updateRecs []Menu, timestamp int64) error {
log.Printf("Update timestamps to : %+v", timestamp)
ctx := context.Background()
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
log.Fatal(err)
}
for _, item := range updateRecs {
log.Println(item)
_, err = tx.ExecContext(ctx, "UPDATE menu SET lastused = ? WHERE _id = ?", item._ID, timestamp)
if err != nil {
log.Fatal(err)
// Incase we find any error in the query execution, rollback the transaction
tx.Rollback()
return err
}
}
err = tx.Commit()
if err != nil {
log.Fatal(err)
} else {
log.Println("Records updated")
}
// UPDATE menu SET lastused = $lastused WHERE _id = $in
return err
}

1
dist/build/bundle.css vendored Executable file
View File

@ -0,0 +1 @@
.filterBar.svelte-17lzm0a{background:var(--medium-color);margin-bottom:1rem;padding:10px 5px}.recipeItem.svelte-qibu9a{display:flex;padding:0.1rem;border-bottom:1px #ccc dotted}.recipeItem.svelte-qibu9a:nth-of-type(odd){background-color:rgba(0, 0, 0, 0.04)}.listItemSix.svelte-qibu9a{flex:6}.listItemThree.svelte-qibu9a{flex:3}.chicken.svelte-qibu9a{background:#8e5241;color:#fff}.beef.svelte-qibu9a{background:#d72414;color:#fff}.pork.svelte-qibu9a{background:#ef96d9;color:#fff}.fish.svelte-qibu9a{background:#005ba0;color:#fff}.egg.svelte-qibu9a{background:#fbc003;color:#000}.vegetable.svelte-qibu9a{background:#00903e;color:#fff}

14
dist/build/bundle.css.map vendored Normal file
View File

@ -0,0 +1,14 @@
{
"version": 3,
"file": "bundle.css",
"sources": [
"../../FilterBar.svelte",
"../../RecipeItem.svelte"
],
"sourcesContent": [
"<script>\n import { state } from '../store/store';\n import { onMount } from 'svelte';\n\n let meatVal = '0';\n let mealVal = '0';\n const meals = ['', '1', '2', '128'];\n\n function updateMeat(event) {\n const newVal = event.target.value;\n state.updateMeatFilter(newVal);\n }\n\n function updateMeal(event) {\n const newVal = event.target.value;\n state.updateMealFilter(newVal);\n }\n\n onMount(() => {\n\n setTimeout(() => {\n\n const mtVal = document.getElementById('meatVal').selectedIndex;\n const mlVal = document.getElementById('mealVal').selectedIndex;\n\n if (mtVal !== 0) {\n meatVal = mtVal.toString(10);\n state.updateMeatFilter(meatVal);\n }\n\n if (mlVal !== 0) {\n mealVal = meals[mlVal];\n state.updateMealFilter(mealVal);\n }\n\n }, 25);\n });\n\n</script>\n\n<style>\n .filterBar {\n background: var(--medium-color);\n margin-bottom: 1rem;\n padding: 10px 5px;\n }\n</style>\n\n<div class=\"container\">\n <div class=\"filterBar grid-4\">\n <select id=\"meatVal\" on:change={updateMeat} bind:value={meatVal}>\n <option value=\"0\">All</option>\n <option value=\"1\">Chicken</option>\n <option value=\"2\">Beef</option>\n <option value=\"3\">Pork</option>\n <option value=\"4\">Fish</option>\n <option value=\"5\">Egg</option>\n <option value=\"6\">Vegetable</option>\n </select>\n\n <select id=\"mealVal\" on:change={updateMeal} bind:value={mealVal}>\n <option value=\"0\">All</option>\n <option value=\"1\">Mains</option>\n <option value=\"2\">Soups</option>\n <option value=\"128\">Notes</option>\n </select>\n </div>\n\n</div>\n",
"<script>\n import { state } from '../store/store';\n\n export let recipeItem = {};\n\n let meatClass;\n let meatText;\n let url;\n const meats = ['x', 'Chicken', 'Beef', 'Pork', 'Fish', 'Egg', 'Vegetable'];\n\n $:{\n meatText = meats[recipeItem.meat];\n meatClass = (recipeItem.meat === '') ? '' : meats[recipeItem.meat].toLowerCase();\n url = `/view/${recipeItem.short}`;\n }\n\n function editRecipe(hash) {\n state.editRecipe(hash);\n }\n\n</script>\n\n<style>\n .recipeItem {\n display: flex;\n padding: 0.1rem;\n border-bottom: 1px #ccc dotted;\n }\n\n .recipeItem:nth-of-type(odd) {\n background-color: rgba(0, 0, 0, 0.04);\n }\n\n .listItemSix {\n\n flex: 6;\n }\n\n .listItemThree {\n\n flex: 3;\n }\n\n .chicken {\n background: #8e5241;\n color: #fff;\n }\n\n .beef {\n background: #d72414;\n color: #fff;\n }\n\n .pork {\n background: #ef96d9;\n color: #fff;\n }\n\n .fish {\n background: #005ba0;\n color: #fff;\n }\n\n .egg {\n background: #fbc003;\n color: #000;\n }\n\n .vegetable {\n background: #00903e;\n color: #fff;\n }\n</style>\n\n<div class=\"recipeItem\">\n <div class=\"listItemSix\"><a href={url}>{recipeItem.name}</a></div>\n <div class=\"listItemThree\">\n {#if recipeItem.mealtype ===2}\n <span class=\"badge badge-light\">Soup</span>\n {:else if recipeItem.mealtype===128}\n <span class=\"badge badge-dark\">Note</span>\n\n {/if}\n <span class=\"badge {meatClass}\">{meatText}</span>\n </div>\n <div class=\"listItemThree all-center\">\n <button class=\"btn btn-primary btn-sm\" type=\"button\" on:click={editRecipe(recipeItem.hash)}>Edit</button>\n </div>\n</div>\n"
],
"names": [],
"mappings": "AAyCI,UAAU,eAAC,CAAC,AACR,UAAU,CAAE,IAAI,cAAc,CAAC,CAC/B,aAAa,CAAE,IAAI,CACnB,OAAO,CAAE,IAAI,CAAC,GAAG,AACrB,CAAC;ACtBD,WAAW,cAAC,CAAC,AACT,OAAO,CAAE,IAAI,CACb,OAAO,CAAE,MAAM,CACf,aAAa,CAAE,GAAG,CAAC,IAAI,CAAC,MAAM,AAClC,CAAC,AAED,yBAAW,aAAa,GAAG,CAAC,AAAC,CAAC,AAC1B,gBAAgB,CAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,AACzC,CAAC,AAED,YAAY,cAAC,CAAC,AAEV,IAAI,CAAE,CAAC,AACX,CAAC,AAED,cAAc,cAAC,CAAC,AAEZ,IAAI,CAAE,CAAC,AACX,CAAC,AAED,QAAQ,cAAC,CAAC,AACN,UAAU,CAAE,OAAO,CACnB,KAAK,CAAE,IAAI,AACf,CAAC,AAED,KAAK,cAAC,CAAC,AACH,UAAU,CAAE,OAAO,CACnB,KAAK,CAAE,IAAI,AACf,CAAC,AAED,KAAK,cAAC,CAAC,AACH,UAAU,CAAE,OAAO,CACnB,KAAK,CAAE,IAAI,AACf,CAAC,AAED,KAAK,cAAC,CAAC,AACH,UAAU,CAAE,OAAO,CACnB,KAAK,CAAE,IAAI,AACf,CAAC,AAED,IAAI,cAAC,CAAC,AACF,UAAU,CAAE,OAAO,CACnB,KAAK,CAAE,IAAI,AACf,CAAC,AAED,UAAU,cAAC,CAAC,AACR,UAAU,CAAE,OAAO,CACnB,KAAK,CAAE,IAAI,AACf,CAAC"
}

2
dist/build/bundle.js vendored Executable file

File diff suppressed because one or more lines are too long

1
dist/build/bundle.js.map vendored Normal file

File diff suppressed because one or more lines are too long

BIN
dist/favicon.png vendored Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

503
dist/global.css vendored Normal file
View File

@ -0,0 +1,503 @@
@import url('https://fonts.googleapis.com/css?family=Roboto');
/* Global Styles */
:root {
--primary-color: #64B5F6;
--dark-color: #333333;
--light-color: #f4f4f4;
--danger-color: #dc3545;
--success-color: #28a745;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Roboto', sans-serif;
font-size: 1rem;
line-height: 1.6;
background-color: #fff;
color: #333;
}
a {
color: var(--primary-color);
text-decoration: none;
}
a:hover {
color: #666;
}
ul {
list-style: none;
}
img {
width: 100%;
}
.dataRow {
cursor: pointer;
}
/* Utilities */
.container {
max-width: 1100px;
margin: auto;
overflow: hidden;
padding: 0 2rem;
}
/* Text Styles*/
.x-large {
font-size: 4rem;
line-height: 1.2;
margin-bottom: 1rem;
}
.large {
font-size: 3rem;
line-height: 1.2;
margin-bottom: 1rem;
}
.lead {
font-size: 1.5rem;
margin-bottom: 1rem;
}
.text-center {
text-align: center;
}
.text-primary {
color: var(--primary-color);
}
.text-dark {
color: var(--dark-color);
}
.text-success {
color: var(--success-color);
}
.text-danger {
color: var(--danger-color);
}
.text-center {
text-align: center;
}
.text-right {
text-align: right;
}
.text-left {
text-align: left;
}
/* Center All */
.all-center {
display: flex;
flex-direction: column;
width: 100%;
margin: auto;
justify-content: center;
align-items: center;
text-align: center;
}
/* Cards */
.card {
padding: 1rem;
border: #ccc 1px dotted;
margin: 0.7rem 0;
}
/* List */
.list {
margin: 0.5rem 0;
}
.list li {
padding-bottom: 0.3rem;
}
/* Padding */
.p {
padding: 0.5rem;
}
.p-1 {
padding: 1rem;
}
.p-2 {
padding: 2rem;
}
.p-3 {
padding: 3rem;
}
.py {
padding: 0.5rem 0;
}
.py-1 {
padding: 1rem 0;
}
.py-2 {
padding: 2rem 0;
}
.py-3 {
padding: 3rem 0;
}
/* Margin */
.m {
margin: 0.5rem;
}
.m-1 {
margin: 1rem;
}
.m-2 {
margin: 2rem;
}
.m-3 {
margin: 3rem;
}
.my {
margin: 0.5rem 0;
}
.my-1 {
margin: 1rem 0;
}
.my-2 {
margin: 2rem 0;
}
.my-3 {
margin: 3rem 0;
}
/* Grid */
.grid-2 {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-gap: 1rem;
}
.grid-3 {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: 1rem;
}
.grid-4 {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-gap: 1rem;
}
.btn {
display: inline-block;
background: var(--light-color);
color: #333;
padding: 0.4rem 1.3rem;
font-size: 1rem;
border: none;
cursor: pointer;
margin-right: 0.5rem;
transition: opacity 0.2s ease-in;
outline: none;
}
.btn-link {
background: none;
padding: 0;
margin: 0;
}
.btn-block {
display: block;
width: 100%;
}
.btn-sm {
font-size: 0.8rem;
padding: 0.3rem 1rem;
margin-right: 0.2rem;
}
.badge {
display: inline-block;
font-size: 0.6rem;
padding: 0.1rem 0.4rem;
text-align: center;
margin: 0.3rem;
background: var(--light-color);
color: #333;
border-radius: 3px;
}
.alert {
padding: 0.7rem;
margin: 1rem 0;
opacity: 0.9;
background: var(--light-color);
color: #333;
}
.btn-primary,
.bg-primary,
.badge-primary,
.alert-primary {
background: var(--primary-color);
color: #fff;
}
.btn-light,
.bg-light,
.badge-light,
.alert-light {
background: var(--light-color);
color: #333;
}
.btn-dark,
.bg-dark,
.badge-dark,
.alert-dark {
background: var(--dark-color);
color: #fff;
}
.btn-danger,
.bg-danger,
.badge-danger,
.alert-danger {
background: var(--danger-color);
color: #fff;
}
.btn-success,
.bg-success,
.badge-success,
.alert-success {
background: var(--success-color);
color: #fff;
}
.btn-white,
.bg-white,
.badge-white,
.alert-white {
background: #fff;
color: #333;
border: #ccc solid 1px;
}
.btn:disabled {
cursor: not-allowed;
pointer-events: none;
opacity: 0.60;
-webkit-box-shadow: none;
box-shadow: none;
}
.btn:enabled:hover {
opacity: 0.8;
}
.bg-light,
.badge-light {
border: #ccc solid 1px;
}
.round-img {
border-radius: 50%;
}
/* Forms */
input {
margin: 1.2rem 0;
}
.form-text {
display: block;
margin-top: 0.3rem;
color: #888;
}
input[type='text'],
input[type='email'],
input[type='password'],
input[type='date'],
select,
textarea {
display: block;
width: 100%;
padding: 0.4rem;
/*font-size: 1.2rem;*/
border: 1px solid #ccc;
}
input[type='submit'],
button {
font: inherit;
}
table th,
table td {
padding: 1rem;
text-align: left;
}
table th {
background: var(--light-color);
}
/* Navbar */
.navbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.7rem 2rem;
z-index: 1;
width: 100%;
opacity: 0.9;
margin-bottom: 1rem;
}
.navbar ul {
display: flex;
}
.navbar a {
color: #fff;
padding: 0.45rem;
margin: 0 0.25rem;
}
.navbar a:hover {
color: var(--light-color);
}
.navbar .welcome span {
margin-right: 0.6rem;
}
/* Mobile Styles */
@media (max-width: 700px) {
.hide-sm {
display: none;
}
.grid-2,
.grid-3,
.grid-4 {
grid-template-columns: 1fr;
}
/* Text Styles */
.x-large {
font-size: 3rem;
}
.large {
font-size: 2rem;
}
.lead {
font-size: 1rem;
}
/* Navbar */
.navbar {
display: block;
text-align: center;
}
.navbar ul {
text-align: center;
justify-content: center;
}
}
:root {
--primary-color: #64B5F6;
--dark-color: #333333;
--light-color: #f4f4f4;
--danger-color: #dc3545;
--success-color: #28a745;
--medium-color: #999999;
}
.table-responsive {
display: block;
overflow-x: auto;
width: 100%;
}
.cardV2 {
border-radius: 4px;
background-color: #fff;
box-shadow: 0 0 4px 0 rgba(0,0,0,.14), 0 3px 4px 0 rgba(0,0,0,.12), 0 1px 5px 0 rgba(0,0,0,.2);
/*display: flex;
flex-direction: column;*/
min-width: 0;
/*position: relative;
word-wrap: break-word;*/
}
table {
max-width: 100%;
width: 100%;
border: 0;
margin-bottom: 1rem;
border-collapse: collapse;
}
tr {
border-top: 1px solid #ccc;
}
tbody tr:nth-of-type(odd){
background-color: rgba(0,0,0,0.04);
}
tbody td {
border-top: 1px solid #e1e1e1;
}
.modalWindow {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: rgba(0,0,0,0.2);
z-index: 99999;
opacity:0;
pointer-events: none;
text-align:center;
}
.modalWindow:target {
opacity:1;
pointer-events: auto;
}
.modalWindow > div {
width: 500px;
position: relative;
margin: 10% auto;
background: #fff;
}

18
dist/index.html vendored Normal file
View File

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width,initial-scale=1'>
<title>Menus</title>
<link rel='icon' type='image/png' href='/favicon.png'>
<link rel='stylesheet' href='/global.css'>
<link rel='stylesheet' href='/build/bundle.css'>
<script defer src='/build/bundle.js'></script>
</head>
<!-- svelte -->
<body>
</body>
</html>

26
docker/Dockerfile Normal file
View File

@ -0,0 +1,26 @@
FROM debian:12-slim
# FROM alpine:latest
WORKDIR /app
RUN mkdir -p /app/{dist,db}
COPY ./menuserver /app/
COPY ./dist /app/dist
# Need the following to get a go app to run inside a docker container
# as per: https://www.fairlyusefulcode.co.uk/post/go-alpine-linux/ -- DEAD!!!
# RUN apk upgrade musl
# RUN apk add gcompat
# RUN mkdir /lib64 && ln -s /lib/libc.musl-x86_64.so.1 /lib64/ld-linux-x86-64.so.2
# RUN ln -s /lib/libc.musl-x86_64.so.1 /lib64/ld-linux-x86-64.so.2
RUN chmod +x /app/menuserver
EXPOSE 3000
CMD [ "/app/menuserver"]

30
go.mod Normal file
View File

@ -0,0 +1,30 @@
module menuserver
go 1.22.0
require (
github.com/Joker/jade v1.1.3
github.com/gofiber/fiber/v2 v2.52.2
github.com/gofiber/template/html/v2 v2.1.1
github.com/mattn/go-sqlite3 v1.14.22
github.com/robfig/cron/v3 v3.0.0
github.com/stoewer/go-strcase v1.3.0
github.com/yuin/goldmark v1.7.0
)
require (
github.com/andybalholm/brotli v1.0.5 // indirect
github.com/gofiber/template v1.8.3 // indirect
github.com/gofiber/utils v1.1.0 // indirect
github.com/google/uuid v1.5.0 // indirect
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible // 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.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.51.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/sys v0.15.0 // indirect
)

91
go.sum Normal file
View File

@ -0,0 +1,91 @@
github.com/Joker/hpp v1.0.0 h1:65+iuJYdRXv/XyN62C1uEmmOx3432rNG/rKlX6V7Kkc=
github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY=
github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk=
github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM=
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gofiber/fiber/v2 v2.52.2 h1:b0rYH6b06Df+4NyrbdptQL8ifuxw/Tf2DgfkZkDaxEo=
github.com/gofiber/fiber/v2 v2.52.2/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
github.com/gofiber/template v1.8.3 h1:hzHdvMwMo/T2kouz2pPCA0zGiLCeMnoGsQZBTSYgZxc=
github.com/gofiber/template v1.8.3/go.mod h1:bs/2n0pSNPOkRa5VJ8zTIvedcI/lEYxzV3+YPXdBvq8=
github.com/gofiber/template/html/v2 v2.1.1 h1:QEy3O3EBkvwDthy5bXVGUseOyO6ldJoiDxlF4+MJiV8=
github.com/gofiber/template/html/v2 v2.1.1/go.mod h1:2G0GHHOUx70C1LDncoBpe4T6maQbNa4x1CVNFW0wju0=
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.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA=
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A=
github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
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.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/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.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/robfig/cron/v3 v3.0.0 h1:kQ6Cb7aHOHTSzNVNEhmp8EcWKLb4CbiMW9h9VyIhO4E=
github.com/robfig/cron/v3 v3.0.0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs=
github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
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.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.7.0 h1:EfOIvIMZIzHdB/R/zVrikYLPPwJlfMcNczJFMs1m6sA=
github.com/yuin/goldmark v1.7.0/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

162
jobhandler/jobhandler.go Normal file
View File

@ -0,0 +1,162 @@
package jobhandler
import (
"bytes"
"github.com/jordan-wright/email"
"html/template"
"log"
"menuserver/dbconnection"
"net/smtp"
"time"
)
var (
from = "Aida <aida@caliban.io>"
login = "aida@caliban.io"
// recipients = []string{"Martin <martind2000@gmail.com>", "Jessica <ing.arvid@gmail.com>"}
recipients = []string{"Martin <martind2000@gmail.com>"}
hostname = "mail.caliban.io"
)
func DoJob(r *dbconnection.SQLiteRepository) {
var data PugData
var store []MenuEntry
var menuItems []dbconnection.Menu
srcURL := "http://menu.lan"
var types = [7]int{0, 0, 0, 0, 0, 0, 0}
limit := 2
log.Printf("Doing Job....")
now := time.Now()
daystart := (now.Unix() / 86400) * 86400
data.Ts = now.Format("Monday, January 2, 2006")
items, err := r.GetRandom(now.Unix())
if err != nil {
log.Fatal(err)
}
for _, item := range items {
if types[item.Meat] < limit && len(store) < 5 {
entry := MenuEntry{
Url: srcURL + "/view/" + item.Short,
Meat: item.Meat,
Name: item.Name,
}
store = append(store, entry)
types[item.Meat] = types[item.Meat] + 1
menuItems = append(menuItems, item)
}
}
data.Menu = store
soupItem, err := r.GetRandomSoup(now.Unix())
if err != nil {
log.Fatal(err)
}
soupEntry := MenuEntry{
Url: srcURL + "/view/" + soupItem.Short,
Meat: soupItem.Meat,
Name: soupItem.Name,
}
data.Soup = soupEntry
menuItems = append(menuItems, soupItem)
outputMsg := generateHTML(data)
subject := "Suggestions 🍽️ - " + data.Ts
sendSMTP(outputMsg, subject)
updateTimestamps(r, menuItems, daystart)
}
func generateHTML(data PugData) string {
var buf bytes.Buffer
t := template.New("mail")
t, err := t.Parse(`<html lang="en">
<head><meta charset="utf-8"/><title>Suggestions</title></head>
<body>
<h1>Suggestions for {{ .Ts }}</h1>
<h2>Mains</h2>
<ol>
{{ range .Menu }}
<li><a href="{{.Url}}">{{ .Name }}</a></li>
{{ end }}
</ol>
<H2>Soup</H2>
<ul>
<li><a href="{{.Soup.Url}}">{{.Soup.Name}}</a></li>
</ul>
<small>V3.00 -- 😊 -- GO!</small>
</body>
</html>`)
if err != nil {
log.Fatal(err)
}
err = t.Execute(&buf, data)
if err != nil {
log.Fatal(err)
}
// log.Printf("%+v\n")
return buf.String()
}
func getEmojis() []string {
return []string{"🍽️", "🍴", "🍳", "👨🏻‍🍳", "👩🏻‍🍳", "🥑", "🥕", "🫑", "🍔"}
}
func updateTimestamps(r *dbconnection.SQLiteRepository, menuItems []dbconnection.Menu, timestamp int64) {
err := r.UpdateTimeStamps(menuItems, timestamp)
if err != nil {
log.Fatal(err)
}
}
func sendSMTP(inmsg string, subject string) {
log.Println("Sending Email")
e := email.NewEmail()
auth := smtp.PlainAuth("", login, "Ultra+Topaz+6XQ", hostname)
e.From = from
e.To = []string{from}
e.Bcc = recipients
e.Subject = subject
e.Text = []byte(inmsg)
e.HTML = []byte(inmsg)
err := e.Send(hostname+":587", auth)
if err != nil {
log.Fatal(err)
} else {
log.Println("Mail sent!")
}
}

13
jobhandler/jobstructs.go Normal file
View File

@ -0,0 +1,13 @@
package jobhandler
type PugData struct {
Menu []MenuEntry `json:"menu"`
Soup MenuEntry `json:"soup"`
Ts string `json:"ts"`
}
type MenuEntry struct {
Url string `json:"url"`
Meat int64 `json:"meat"`
Name string `json:"name"`
}

9
menuserver.iml Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

17
pug/emailV2.pug Normal file
View File

@ -0,0 +1,17 @@
html(lang="en")
head
meta(charset='utf-8')
title Suggestions
body
h1 Suggestions for !{Ts}
h2 Mains
ol
li: a(href="google.com")= line "name"
H2 Soup
ul
li: a(href="reddit.com")= "Reddit"
small V3.00 -- GO!

268
server.go Normal file
View File

@ -0,0 +1,268 @@
package main
import (
"bytes"
"database/sql"
"encoding/json"
"fmt"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/template/html/v2"
_ "github.com/mattn/go-sqlite3"
"github.com/robfig/cron/v3"
"github.com/stoewer/go-strcase"
"github.com/yuin/goldmark"
"log"
"menuserver/dbconnection"
"menuserver/jobhandler"
"os"
"strconv"
"strings"
)
const fileName = "./db/menu.db"
const htmlFront = "<!DOCTYPE html>\n <html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>${data.name}</title>\n <style>\n @import url('https://fonts.googleapis.com/css?family=Roboto');:root {--primary-color: #64B5F6;--dark-color: #333333;--light-color: #f4f4f4;--danger-color: #dc3545;--success-color: #28a745;}* {box-sizing: border-box;margin: 0;padding: 0;}body {font-family: 'Roboto', sans-serif;font-size: 1rem;line-height: 1.6;background-color: #fff;color: #333;}a {color: var(--primary-color);text-decoration: none;}a:hover {color: #666;}ul {list-style: none;}img {width: 100%;}.container {max-width: 1100px;margin: auto;overflow: hidden;padding: 0 2rem;}@media (max-width: 700px) {.hide-sm {display: none;}.grid-2, .grid-3, .grid-4 {grid-template-columns: 1fr;}.x-large {font-size: 3rem;}.large {font-size: 2rem;}.lead {font-size: 1rem;}.navbar {display: block;text-align: center;}.navbar ul {text-align: center;justify-content: center;}}\n </style>\n </head>\n <body>\n <div class=\"container\">"
const htmlEnd = "</div>\n \n </body>\n </html>"
var (
version string
build string
)
func main() {
fmt.Printf("Menu server v%+v build %+v\n\n", version, build)
// setup connections n stuff
db, err := sql.Open("sqlite3", fileName) // db
if err != nil {
log.Fatal(err)
}
engine := html.New("./dist", ".html")
app := fiber.New(fiber.Config{
Views: engine,
})
recipes := dbconnection.NewSQLiteRepository(db)
// Caching..
/*app.Use(cache.New(cache.Config{
Next: func(c *fiber.Ctx) bool {
return c.Query("noCache") == "true"
},
Expiration: 30 * time.Minute,
CacheControl: false,
}))*/
app.Get("/", indexHandler)
port := os.Getenv("PORT")
if port == "" {
port = "3000"
}
// routes
app.Static("/", "./dist")
app.Get("/view/:recipeId", func(c *fiber.Ctx) error {
return ViewRecipeHandler(c, recipes)
})
app.Get("/recipes", func(c *fiber.Ctx) error {
return RecipeHandler(c, recipes)
})
app.Post("/recipes", func(c *fiber.Ctx) error {
return PostRecipeHandler(c, recipes)
})
app.Get("/recipes/:recipeId", func(c *fiber.Ctx) error {
return RecipeIdHandler(c, recipes)
})
app.Put("/recipes/:recipeId", func(c *fiber.Ctx) error {
return PutRecipeHandler(c, recipes)
})
c := cron.New()
var _, cronerr = c.AddFunc("0 8 * * 6", func() { jobhandler.DoJob(recipes) })
if cronerr != nil {
log.Fatal(cronerr)
return
} else {
log.Println("Starting cron..")
c.Start()
}
log.Fatalln(app.Listen(fmt.Sprintf(":%v", port)))
}
func indexHandler(c *fiber.Ctx) error {
return c.Render("index", nil)
}
func NotImplemented(c *fiber.Ctx, from string) error {
fmt.Printf("NotImplemented from: %s\n", from)
return c.SendString("{}")
}
func RecipeHandler(c *fiber.Ctx, r *dbconnection.SQLiteRepository) error {
fmt.Println("RecipeHandler")
items, err := r.All()
if err != nil {
log.Fatal(err)
}
// log.Printf("%+v\n", items)
jsonStr, err := json.Marshal(items)
if err != nil {
log.Println("RecipeHandler marshalling")
log.Fatal(err)
}
c.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON)
return c.SendString(string(jsonStr))
}
func ViewRecipeHandler(c *fiber.Ctx, r *dbconnection.SQLiteRepository) error {
params := c.AllParams()
fmt.Printf("ViewRecipeHandler: %s\n", params["recipeId"])
item, err := r.GetOneShort(params["recipeId"])
if err != nil {
log.Fatal(err)
}
var buf bytes.Buffer
if err := goldmark.Convert([]byte(item.Md), &buf); err != nil {
log.Println("ViewRecipeHandler md conversion")
panic(err)
}
s := []string{htmlFront, buf.String(), htmlEnd}
output := []byte(strings.Join(s, ""))
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
return c.Send(output)
}
func RecipeIdHandler(c *fiber.Ctx, r *dbconnection.SQLiteRepository) error {
params := c.AllParams()
fmt.Printf("RecipeID Handler: %s\n", params["recipeId"])
item, err := r.GetOneHash(params["recipeId"])
if err != nil {
log.Fatal(err)
}
jsonStr, err := json.Marshal(item)
if err != nil {
log.Println("RecipeIDHandler marshalling")
log.Fatal(err)
}
c.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON)
return c.SendString(string(jsonStr))
}
func hash(instring string) uint64 {
var workhash uint64
workhash = 5381
index := len(instring)
buf := bytes.NewBufferString(instring)
for i := 0; i < index; i++ {
c, err := buf.ReadByte()
if err != nil {
log.Fatal(err)
}
workhash = (workhash * 33) ^ uint64(c)
}
return workhash >> 0
}
func ShortHash(instring string) string {
h := hash(instring)
return strconv.FormatInt(int64(h), 16)
}
func PostRecipeHandler(c *fiber.Ctx, r *dbconnection.SQLiteRepository) error {
fmt.Printf("RecipeID Post Handler: \n")
payload := dbconnection.MenuStrings{}
if err := c.BodyParser(&payload); err != nil {
log.Fatal(err)
return err
}
payload.Hash = ShortHash(payload.Name)
payload.Short = strcase.KebabCase(payload.Name)
payload.Lastused = 0
var returnVal dbconnection.InsertedType
returnVal, _ = r.InsertOne(payload)
jsonStr, err := json.Marshal(returnVal)
if err != nil {
log.Println("PostRecipeHandler marshalling")
log.Fatal(err)
}
c.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON)
return c.SendString(string(jsonStr))
}
func PutRecipeHandler(c *fiber.Ctx, r *dbconnection.SQLiteRepository) error {
fmt.Printf("RecipeID Put Handler:\n")
payload := dbconnection.MenuStrings{}
if err := c.BodyParser(&payload); err != nil {
log.Fatal(err)
return err
}
log.Printf("%+v\n", payload)
c.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON)
return c.SendString("{}")
}
// string splitter for kebab case
// [A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+

27
stuff.txt Normal file
View File

@ -0,0 +1,27 @@
// web server
type Foo struct {
Number int `json:"number"`
Title string `json:"title"`
}
foo_marshalled, err := json.Marshal(Foo{Number: 1, Title: "test"})
fmt.Fprint(w, string(foo_marshalled)) // write response to ResponseWriter (w)
first character being uppercase makes it visible to json, then with `json:number` bit tells
json it's proper name
* attached to a type (*string) indicates a pointer to the type.
* attached to a variable in an assignment (*v = ...) indicates an indirect assignment.
That is, change the value pointed at by the variable.
* attached to a variable or expression (*v) indicates a pointer dereference.
That is, take the value the variable is pointing at.
& attached to a variable or expression (&v) indicates a reference.
That is, create a pointer to the value of the variable or to the field.