Add a basic main page with login
None of the links work and the marketing copy sucks, but it's just showing the bones while I figure the technical bits out.
This commit is contained in:
parent
25039a8f54
commit
56eaa4ed1c
10 changed files with 266 additions and 3 deletions
3
.gitattributes
vendored
Normal file
3
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
static/img filter=lfs diff=lfs merge=lfs -text
|
||||||
|
static/vendor/css filter=lfs diff=lfs merge=lfs -text
|
||||||
|
static/vendor/js filter=lfs diff=lfs merge=lfs -text
|
||||||
116
fileserver.go
Normal file
116
fileserver.go
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
package main
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed static
|
||||||
|
var embeddedStaticFS embed.FS
|
||||||
|
|
||||||
|
// FileServer conveniently sets up a http.FileServer handler to serve
|
||||||
|
// static files from a http.FileSystem.
|
||||||
|
func FileServer(r chi.Router, path string, root http.FileSystem, embeddedFS embed.FS, embeddedPath string) {
|
||||||
|
if strings.ContainsAny(path, "{}*") {
|
||||||
|
panic("FileServer does not permit any URL parameters.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if path != "/" && path[len(path)-1] != '/' {
|
||||||
|
r.Get(path, http.RedirectHandler(path+"/", 301).ServeHTTP)
|
||||||
|
path += "/"
|
||||||
|
}
|
||||||
|
path += "*"
|
||||||
|
|
||||||
|
r.Get(path, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
rctx := chi.RouteContext(r.Context())
|
||||||
|
pathPrefix := strings.TrimSuffix(rctx.RoutePattern(), "/*")
|
||||||
|
|
||||||
|
// Determine the actual file path
|
||||||
|
requestedPath := strings.TrimPrefix(r.URL.Path, pathPrefix)
|
||||||
|
|
||||||
|
// Try to open from local filesystem first for development
|
||||||
|
localFile, localErr := root.Open(requestedPath)
|
||||||
|
|
||||||
|
var fileToServe http.File
|
||||||
|
|
||||||
|
if localErr == nil {
|
||||||
|
// File found in local filesystem
|
||||||
|
fileToServe = localFile
|
||||||
|
} else {
|
||||||
|
// If not found locall, try embedded filesystem
|
||||||
|
embeddedFilePath := filepath.Join(embeddedPath, requestedPath)
|
||||||
|
embeddedFile, err := embeddedFS.Open(embeddedFilePath)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap the embedded file to implement http.File interface
|
||||||
|
fileToServe = &embeddedFileWrapper{embeddedFile}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a custom ResponseWriter that allows us to modify headers
|
||||||
|
crw := &customResponseWriter{ResponseWriter: w}
|
||||||
|
|
||||||
|
// Serve the file
|
||||||
|
http.ServeContent(crw, r, requestedPath, time.Time{}, fileToServe)
|
||||||
|
|
||||||
|
// Close the file
|
||||||
|
fileToServe.Close()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
type embeddedFileWrapper struct {
|
||||||
|
file fs.File
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *embeddedFileWrapper) Close() error {
|
||||||
|
return e.file.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *embeddedFileWrapper) Read(p []byte) (n int, err error) {
|
||||||
|
return e.file.Read(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Seeker interface {
|
||||||
|
Seek(offset int64, whence int) (int64, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *embeddedFileWrapper) Seek(offset int64, whence int) (int64, error) {
|
||||||
|
if seeker, ok := e.file.(Seeker); ok {
|
||||||
|
return seeker.Seek(offset, whence)
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("Seek not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *embeddedFileWrapper) Readdir(count int) ([]os.FileInfo, error) {
|
||||||
|
// This is a bit tricky with embedded files
|
||||||
|
if dirFile, ok := e.file.(fs.ReadDirFile); ok {
|
||||||
|
entries, err := dirFile.ReadDir(count)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fileInfos := make([]os.FileInfo, len(entries))
|
||||||
|
for i, entry := range entries {
|
||||||
|
fileInfos[i], err = entry.Info()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fileInfos, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("Readdir not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *embeddedFileWrapper) Stat() (os.FileInfo, error) {
|
||||||
|
return e.file.Stat()
|
||||||
|
}
|
||||||
4
main.go
4
main.go
|
|
@ -46,6 +46,10 @@ func main() {
|
||||||
|
|
||||||
r.Get("/", getRoot)
|
r.Get("/", getRoot)
|
||||||
r.Get("/favicon.ico", getFavicon)
|
r.Get("/favicon.ico", getFavicon)
|
||||||
|
|
||||||
|
localFS := http.Dir("./static")
|
||||||
|
FileServer(r, "/static", localFS, embeddedStaticFS, "static")
|
||||||
|
|
||||||
log.Printf("Serving on %s", bind)
|
log.Printf("Serving on %s", bind)
|
||||||
log.Fatal(http.ListenAndServe(bind, r))
|
log.Fatal(http.ListenAndServe(bind, r))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
31
response.go
Normal file
31
response.go
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
package main
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Custom ResponseWriter to track Content-Type
|
||||||
|
type customResponseWriter struct {
|
||||||
|
http.ResponseWriter
|
||||||
|
contentType string
|
||||||
|
wroteHeader bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (crw *customResponseWriter) WriteHeader(code int) {
|
||||||
|
crw.wroteHeader = true
|
||||||
|
crw.ResponseWriter.WriteHeader(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (crw *customResponseWriter) Header() http.Header {
|
||||||
|
return crw.ResponseWriter.Header()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (crw *customResponseWriter) Write(b []byte) (int, error) {
|
||||||
|
if !crw.wroteHeader {
|
||||||
|
if crw.contentType == "" {
|
||||||
|
crw.contentType = http.DetectContentType(b)
|
||||||
|
crw.ResponseWriter.Header().Set("Content-Type", crw.contentType)
|
||||||
|
}
|
||||||
|
crw.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
return crw.ResponseWriter.Write(b)
|
||||||
|
}
|
||||||
BIN
static/img/nidus-logo-256-transparent.png
Normal file
BIN
static/img/nidus-logo-256-transparent.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
7
static/vendor/css/bootstrap.min.css
vendored
Normal file
7
static/vendor/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
7
static/vendor/js/bootstrap.bundle.min.js
vendored
Normal file
7
static/vendor/js/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
7
static/vendor/js/bootstrap.min.js
vendored
Normal file
7
static/vendor/js/bootstrap.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -2,9 +2,15 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>Nidus Sync</title>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{{template "title" .}} - Nidus Sync</title>
|
||||||
|
<link href="/static/vendor/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
{{template "style" .}}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{{template "content" .}}
|
{{template "content" .}}
|
||||||
|
<script src="/static/vendor/js/bootstrap.bundle.min.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,87 @@
|
||||||
{{template "base.html" .}}
|
{{template "base.html" .}}
|
||||||
|
|
||||||
{{define "content"}}
|
{{define "title"}}Login{{end}}
|
||||||
<p>Imagine there's cool stuff here.</p>
|
{{define "style"}}
|
||||||
|
.login-container {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
.login-box {
|
||||||
|
box-shadow: 0 0 15px rgba(0,0,0,0.1);
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.login-form-section {
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
.product-info-section {
|
||||||
|
padding: 40px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
.login-header {
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
{{end}}
|
||||||
|
{{define "content"}}
|
||||||
|
<div class="container min-vh-100 d-flex align-items-center justify-content-center py-5">
|
||||||
|
<div class="login-container">
|
||||||
|
<div class="row login-box g-0">
|
||||||
|
<!-- Left side: Login Form -->
|
||||||
|
<div class="col-md-6 login-form-section">
|
||||||
|
<div class="login-header">
|
||||||
|
<h2>Welcome Back</h2>
|
||||||
|
<p class="text-muted">Please enter your credentials</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="username" class="form-label">Username</label>
|
||||||
|
<input type="text" class="form-control" id="username" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="password" class="form-label">Password</label>
|
||||||
|
<input type="password" class="form-control" id="password" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3 form-check">
|
||||||
|
<input type="checkbox" class="form-check-input" id="remember">
|
||||||
|
<label class="form-check-label" for="remember">Remember me</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<button type="submit" class="btn btn-primary">Login</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3 text-center">
|
||||||
|
<p>Don't have an account? <a href="/signup">Sign up</a></p>
|
||||||
|
<a href="forgot-password.html">Forgot password?</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right side: Product Information -->
|
||||||
|
<div class="col-md-6 product-info-section">
|
||||||
|
<div>
|
||||||
|
<img src="/static/img/nidus-logo-256-transparent.png"></img>
|
||||||
|
<h2>Nidus Sync</h2>
|
||||||
|
<p class="lead mb-4">All your field data, sync'd to all your techs</p>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<p>Something intelligent and intriguing</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<h5>Key Features</h5>
|
||||||
|
<ul>
|
||||||
|
<li>Works with <b>Fieldseeker</b></li>
|
||||||
|
<li>Works <i>with</i> Fieldseeker</li>
|
||||||
|
<li><b>Works</b> with Fieldseeker</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue