B: Add search and greatly clean up UI

This commit is contained in:
Preston Baxter 2023-11-24 01:00:28 -06:00
parent cf1c32208e
commit d632f714d0
16 changed files with 545 additions and 39 deletions

View File

@ -319,6 +319,7 @@ func ScheduleBroadcastFromWebhook(c *gin.Context, body *webhooks.EventDelivery)
UserId: *uid,
TriggeringEvent: eventRecievedAudit.MongoId(),
Result: result,
CorrelationId: payload.Id,
VendorName: models.YOUTUBE_VENDOR_NAME,
}

View File

@ -1,7 +1,7 @@
BASE_URL="us-central1-docker.pkg.dev/pbaxter-infra/capstone-repo"
local-build:
rm **/*_templ.go; templ generate -path ./templates
rm -f templates/*.go; templ generate -path ./templates
npx tailwindcss -i static/index.css -o dist/output.css
GOEXPERIMENT=loopvar go build -o ./tmp/main .

View File

@ -87,18 +87,36 @@ func eventsRecievedMetricFunction(c *gin.Context) *DashboardMetric {
if events[biggestVendor].Count < event.Count {
biggestVendor = index
}
}
p := message.NewPrinter(language.English)
return &DashboardMetric{
Title: "Events Recieved",
PrimaryValue: p.Sprintf("%d", totalEvents),
SecondaryValue: p.Sprintf("Most events came from: %s", events[biggestVendor].Name),
Subtitle: "thats a lot of events",
SecondaryValue: "",
Subtitle: p.Sprintf("Most events came from: %s", events[biggestVendor].Name),
}
}
func streamsScheduledMetricFunction(c *gin.Context) *DashboardMetric {
user := getUserFromContext(c)
events, err := mongo.AggregateBroadcastReport(user.Id)
if err != nil {
log.WithError(err).Errorf("Failed to find broadcast report for user: %s", user.Id.Hex())
return defaultMetricFunction(c)
}
totalEvents := 0
for _, event := range events {
totalEvents += event.Count
}
p := message.NewPrinter(language.English)
return &DashboardMetric{
Title: "Broadcasts scheduled",
PrimaryValue: p.Sprintf("%d", totalEvents),
SecondaryValue: "",
Subtitle: "Scheduled to youtube",
}
}

View File

@ -28,7 +28,7 @@ func BuildRouter(r *gin.Engine) {
r.Use(cors.Default())
r.Static("/static", "/var/capstone/dist")
r.Static("/static", "./dist")
//mainpage
r.GET("/", AuthMiddleware(false), LandingPage)
@ -51,6 +51,11 @@ func BuildRouter(r *gin.Engine) {
//Dashboard Components
dashboardComponents := dashboard.Group("/components")
dashboardComponents.GET("/metric_card", GetMetricCard)
//Dashboard Events
dashboardEvents := dashboard.Group("/events")
dashboardEvents.GET("", EventsPage)
dashboardEventComponents := dashboardEvents.Group("/components")
dashboardEventComponents.GET("/table_data", GetTableComponent)
//Vendor stuff
vendor := r.Group("/vendor")

View File

@ -0,0 +1,121 @@
package controllers
import (
"strings"
"time"
"git.preston-baxter.com/Preston_PLB/capstone/frontend-service/templates"
"github.com/gin-gonic/gin"
)
var (
eventsTableMap = map[string]eventsTableFunc{"default": defaultTableData, "events_for_user": eventsForUserTableData, "actions_for_user": actionsForUserTableData}
)
type eventsTableFunc func(c *gin.Context) templates.TableData
func GetTableComponent(c *gin.Context) {
user := getUserFromContext(c)
if user == nil {
log.Warnf("Could not find user in context. Trying to redner Action form")
badRequest(c, "No user available in context")
return
}
if table, ok := c.GetQuery("table_name"); ok {
if tableFunc, mok := eventsTableMap[table]; mok {
renderEventTable(c, table, &tableFunc)
return
}
}
//send default metric function
log.Warn("Failed to find eventsTableFunc")
defaultFunc := eventsTableMap["default"]
renderEventTable(c, "default", &defaultFunc)
}
func defaultTableData(c *gin.Context) templates.TableData {
return [][]string{{"id", "col 1", "col 2"}, {"row data", "item data", "stuff"}}
}
func renderEventTable(c *gin.Context, title string, tableFunc *eventsTableFunc) {
tableData := (*tableFunc)(c)
renderTempl(c, templates.EventTableData(tableData, title))
}
func eventsForUserTableData(c *gin.Context) templates.TableData {
//User can't be nil because we check before we get here
user := getUserFromContext(c)
events, err := mongo.FindEventsRecievedByUserId(user.Id)
if err != nil {
log.WithError(err).Errorf("Failed to find events for user: %s to load table data", user.Id.Hex())
return defaultTableData(c)
}
//check for filter
filter, filter_exists := c.GetQuery("filter")
table := make([][]string, len(events)+1)
index := 1
for _, event := range events {
arr := []string{event.CreatedAt.Format(time.Stamp), strings.ToUpper(event.VendorName), event.VendorId, event.Type}
if filter_exists {
//if the filter exists loop through the row. Check if anything meets the filter
pass := false
for _, item := range arr {
//if we already have a match short circuit. If we don't we can potentially flip from false -> true
pass = pass || strings.Contains(item, filter)
}
//If we did not find a matching item continue
if !pass {
continue
}
}
//We either had no filter or passed the filter check. Add to the pool
table[index] = arr
index += 1
table[0] = []string{"Timestamp", "Vendor", "Id", "Event Type"}
}
return table[0:index]
}
func actionsForUserTableData(c *gin.Context) templates.TableData {
//User can't be nil because we check before we get here
user := getUserFromContext(c)
actions, err := mongo.FindActionTakenByUserId(user.Id)
if err != nil {
log.WithError(err).Errorf("Failed to find actions for user: %s to load table data", user.Id.Hex())
return defaultTableData(c)
}
//check for filter
filter, filter_exists := c.GetQuery("filter")
index := 1
table := make([][]string, len(actions)+1)
for _, action := range actions {
arr := []string{action.CreatedAt.Format(time.RFC1123), action.VendorName, action.CorrelationId, action.Result}
if filter_exists {
//if the filter exists loop through the row. Check if anything meets the filter
pass := false
for _, item := range arr {
//if we already have a match short circuit. If we don't we can potentially flip from false -> true
pass = pass || strings.Contains(item, filter)
}
//If we did not find a matching item continue
if !pass {
continue
}
}
table[index] = []string{action.CreatedAt.Format(time.RFC1123), action.VendorName, action.CorrelationId, action.Result}
index += 1
}
table[0] = []string{"Timestamp", "Vendor", "Id", "Result"}
return table[0:index]
}

View File

@ -70,3 +70,15 @@ func DashboardPage(c *gin.Context) {
renderTempl(c, templates.DashboardPage(user, vendors, actions))
}
func EventsPage(c *gin.Context) {
user := getUserFromContext(c)
if user == nil {
log.Error("No user found in context")
serverError(c, "No user found in context")
return
}
renderTempl(c, templates.EventsPage(user))
}

View File

@ -96,17 +96,71 @@ func (db *DB) FindEventsRecievedByUserId(userId primitive.ObjectID) ([]models.Ev
return events, nil
}
func (db *DB) FindActionTakenByUserId(userId primitive.ObjectID) ([]models.ActionTaken, error) {
conf := config.Config()
opts := options.Find()
res, err := db.client.Database(conf.Mongo.EntDb).Collection(conf.Mongo.EntCol).Find(context.Background(), bson.M{"user_id": userId, "obj_info.ent": models.ACTION_TAKEN_TYPE}, opts)
if err != nil {
if err == mongo.ErrNoDocuments {
return nil, nil
}
return nil, err
}
events := []models.ActionTaken{}
err = res.All(context.Background(), &events)
if err != nil {
return nil, err
}
return events, nil
}
type VendorEventReport struct {
Count int `bson:"count"`
Name string `bson:"_id"`
}
func (db *DB) AggregateBroadcastReport(userId primitive.ObjectID) ([]VendorEventReport, error) {
conf := config.Config()
opts := options.Aggregate().SetAllowDiskUse(false)
aggregation := bson.A{
bson.D{{Key: "$match", Value: bson.D{{Key: "obj_info.ent", Value: models.ACTION_TAKEN_TYPE}, {Key: "result", Value: "Updated Broadcast"}}}},
bson.D{
{Key: "$group",
Value: bson.D{
{Key: "_id", Value: nil},
{Key: "count", Value: bson.D{{Key: "$sum", Value: 1}}},
},
},
},
}
res, err := db.client.Database(conf.Mongo.EntDb).Collection(conf.Mongo.EntCol).Aggregate(context.Background(), aggregation, opts)
if err != nil {
if err == mongo.ErrNoDocuments {
return nil, nil
}
return nil, err
}
events := []VendorEventReport{}
err = res.All(context.Background(), &events)
if err != nil {
return nil, err
}
return events, nil
}
func (db *DB) AggregateVendorEventReport(userId primitive.ObjectID) ([]VendorEventReport, error) {
conf := config.Config()
opts := options.Aggregate().SetAllowDiskUse(false)
aggregation := bson.A{
bson.D{{Key: "$match", Value: bson.D{{Key: "obj_info.ent", Value: "audit_event_recieved"}}}},
bson.D{{Key: "$match", Value: bson.D{{Key: "obj_info.ent", Value: models.EVENT_RECIEVED_TYPE}}}},
bson.D{
{Key: "$group",
Value: bson.D{

View File

@ -17,7 +17,7 @@ type EventRecieved struct {
Id primitive.ObjectID `bson:"_id,omitempty"`
UserId primitive.ObjectID `bson:"user_id,omitempty"` //what user is this associated too
VendorName string `bson:"vendor_name,omitempty"` //Vendor name of who sent us the event
VendorId string `bson:"vendor_id:omitempty"`
VendorId string `bson:"vendor_id,omitempty"`
Type string `bson:"type,omitempty"` //type of event
}
@ -34,7 +34,7 @@ func (obj *EventRecieved) UpdateObjectInfo() {
now := time.Now()
if obj.CommonFields == nil {
obj.CommonFields = new(CommonFields)
obj.EntityType = EVENT_RECIEVED_TYPE
obj.EntityType = ACTION_TAKEN_TYPE
obj.CreatedAt = now
}
obj.UpdatedAt = now
@ -47,6 +47,7 @@ type ActionTaken struct {
UserId primitive.ObjectID `bson:"user_id,omitempty"` //what user is this associated too
TriggeringEvent primitive.ObjectID `bson:"triggering_event,omitempty"` //what triggered this action to be taken
Result string `bson:"result,omitempty"` //list of entities effected or created from action
CorrelationId string `bson:"correlation_id,omitempty"` //list of entities effected or created from action
VendorName string `bson:"vendor_name,omitempty"` //Vendor name that the action was taken against
}

Binary file not shown.

View File

@ -36,11 +36,11 @@ templ DashboardPage(user *models.User, vendorAccounts []models.VendorAccount, ac
tabindex="-1"
class="transition-all hidden"
></div>
<div id="root">
<div id="root" class="h-screen overflow-scroll">
@DashboardNav(user)
@DashboardContent(user, vendorAccounts, actionMappings)
@Footer()
</div>
@DashboardFooter()
</body>
@DashboardScript()
@toggleDropdown()
@ -104,7 +104,9 @@ templ DashboardNav(user *models.User) {
</div>
</form>
<ul class="md:flex-col md:min-w-full flex flex-col list-none">
@DashboardNavItem("fa-tv", "Dashboard", "#", true)
@DashboardNavItem("fa-tv", "Dashboard", "/dashboard", true)
@DashboardNavItem("fa-chart-bar", "Events", "/dashboard/events", true)
<hr/>
@DashboardNavItem("fa-newspaper", "Home Page", "/", true)
@DashboardNavItem("fa-user-circle", "Profile (SOON)", "#", false)
</ul>
@ -117,7 +119,7 @@ templ DashboardNavItem(icon, name, link string, enabled bool) {
<li class="items-center">
<a
if enabled {
class="text-pink-500 hover:text-pink-600 text-xs uppercase py-3 font-bold block"
class="text-blue-500 hover:text-blue-600 text-xs uppercase py-3 font-bold block"
href={ templ.URL(link) }
} else {
class="text-blueGray-300 text-xs uppercase py-3 font-bold block"
@ -131,8 +133,7 @@ templ DashboardNavItem(icon, name, link string, enabled bool) {
} else {
class={ fmt.Sprintf("fas %s text-blueGray-300 mr-2 text-sm", icon) }
}
></i>
{ name }
></i>{ name }
</a>
</li>
}
@ -151,7 +152,7 @@ templ DashboardContentNav(user *models.User) {
href="./index.html"
>Capstone</a>
<form
class="md:flex hidden flex-row flex-wrap items-center lg:ml-auto mr-3"
class="hidden flex-row flex-wrap items-center lg:ml-auto mr-3"
>
<div class="relative flex w-full flex-wrap items-stretch">
<span
@ -165,9 +166,9 @@ templ DashboardContentNav(user *models.User) {
</div>
</form>
<ul
class="flex-col md:flex-row list-none items-center hidden md:flex"
class="hidden flex-col md:flex-row list-none items-center hidden md:flex"
>
<a class="text-blueGray-500 block" href="#pablo" onclick="openDropdown(event,'user-dropdown')">
<a class="hidden text-blueGray-500 block" href="#pablo" onclick="openDropdown(event,'user-dropdown')">
<div class="items-center flex">
<span
class="w-12 h-12 text-sm text-white bg-blueGray-200 inline-flex items-center justify-center rounded-full"
@ -207,7 +208,36 @@ templ DashboardContentNav(user *models.User) {
}
templ DashboardCardLoader(kind string) {
<div class="w-full lg:w-6/12 xl:w-6/12 px-4" hx-get={ fmt.Sprintf("/dashboard/components/metric_card?metric=%s", kind) } hx-trigger="load" hx-swap="outerHTML"></div>
<div class="w-full lg:w-6/12 xl:w-6/12 px-4" hx-get={ fmt.Sprintf("/dashboard/components/metric_card?metric=%s", kind) } hx-trigger="load" hx-swap="outerHTML">
<div class="relative flex flex-col min-w-0 break-words bg-white rounded mb-6 xl:mb-0 shadow-lg opacity-50">
<div class="flex-auto p-4">
<div class="flex flex-wrap">
<div class="relative w-full pr-4 max-w-full flex-grow flex-1">
<h5 class="text-blueGray-400 uppercase font-bold text-xs">
---
</h5>
<span class="font-semibold text-xl text-blueGray-700">
---
</span>
</div>
<div class="relative w-auto pl-4 flex-initial">
<div class="text-white p-3 text-center inline-flex items-center justify-center w-12 h-12 shadow-lg rounded-full bg-blue-300">
<i class="far fa-chart-bar"></i>
</div>
</div>
</div>
<p class="text-sm text-blueGray-400 mt-4">
<span class="text-emerald-500 mr-2">
---
</span>
<span class="whitespace-nowrap">
---
</span>
</p>
</div>
@spinnerCentered()
</div>
</div>
}
templ DashboardCard(title, primaryVal, secondaryVal, subtitle string) {
@ -224,14 +254,14 @@ templ DashboardCard(title, primaryVal, secondaryVal, subtitle string) {
</span>
</div>
<div class="relative w-auto pl-4 flex-initial">
<div class="text-white p-3 text-center inline-flex items-center justify-center w-12 h-12 shadow-lg rounded-full bg-red-500">
<div class="text-white p-3 text-center inline-flex items-center justify-center w-12 h-12 shadow-lg rounded-full bg-blue-300">
<i class="far fa-chart-bar"></i>
</div>
</div>
</div>
<p class="text-sm text-blueGray-400 mt-4">
<span class="text-emerald-500 mr-2">
<i class="fas fa-arrow-up"></i> { secondaryVal }
{ secondaryVal }
</span>
<span class="whitespace-nowrap">
{ subtitle }
@ -397,7 +427,7 @@ templ DashboardActionDropDown() {
hx-get="/dashboard/forms/addAction"
hx-target="#add-action-modal"
hx-swap="outerHTML"
class="bg-pink-500 text-white active:bg-pink-600 font-bold uppercase text-sm px-6 py-3 rounded shadow hover:shadow-lg outline-none focus:outline-none mr-1 mb-1 ease-linear transition-all duration-150"
class="bg-blue-500 text-white active:bg-blue-600 font-bold uppercase text-sm px-6 py-3 rounded shadow hover:shadow-lg outline-none focus:outline-none mr-1 mb-1 ease-linear transition-all duration-150"
type="button"
>
Add Action
@ -475,10 +505,10 @@ templ DashboardActionsWidget(actions []models.ActionMapping) {
templ DashboardContent(user *models.User, vendorAccounts []models.VendorAccount, actions []models.ActionMapping) {
<noscript>You need to enable JavaScript to run this app.</noscript>
<div class="relative md:ml-64 bg-blueGray-50">
<div class="relative md:ml-64 bg-blueGray-50 h-full">
@DashboardContentNav(user)
<!-- Header -->
<div class="relative bg-pink-600 md:pt-32 pb-32 pt-12">
<div class="relative bg-blue-600 md:pt-32 pb-32 pt-12">
<div class="px-4 md:px-10 mx-auto w-full">
<div>
<!-- Card stats -->

View File

@ -0,0 +1,180 @@
package templates
import (
"fmt"
"git.preston-baxter.com/Preston_PLB/capstone/frontend-service/db/models"
)
type TableData [][]string
templ EventsPage(user *models.User) {
<!DOCTYPE html>
<html>
@Head("Events")
<body class="text-blueGray-700 antialiased">
<div
id="add-action-modal"
aria-hidden="false"
tabindex="-1"
class="transition-all hidden"
></div>
<div id="root" class="h-screen overflow-scroll">
@DashboardNav(user)
@EventContent(user)
</div>
@DashboardFooter()
</body>
@DashboardScript()
@toggleDropdown()
@updateSearchScript()
</html>
}
var sampleData = [][]string{{"head 1", "head 2"}, {"row 1", "row 1"}, {"row 2", "row 2"}}
var blankData = [][]string{{"head 1", "head 2"}}
templ EventContent(user *models.User) {
<noscript>You need to enable JavaScript to run this app.</noscript>
<div class="relative md:ml-64 bg-blueGray-50">
@DashboardContentNav(user)
<!-- Header -->
<div class="relative bg-blue-600 md:pt-32 pb-32 pt-12">
<div class="px-4 md:px-10 mx-auto w-1/4 h-3/4"></div>
<div class="px-4 md:px-10 mx-auto w-2/4 h-3/4">
<div class="relative flex flex-col min-w-0 break-words bg-white rounded mb-6 xl:mb-0 shadow-lg">
<div class="flex-auto p-4">
<div
class="flex-row flex-wrap items-center lg:ml-auto mr-3"
>
<div class="relative flex w-full flex-wrap items-stretch">
<span
class="z-10 h-full leading-snug font-normal text-center text-blueGray-300 absolute bg-transparent rounded text-base items-center justify-center w-8 pl-3 py-3"
><i class="fas fa-search"></i></span>
<input
id="search_bar"
type="text"
onkeyup="onSearchKeyUp()"
placeholder="Search here..."
class="border-0 px-3 py-3 placeholder-blueGray-300 text-blueGray-600 relative bg-white rounded text-sm shadow outline-none focus:outline-none focus:ring w-full pl-10"
/>
</div>
</div>
</div>
</div>
</div>
<div class="px-4 md:px-10 mx-auto w-1/4 h-3/4"></div>
</div>
<div class="px-4 md:px-10 mx-auto w-full -m-24">
<div class="flex flex-wrap">
<div class="w-full xl:w-6/12 mb-12 xl:mb-0 px-4">
@EventTableWidget("Events", "events_for_user")
</div>
<div class="w-full xl:w-6/12 mb-12 xl:mb-0 px-4">
@EventTableWidget("Actions", "actions_for_user")
</div>
</div>
</div>
</div>
}
templ EventTableWidget(title, table_name string) {
<div class="relative flex flex-col min-w-0 break-words bg-white w-full mb-6 shadow-lg rounded">
<div class="rounded-t mb-0 px-4 py-3 border-0">
<div class="flex flex-wrap items-center">
<div class="relative w-full px-4 max-w-full flex-grow flex-1">
<h3 class="font-semibold text-base text-blueGray-700">
{ title }
</h3>
</div>
</div>
</div>
<div class="block w-full overflow-x-auto">
<!-- Projects table -->
@EventTableDataLoader(table_name)
</div>
</div>
}
templ EventTableDataLoader(table_name string) {
<div class="relative" hx-get={ fmt.Sprintf("/dashboard/events/components/table_data?table_name=%s", table_name) } hx-swap="outerHTML" hx-trigger="load">
@EventTableDataLazy(table_name)
@spinnerCentered()
</div>
}
//hx-get={ fmt.Sprintf("/dashboard/events/components/table_data?table_name=%s", table_name) }
templ EventTableDataLazy(table_name string) {
<table class="items-center opacity-50 w-full bg-transparent border-collapse">
<thead>
<tr>
<th class="px-6 bg-blueGray-50 opacity-50 text-blueGray-500 align-middle border border-solid border-blueGray-100 py-3 text-xs uppercase border-l-0 border-r-0 whitespace-nowrap font-semibold text-left">
Lorem Ipsum
</th>
<th class="px-6 bg-blueGray-50 opacity-50 text-blueGray-500 align-middle border border-solid border-blueGray-100 py-3 text-xs uppercase border-l-0 border-r-0 whitespace-nowrap font-semibold text-left">
Lorem Ipsum
</th>
</tr>
</thead>
<tbody>
<tr>
<th class="border-t-0 px-6 align-middle opacity-50 border-l-0 border-r-0 text-xs whitespace-nowrap p-4 text-left">
No Data Available
</th>
<th class="border-t-0 px-6 align-middle opacity-50 border-l-0 border-r-0 text-xs whitespace-nowrap p-4 text-left">
No Data Available
</th>
</tr>
</tbody>
</table>
}
templ EventTableData(data TableData, table_name string) {
<table class="items-center w-full bg-transparent border-collapse" hx-get={ fmt.Sprintf("/dashboard/events/components/table_data?table_name=%s", table_name) } hx-params="*" hx-trigger="search delay:200ms from:body">
<thead>
<tr>
for _, header := range data[0] {
<th class="px-6 bg-blueGray-50 text-blueGray-500 align-middle border border-solid border-blueGray-100 py-3 text-xs uppercase border-l-0 border-r-0 whitespace-nowrap font-semibold text-left">
{ header }
</th>
}
</tr>
</thead>
<tbody>
if len(data) <= 1 {
<tr>
<th class="border-t-0 px-6 align-middle border-l-0 border-r-0 text-xs whitespace-nowrap p-4 text-left">
No Data Available
</th>
</tr>
} else {
for _, row := range data[1:] {
<tr>
for _, item := range row {
<th class="border-t-0 px-6 align-middle border-l-0 border-r-0 text-xs whitespace-nowrap p-4 text-left">
{ item }
</th>
}
</tr>
}
}
</tbody>
</table>
}
templ updateSearchScript() {
<script>
const searchEvent = new Event("search");
function onSearchKeyUp() {
console.log("keyup")
document.body.dispatchEvent(searchEvent);
}
document.body.addEventListener('htmx:configRequest', function(evt) {
var query = document.getElementById("search_bar").value;
evt.detail.parameters['filter'] = query // add a new parameter into the request
});
</script>
}

View File

@ -1,7 +1,7 @@
package templates
templ Footer() {
<footer class="absolute w-full bottom-0 bg-gray-900 pb-6">
templ LandingFooter() {
<footer class="relative w-full bottom-0 bg-gray-900 pb-6">
<div class="container mx-auto px-4">
<hr class="mb-6 border-b-1 border-gray-700"/>
<div
@ -32,11 +32,83 @@ templ Footer() {
class="text-white hover:text-gray-400 text-sm font-semibold block py-1 px-3"
>About Us</a>
</li>
</ul>
</div>
</div>
</div>
</footer>
}
templ DashboardFooter() {
<footer class="absolute w-full bottom-0 bg-gray-900 pb-6">
<div class="md:ml-64 md:mr-16 px-4">
<hr class="mb-6 border-b-1 border-gray-700"/>
<div
class="flex flex-wrap items-center md:justify-end justify-center"
>
<div class="w-full md:w-6/12 px-4">
<div class="text-sm text-white font-semibold py-1">
Copyright © 2023
<a
href="https://git.preston-baxter.com"
class="text-white hover:text-gray-400 text-sm font-semibold py-1"
>Preston Baxter</a>
</div>
</div>
<div class="w-full md:w-6/12 px-4">
<ul
class="flex flex-wrap list-none md:justify-end justify-center"
>
<li>
<a
href="https://github.com/creativetimofficial/argon-design-system/blob/master/LICENSE.md"
href="https://git.preston-baxter.com"
class="text-white hover:text-gray-400 text-sm font-semibold block py-1 px-3"
>MIT License</a>
>Preston Baxter</a>
</li>
<li>
<a
href="/about-us"
class="text-white hover:text-gray-400 text-sm font-semibold block py-1 px-3"
>About Us</a>
</li>
</ul>
</div>
</div>
</div>
</footer>
}
templ LoginFooter() {
<footer class="absolute w-full bottom-0 bg-gray-900 pb-6">
<div class="md:mx-16 px-4">
<hr class="mb-6 border-b-1 border-gray-700"/>
<div
class="flex flex-wrap items-center md:justify-end justify-center"
>
<div class="w-full md:w-6/12 px-4">
<div class="text-sm text-white font-semibold py-1">
Copyright © 2023
<a
href="https://git.preston-baxter.com"
class="text-white hover:text-gray-400 text-sm font-semibold py-1"
>Preston Baxter</a>
</div>
</div>
<div class="w-full md:w-6/12 px-4">
<ul
class="flex flex-wrap list-none md:justify-end justify-center"
>
<li>
<a
href="https://git.preston-baxter.com"
class="text-white hover:text-gray-400 text-sm font-semibold block py-1 px-3"
>Preston Baxter</a>
</li>
<li>
<a
href="/about-us"
class="text-white hover:text-gray-400 text-sm font-semibold block py-1 px-3"
>About Us</a>
</li>
</ul>
</div>

View File

@ -127,7 +127,7 @@ templ LandingContent() {
</div>
<div class="w-full md:w-4/12 px-4 mr-auto ml-auto">
<div
class="relative flex flex-col min-w-0 break-words bg-white w-full mb-6 shadow-lg rounded-lg bg-pink-600"
class="relative flex flex-col min-w-0 break-words bg-white w-full mb-6 shadow-lg rounded-lg bg-blue-600"
>
<img
alt="..."
@ -142,7 +142,7 @@ templ LandingContent() {
class="absolute left-0 w-full block"
style="height: 95px; top: -94px;"
>
<polygon points="-30,95 583,95 583,65" class="text-pink-600 fill-current"></polygon>
<polygon points="-30,95 583,95 583,65" class="text-blue-600 fill-current"></polygon>
</svg>
<h4 class="text-xl font-bold text-white">
Top Notch Services
@ -187,7 +187,7 @@ templ LandingContent() {
<div class="w-full md:w-5/12 ml-auto mr-auto px-4">
<div class="md:pr-12">
<div
class="text-pink-600 p-3 text-center inline-flex items-center justify-center w-16 h-16 mb-6 shadow-lg rounded-full bg-pink-300"
class="text-blue-600 p-3 text-center inline-flex items-center justify-center w-16 h-16 mb-6 shadow-lg rounded-full bg-pink-300"
>
<i class="fas fa-rocket text-xl"></i>
</div>
@ -202,7 +202,7 @@ templ LandingContent() {
<div class="flex items-center">
<div>
<span
class="text-xs font-semibold inline-block py-1 px-2 uppercase rounded-full text-pink-600 bg-pink-200 mr-3"
class="text-xs font-semibold inline-block py-1 px-2 uppercase rounded-full text-blue-600 bg-pink-200 mr-3"
>
<i
class="fas fa-fingerprint"
@ -220,7 +220,7 @@ templ LandingContent() {
<div class="flex items-center">
<div>
<span
class="text-xs font-semibold inline-block py-1 px-2 uppercase rounded-full text-pink-600 bg-pink-200 mr-3"
class="text-xs font-semibold inline-block py-1 px-2 uppercase rounded-full text-blue-600 bg-pink-200 mr-3"
>
<i
class="fab fa-html5"
@ -236,7 +236,7 @@ templ LandingContent() {
<div class="flex items-center">
<div>
<span
class="text-xs font-semibold inline-block py-1 px-2 uppercase rounded-full text-pink-600 bg-pink-200 mr-3"
class="text-xs font-semibold inline-block py-1 px-2 uppercase rounded-full text-blue-600 bg-pink-200 mr-3"
>
<i
class="far fa-paper-plane"

View File

@ -10,6 +10,7 @@ templ Head(msg string) {
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<meta name="theme-color" content="#000000"/>
<meta name="view-transition" content="same-origin"/>
<meta name="htmx-config" content='{"globalViewTransistions": true}'/>
<link rel="shortcut icon" href="./assets/img/favicon.ico"/>
<link
rel="apple-touch-icon"
@ -41,7 +42,7 @@ templ LandingPage(user *models.User) {
@Nav(user)
@LandingContent()
</body>
@Footer()
@LandingFooter()
@toggleNavBar()
</html>
}

View File

@ -4,11 +4,11 @@ templ LoginPage(errorMsg string) {
<!DOCTYPE html>
<html>
@Head("Log in")
<body class="text-gray-800 antialiased">
<body class="text-gray-800 antialiased h-full overflow-hidden">
@Nav(nil)
@LoginContent(false, errorMsg)
</body>
@Footer()
@LoginFooter()
@toggleNavBar()
</html>
}
@ -17,11 +17,11 @@ templ SignupPage(errorMsg string) {
<!DOCTYPE html>
<html>
@Head("Sign up")
<body class="text-gray-800 antialiased">
<body class="text-gray-800 antialiased h-full overflow-hidden">
@Nav(nil)
@LoginContent(true, errorMsg)
</body>
@Footer()
@LoginFooter()
@toggleNavBar()
</html>
}

11
ui/templates/util.templ Normal file
View File

@ -0,0 +1,11 @@
package templates
templ spinnerCentered() {
<div role="status" class="absolute -translate-x-1/2 -translate-y-1/2 top-2/4 left-1/2">
<svg aria-hidden="true" class="w-8 h-8 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor"></path>
<path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill"></path>
</svg>
<span class="sr-only">Loading...</span>
</div>
}