initial commit
This commit is contained in:
6
.env example
Normal file
6
.env example
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
OPENWEATHER_API_KEY=1234567890
|
||||||
|
OPENWEATHER_LAT=34.052235
|
||||||
|
OPENWEATHER_LON=-118.243683
|
||||||
|
PASSPORT_ADMIN_USERNAME=admin
|
||||||
|
PASSPORT_ADMIN_PASSWORD=P@ssw0rd
|
||||||
|
PASSPORT_SEARCH_PROVIDER=https://google.com/search?q=%s
|
||||||
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
passport
|
||||||
|
.env
|
||||||
|
passport.db
|
||||||
|
public/uploads/
|
||||||
23
LICENSE
Normal file
23
LICENSE
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
Boost Software License - Version 1.0 - August 17th, 2003
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person or organization
|
||||||
|
obtaining a copy of the software and accompanying documentation covered by
|
||||||
|
this license (the "Software") to use, reproduce, display, distribute,
|
||||||
|
execute, and transmit the Software, and to prepare derivative works of the
|
||||||
|
Software, and to permit third-parties to whom the Software is furnished to
|
||||||
|
do so, all subject to the following:
|
||||||
|
|
||||||
|
The copyright notices in the Software and this entire statement, including
|
||||||
|
the above license grant, this restriction and the following disclaimer,
|
||||||
|
must be included in all copies of the Software, in whole or in part, and
|
||||||
|
all derivative works of the Software, unless such copies or derivative
|
||||||
|
works are solely in the form of machine-executable object code generated by
|
||||||
|
a source language processor.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||||
|
SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||||
|
FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||||
|
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
DEALINGS IN THE SOFTWARE.
|
||||||
31
README.md
Normal file
31
README.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# Passport
|
||||||
|
|
||||||
|
Passport is a simple, fast, and lightweight web dashboard/new tab replacement.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- [Go](https://go.dev/doc/install)
|
||||||
|
- [sqlite3](https://www.sqlite.org/download.html)
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
1. Clone the repository
|
||||||
|
2. Configure the `.env` file, an example is provided in the `.env example` file
|
||||||
|
- The `OPENWEATHER_API_KEY` is required for the weather data to be displayed
|
||||||
|
- The `OPENWEATHER_LAT` and `OPENWEATHER_LON` are required for the weather data to be displayed
|
||||||
|
- The `PASSPORT_ADMIN_USERNAME` and `PASSPORT_ADMIN_PASSWORD` are required for the admin dashboard
|
||||||
|
- The `PASSPORT_SEARCH_PROVIDER` is the search provider used for the search bar, %s is replaced with the search query
|
||||||
|
3. Run `sqlite3 passport.db < passport.sql` to create the database
|
||||||
|
4. Run `go build` to build the project
|
||||||
|
5. Deploy passport, passport.db and .env, and preferably the public folder (but you dont have to) to your web server
|
||||||
|
6. profit
|
||||||
|
|
||||||
|
### Adding links and categories
|
||||||
|
|
||||||
|
The admin dashboard can be accessed at `/admin`, you will be redirected to the login page if you are not logged in, use the credentials you configured in the `.env` file to login. Once logged in you can add links and categories.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the BSL-1.0 License - see the [LICENSE](LICENSE) file for details
|
||||||
BIN
fonts/InstrumentSans-Italic.ttf
Normal file
BIN
fonts/InstrumentSans-Italic.ttf
Normal file
Binary file not shown.
BIN
fonts/InstrumentSans-Regular.ttf
Normal file
BIN
fonts/InstrumentSans-Regular.ttf
Normal file
Binary file not shown.
BIN
fonts/InstrumentSans-SemiBold.ttf
Normal file
BIN
fonts/InstrumentSans-SemiBold.ttf
Normal file
Binary file not shown.
30
go.mod
Normal file
30
go.mod
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
module github.com/juls0730/passport
|
||||||
|
|
||||||
|
go 1.23.2
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/mailgun/raymond/v2 v2.0.48 // indirect
|
||||||
|
github.com/sirupsen/logrus v1.8.1 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||||
|
github.com/gofiber/fiber/v2 v2.52.5 // indirect
|
||||||
|
github.com/gofiber/fiber/v3 v3.0.0-beta.3
|
||||||
|
github.com/gofiber/template v1.8.3 // indirect
|
||||||
|
github.com/gofiber/template/handlebars/v2 v2.1.10
|
||||||
|
github.com/gofiber/utils v1.1.0 // indirect
|
||||||
|
github.com/gofiber/utils/v2 v2.0.0-beta.4 // indirect
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/joho/godotenv v1.5.1
|
||||||
|
github.com/klauspost/compress v1.17.9 // 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/mattn/go-sqlite3 v1.14.24
|
||||||
|
github.com/rivo/uniseg v0.2.0 // indirect
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
|
github.com/valyala/fasthttp v1.55.0 // indirect
|
||||||
|
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||||
|
golang.org/x/sys v0.21.0 // indirect
|
||||||
|
)
|
||||||
62
go.sum
Normal file
62
go.sum
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||||
|
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||||
|
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.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo=
|
||||||
|
github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
|
||||||
|
github.com/gofiber/fiber/v3 v3.0.0-beta.3 h1:7Q2I+HsIqnIEEDB+9oe7Gadpakh6ZLhXpTYz/L20vrg=
|
||||||
|
github.com/gofiber/fiber/v3 v3.0.0-beta.3/go.mod h1:kcMur0Dxqk91R7p4vxEpJfDWZ9u5IfvrtQc8Bvv/JmY=
|
||||||
|
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/handlebars/v2 v2.1.10 h1:Qc+uUMULCqW60LF4VKO1REpiyDAUy3vqW7xq66FPJGM=
|
||||||
|
github.com/gofiber/template/handlebars/v2 v2.1.10/go.mod h1:84WH9st5OJi255EGjuMAOqUVQ+Q2jUNhUKYbS5DgAcI=
|
||||||
|
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/gofiber/utils/v2 v2.0.0-beta.4 h1:1gjbVFFwVwUb9arPcqiB6iEjHBwo7cHsyS41NeIW3co=
|
||||||
|
github.com/gofiber/utils/v2 v2.0.0-beta.4/go.mod h1:sdRsPU1FXX6YiDGGxd+q2aPJRMzpsxdzCXo9dz+xtOY=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
|
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||||
|
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||||
|
github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw=
|
||||||
|
github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18=
|
||||||
|
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.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.24/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/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
|
||||||
|
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
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.55.0 h1:Zkefzgt6a7+bVKHnu/YaYSOPfNYNisSVBo/unVCf8k8=
|
||||||
|
github.com/valyala/fasthttp v1.55.0/go.mod h1:NkY9JtkrpPKmgwV3HTaS2HWaJss9RSIsRVfcxxoHiOM=
|
||||||
|
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-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
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.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
||||||
|
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
|
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=
|
||||||
527
main.go
Normal file
527
main.go
Normal file
@@ -0,0 +1,527 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"embed"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"log"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v3"
|
||||||
|
"github.com/gofiber/fiber/v3/middleware/static"
|
||||||
|
"github.com/gofiber/template/handlebars/v2"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
"github.com/juls0730/passport/middleware"
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed views/**
|
||||||
|
var viewsFS embed.FS
|
||||||
|
|
||||||
|
//go:embed fonts/**
|
||||||
|
var fontsFS embed.FS
|
||||||
|
|
||||||
|
type Category struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Icon string `json:"icon"`
|
||||||
|
Links []Link `json:"links"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Link struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
CategoryID int64 `json:"category_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Icon string `json:"icon"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type App struct {
|
||||||
|
db *sql.DB
|
||||||
|
*WeatherCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewApp(dbPath string) (*App, error) {
|
||||||
|
db, err := sql.Open("sqlite3", dbPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &App{
|
||||||
|
db: db,
|
||||||
|
WeatherCache: NewWeatherCache(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) GetCategories() ([]Category, error) {
|
||||||
|
rows, err := app.db.Query(`
|
||||||
|
SELECT id, name, icon
|
||||||
|
FROM categories
|
||||||
|
ORDER BY id ASC
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var categories []Category
|
||||||
|
for rows.Next() {
|
||||||
|
var cat Category
|
||||||
|
if err := rows.Scan(&cat.ID, &cat.Name, &cat.Icon); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
links, err := app.GetLinksByCategory(cat.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cat.Links = links
|
||||||
|
categories = append(categories, cat)
|
||||||
|
}
|
||||||
|
return categories, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) GetLinksByCategory(categoryID int64) ([]Link, error) {
|
||||||
|
rows, err := app.db.Query(`
|
||||||
|
SELECT id, category_id, name, description, icon, url
|
||||||
|
FROM links
|
||||||
|
WHERE category_id = ?
|
||||||
|
ORDER BY id ASC
|
||||||
|
`, categoryID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var links []Link
|
||||||
|
for rows.Next() {
|
||||||
|
var link Link
|
||||||
|
if err := rows.Scan(&link.ID, &link.CategoryID, &link.Name, &link.Description,
|
||||||
|
&link.Icon, &link.URL); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
links = append(links, link)
|
||||||
|
}
|
||||||
|
return links, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type OpenWeatherResponse struct {
|
||||||
|
Weather []struct {
|
||||||
|
Name string `json:"main"`
|
||||||
|
IconId string `json:"icon"`
|
||||||
|
} `json:"weather"`
|
||||||
|
Main struct {
|
||||||
|
Temp float64 `json:"temp"`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type WeatherData struct {
|
||||||
|
Temperature float64
|
||||||
|
WeatherText string
|
||||||
|
Icon string
|
||||||
|
}
|
||||||
|
|
||||||
|
type WeatherCache struct {
|
||||||
|
data *WeatherData
|
||||||
|
lastUpdate time.Time
|
||||||
|
mutex sync.RWMutex
|
||||||
|
updateChan chan struct{}
|
||||||
|
apiKey string
|
||||||
|
lat string
|
||||||
|
lon string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWeatherCache() *WeatherCache {
|
||||||
|
cache := &WeatherCache{
|
||||||
|
data: &WeatherData{},
|
||||||
|
updateChan: make(chan struct{}),
|
||||||
|
apiKey: os.Getenv("OPENWEATHER_API_KEY"),
|
||||||
|
lat: os.Getenv("OPENWEATHER_LAT"),
|
||||||
|
lon: os.Getenv("OPENWEATHER_LON"),
|
||||||
|
}
|
||||||
|
|
||||||
|
go cache.weatherWorker()
|
||||||
|
|
||||||
|
cache.updateChan <- struct{}{}
|
||||||
|
|
||||||
|
return cache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *WeatherCache) GetWeather() WeatherData {
|
||||||
|
c.mutex.RLock()
|
||||||
|
defer c.mutex.RUnlock()
|
||||||
|
return *c.data
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *WeatherCache) weatherWorker() {
|
||||||
|
ticker := time.NewTicker(30 * time.Minute)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-c.updateChan:
|
||||||
|
c.updateWeather()
|
||||||
|
case <-ticker.C:
|
||||||
|
c.updateWeather()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *WeatherCache) updateWeather() {
|
||||||
|
url := fmt.Sprintf("https://api.openweathermap.org/data/2.5/weather?lat=%s&lon=%s&appid=%s&units=metric",
|
||||||
|
c.lat, c.lon, c.apiKey)
|
||||||
|
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error fetching weather: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error reading response: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var weatherResp OpenWeatherResponse
|
||||||
|
if err := json.Unmarshal(body, &weatherResp); err != nil {
|
||||||
|
fmt.Printf("Error parsing weather data: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.mutex.Lock()
|
||||||
|
c.data.Temperature = weatherResp.Main.Temp
|
||||||
|
c.data.WeatherText = weatherResp.Weather[0].Name
|
||||||
|
c.data.Icon = weatherResp.Weather[0].IconId
|
||||||
|
c.lastUpdate = time.Now()
|
||||||
|
c.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateLinkRequest struct {
|
||||||
|
Name string `form:"name"`
|
||||||
|
Description string `form:"description"`
|
||||||
|
URL string `form:"url"`
|
||||||
|
Icon *multipart.FileHeader `form:"icon"`
|
||||||
|
CategoryID int64 `form:"category_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateLink(db *sql.DB) fiber.Handler {
|
||||||
|
return func(c fiber.Ctx) error {
|
||||||
|
var req CreateLinkRequest
|
||||||
|
if err := c.Bind().MultipartForm(&req); err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||||
|
"message": "Failed to parse request",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Name == "" || req.URL == "" {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||||
|
"message": "Name and URL are required",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := c.FormFile("icon")
|
||||||
|
if err != nil || file == nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||||
|
"message": "Icon is required",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if file.Size > 5*1024*1024 {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||||
|
"message": "File size too large. Maximum size is 5MB",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType := file.Header.Get("Content-Type")
|
||||||
|
if !strings.HasPrefix(contentType, "image/") {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||||
|
"message": "Only image files are allowed",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
assetsDir := "public/uploads"
|
||||||
|
|
||||||
|
ext := filepath.Ext(file.Filename)
|
||||||
|
filename := fmt.Sprintf("%d_%s%s", time.Now().Unix(), strings.ReplaceAll(req.Name, " ", "_"), ext)
|
||||||
|
iconPath := filepath.Join(assetsDir, filename)
|
||||||
|
|
||||||
|
if err := c.SaveFile(file, iconPath); err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||||
|
"message": "Failed to save file",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
iconPath = "/uploads/" + filename
|
||||||
|
|
||||||
|
link := Link{
|
||||||
|
Name: req.Name,
|
||||||
|
Description: req.Description,
|
||||||
|
URL: req.URL,
|
||||||
|
Icon: iconPath,
|
||||||
|
CategoryID: req.CategoryID,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.Exec(`
|
||||||
|
INSERT INTO links (category_id, name, description, icon, url)
|
||||||
|
VALUES (?, ?, ?, ?, ?)`,
|
||||||
|
link.CategoryID, link.Name, link.Description, link.Icon, link.URL)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||||
|
"message": "Failed to create link",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusCreated).JSON(fiber.Map{
|
||||||
|
"message": "Link created successfully",
|
||||||
|
"link": link,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateCategoryRequest struct {
|
||||||
|
Name string `form:"name"`
|
||||||
|
Icon *multipart.FileHeader `form:"icon"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateCategory(db *sql.DB) fiber.Handler {
|
||||||
|
return func(c fiber.Ctx) error {
|
||||||
|
var req CreateCategoryRequest
|
||||||
|
if err := c.Bind().MultipartForm(&req); err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||||
|
"message": "Failed to parse request",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Name == "" {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||||
|
"message": "Name is required",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := c.FormFile("icon")
|
||||||
|
if err != nil || file == nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||||
|
"message": "Icon is required",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if file.Size > 5*1024*1024 {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||||
|
"message": "File size too large. Maximum size is 5MB",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType := file.Header.Get("Content-Type")
|
||||||
|
if !strings.HasPrefix(contentType, "image/") {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||||
|
"message": "Only image files are allowed",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
assetsDir := "public/uploads"
|
||||||
|
|
||||||
|
ext := filepath.Ext(file.Filename)
|
||||||
|
filename := fmt.Sprintf("%d_%s%s", time.Now().Unix(), strings.ReplaceAll(req.Name, " ", "_"), ext)
|
||||||
|
iconPath := filepath.Join(assetsDir, filename)
|
||||||
|
|
||||||
|
if err := c.SaveFile(file, iconPath); err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||||
|
"message": "Failed to save file",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
iconPath = "/uploads/" + filename
|
||||||
|
|
||||||
|
category := Category{
|
||||||
|
Name: req.Name,
|
||||||
|
Icon: iconPath,
|
||||||
|
Links: []Link{},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.Exec(`
|
||||||
|
INSERT INTO categories (name, icon)
|
||||||
|
VALUES (?, ?)`,
|
||||||
|
category.Name, category.Icon)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||||
|
"message": "Failed to create category",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusCreated).JSON(fiber.Map{
|
||||||
|
"message": "Category created successfully",
|
||||||
|
"category": category,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var WeatherIcons = map[string]string{
|
||||||
|
"01d": `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="currentColor" d="M16 12.005a4 4 0 1 1-4 4a4.005 4.005 0 0 1 4-4m0-2a6 6 0 1 0 6 6a6 6 0 0 0-6-6M5.394 6.813L6.81 5.399l3.505 3.506L8.9 10.319zM2 15.005h5v2H2zm3.394 10.193L8.9 21.692l1.414 1.414l-3.505 3.506zM15 25.005h2v5h-2zm6.687-1.9l1.414-1.414l3.506 3.506l-1.414 1.414zm3.313-8.1h5v2h-5zm-3.313-6.101l3.506-3.506l1.414 1.414l-3.506 3.506zM15 2.005h2v5h-2z"/></svg>`,
|
||||||
|
"01n": `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="currentColor" d="M13.503 5.414a15.076 15.076 0 0 0 11.593 18.194a11.1 11.1 0 0 1-7.975 3.39c-.138 0-.278.005-.418 0a11.094 11.094 0 0 1-3.2-21.584M14.98 3a1 1 0 0 0-.175.016a13.096 13.096 0 0 0 1.825 25.981c.164.006.328 0 .49 0a13.07 13.07 0 0 0 10.703-5.555a1.01 1.01 0 0 0-.783-1.565A13.08 13.08 0 0 1 15.89 4.38A1.015 1.015 0 0 0 14.98 3"/></svg>`,
|
||||||
|
"02d": `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="currentColor" d="M27 15h4v2h-4zm-4-7.413l3-3L27.415 6l-3 3zM15 1h2v4h-2zM4.586 26l3-3l1.415 1.415l-3 3zM4.585 6L6 4.587l3 3l-1.414 1.415z"/><path fill="currentColor" d="M1 15h4v2H1zm25.794 5.342a6.96 6.96 0 0 0-1.868-3.267A9 9 0 0 0 25 16a9 9 0 1 0-14.585 7.033A4.977 4.977 0 0 0 15 30h10a4.995 4.995 0 0 0 1.794-9.658M9 16a6.996 6.996 0 0 1 13.985-.297A6.9 6.9 0 0 0 20 15a7.04 7.04 0 0 0-6.794 5.342a5 5 0 0 0-1.644 1.048A6.97 6.97 0 0 1 9 16m16 12H15a2.995 2.995 0 0 1-.696-5.908l.658-.157l.099-.67a4.992 4.992 0 0 1 9.878 0l.099.67l.658.156A2.995 2.995 0 0 1 25 28"/></svg>`,
|
||||||
|
"02n": `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="currentColor" d="M30 19a4.97 4.97 0 0 0-3.206-4.658A6.971 6.971 0 0 0 13.758 12.9a13.14 13.14 0 0 1 .131-8.52A1.015 1.015 0 0 0 12.98 3a1 1 0 0 0-.175.016a13.096 13.096 0 0 0 1.825 25.981c.164.006.328 0 .49 0a13.04 13.04 0 0 0 10.29-5.038A4.99 4.99 0 0 0 30 19m-15.297 7.998a11.095 11.095 0 0 1-3.2-21.584a15.2 15.2 0 0 0 .844 9.367A4.988 4.988 0 0 0 15 24h7.677a11.1 11.1 0 0 1-7.556 2.998c-.138 0-.278.004-.418 0M25 22H15a2.995 2.995 0 0 1-.696-5.908l.658-.157l.099-.67a4.992 4.992 0 0 1 9.878 0l.099.67l.658.157A2.995 2.995 0 0 1 25 22"/></svg>`,
|
||||||
|
"03d": `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="currentColor" d="M27 15h4v2h-4zm-4-7.413l3-3L27.415 6l-3 3zM15 1h2v4h-2zM4.586 26l3-3l1.415 1.415l-3 3zM4.585 6L6 4.587l3 3l-1.414 1.415z"/><path fill="currentColor" d="M1 15h4v2H1zm25.794 5.342a6.96 6.96 0 0 0-1.868-3.267A9 9 0 0 0 25 16a9 9 0 1 0-14.585 7.033A4.977 4.977 0 0 0 15 30h10a4.995 4.995 0 0 0 1.794-9.658M9 16a6.996 6.996 0 0 1 13.985-.297A6.9 6.9 0 0 0 20 15a7.04 7.04 0 0 0-6.794 5.342a5 5 0 0 0-1.644 1.048A6.97 6.97 0 0 1 9 16m16 12H15a2.995 2.995 0 0 1-.696-5.908l.658-.157l.099-.67a4.992 4.992 0 0 1 9.878 0l.099.67l.658.156A2.995 2.995 0 0 1 25 28"/></svg>`,
|
||||||
|
"03n": `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="currentColor" d="M30 19a4.97 4.97 0 0 0-3.206-4.658A6.971 6.971 0 0 0 13.758 12.9a13.14 13.14 0 0 1 .131-8.52A1.015 1.015 0 0 0 12.98 3a1 1 0 0 0-.175.016a13.096 13.096 0 0 0 1.825 25.981c.164.006.328 0 .49 0a13.04 13.04 0 0 0 10.29-5.038A4.99 4.99 0 0 0 30 19m-15.297 7.998a11.095 11.095 0 0 1-3.2-21.584a15.2 15.2 0 0 0 .844 9.367A4.988 4.988 0 0 0 15 24h7.677a11.1 11.1 0 0 1-7.556 2.998c-.138 0-.278.004-.418 0M25 22H15a2.995 2.995 0 0 1-.696-5.908l.658-.157l.099-.67a4.992 4.992 0 0 1 9.878 0l.099.67l.658.157A2.995 2.995 0 0 1 25 22"/></svg>`,
|
||||||
|
"04d": `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="currentColor" d="M21.743 18.692a6 6 0 0 0 1.057-1.086a5.998 5.998 0 1 0-10.733-4.445A7.56 7.56 0 0 0 6.35 18.25A5.993 5.993 0 0 0 8 30.005h11a5.985 5.985 0 0 0 2.743-11.313M18 10.005a4.004 4.004 0 0 1 4 4a3.96 3.96 0 0 1-.8 2.4a4 4 0 0 1-.94.891a7.54 7.54 0 0 0-6.134-4.24A4 4 0 0 1 18 10.006m1 18H8a3.993 3.993 0 0 1-.673-7.93l.663-.112l.146-.656a5.496 5.496 0 0 1 10.729 0l.146.656l.662.112a3.993 3.993 0 0 1-.673 7.93m7-15.001h4v2h-4zM22.95 7.64l2.828-2.827l1.415 1.414l-2.829 2.828zM17 2.005h2v4h-2zM8.808 6.227l1.414-1.414l2.829 2.828l-1.415 1.414z"/></svg>`,
|
||||||
|
"04n": `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="currentColor" d="M29.844 15.035a1.52 1.52 0 0 0-1.23-.866a5.36 5.36 0 0 1-3.41-1.716a6.47 6.47 0 0 1-1.286-6.392a1.6 1.6 0 0 0-.299-1.546a1.45 1.45 0 0 0-1.36-.493l-.019.003a7.93 7.93 0 0 0-6.22 7.431A7.4 7.4 0 0 0 13.5 11a7.55 7.55 0 0 0-7.15 5.244A5.993 5.993 0 0 0 8 28h11a5.977 5.977 0 0 0 5.615-8.088a7.5 7.5 0 0 0 5.132-3.357a1.54 1.54 0 0 0 .097-1.52M19 26H8a3.993 3.993 0 0 1-.673-7.93l.663-.112l.145-.656a5.496 5.496 0 0 1 10.73 0l.145.656l.663.113A3.993 3.993 0 0 1 19 26m4.465-8.001h-.021a5.96 5.96 0 0 0-2.795-1.755a7.5 7.5 0 0 0-2.6-3.677c-.01-.101-.036-.197-.041-.3a6.08 6.08 0 0 1 3.79-6.05a8.46 8.46 0 0 0 1.94 7.596a7.4 7.4 0 0 0 3.902 2.228a5.43 5.43 0 0 1-4.175 1.958"/></svg>`,
|
||||||
|
"09d": `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="currentColor" d="M11 30a1 1 0 0 1-.894-1.447l2-4a1 1 0 1 1 1.788.894l-2 4A1 1 0 0 1 11 30"/><path fill="currentColor" d="M24.8 9.136a8.994 8.994 0 0 0-17.6 0A6.497 6.497 0 0 0 8.5 22h10.881l-1.276 2.553a1 1 0 0 0 1.789.894L21.618 22H23.5a6.497 6.497 0 0 0 1.3-12.864M23.5 20h-15a4.498 4.498 0 0 1-.356-8.981l.816-.064l.099-.812a6.994 6.994 0 0 1 13.883 0l.099.812l.815.064A4.498 4.498 0 0 1 23.5 20"/></svg>`,
|
||||||
|
"09n": `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="currentColor" d="M11 30a1 1 0 0 1-.894-1.447l2-4a1 1 0 1 1 1.788.894l-2 4A1 1 0 0 1 11 30"/><path fill="currentColor" d="M24.8 9.136a8.994 8.994 0 0 0-17.6 0A6.497 6.497 0 0 0 8.5 22h10.881l-1.276 2.553a1 1 0 0 0 1.789.894L21.618 22H23.5a6.497 6.497 0 0 0 1.3-12.864M23.5 20h-15a4.498 4.498 0 0 1-.356-8.981l.816-.064l.099-.812a6.994 6.994 0 0 1 13.883 0l.099.812l.815.064A4.498 4.498 0 0 1 23.5 20"/></svg>`,
|
||||||
|
"10d": `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="currentColor" d="M23.5 22h-15A6.5 6.5 0 0 1 7.2 9.14a9 9 0 0 1 17.6 0A6.5 6.5 0 0 1 23.5 22M16 4a7 7 0 0 0-6.94 6.14L9 11h-.86a4.5 4.5 0 0 0 .36 9h15a4.5 4.5 0 0 0 .36-9H23l-.1-.82A7 7 0 0 0 16 4m-2 26a.93.93 0 0 1-.45-.11a1 1 0 0 1-.44-1.34l2-4a1 1 0 1 1 1.78.9l-2 4A1 1 0 0 1 14 30m6 0a.93.93 0 0 1-.45-.11a1 1 0 0 1-.44-1.34l2-4a1 1 0 1 1 1.78.9l-2 4A1 1 0 0 1 20 30M8 30a.93.93 0 0 1-.45-.11a1 1 0 0 1-.44-1.34l2-4a1 1 0 1 1 1.78.9l-2 4A1 1 0 0 1 8 30"/></svg>`,
|
||||||
|
"10n": `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="currentColor" d="M23.5 22h-15A6.5 6.5 0 0 1 7.2 9.14a9 9 0 0 1 17.6 0A6.5 6.5 0 0 1 23.5 22M16 4a7 7 0 0 0-6.94 6.14L9 11h-.86a4.5 4.5 0 0 0 .36 9h15a4.5 4.5 0 0 0 .36-9H23l-.1-.82A7 7 0 0 0 16 4m-2 26a.93.93 0 0 1-.45-.11a1 1 0 0 1-.44-1.34l2-4a1 1 0 1 1 1.78.9l-2 4A1 1 0 0 1 14 30m6 0a.93.93 0 0 1-.45-.11a1 1 0 0 1-.44-1.34l2-4a1 1 0 1 1 1.78.9l-2 4A1 1 0 0 1 20 30M8 30a.93.93 0 0 1-.45-.11a1 1 0 0 1-.44-1.34l2-4a1 1 0 1 1 1.78.9l-2 4A1 1 0 0 1 8 30"/></svg>`,
|
||||||
|
"11d": `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="currentColor" d="M21 30a1 1 0 0 1-.894-1.447l2-4a1 1 0 1 1 1.788.894l-2 4A1 1 0 0 1 21 30M9 32a1 1 0 0 1-.894-1.447l2-4a1 1 0 1 1 1.788.894l-2 4A1 1 0 0 1 9 32m6.901-1.504l-1.736-.992L17.31 24h-6l4.855-8.496l1.736.992L14.756 22h6.001z"/><path fill="currentColor" d="M24.8 9.136a8.994 8.994 0 0 0-17.6 0a6.493 6.493 0 0 0 .23 12.768l-1.324 2.649a1 1 0 1 0 1.789.894l2-4a1 1 0 0 0-.447-1.341A1 1 0 0 0 9 20.01V20h-.5a4.498 4.498 0 0 1-.356-8.981l.816-.064l.099-.812a6.994 6.994 0 0 1 13.883 0l.099.812l.815.064A4.498 4.498 0 0 1 23.5 20H23v2h.5a6.497 6.497 0 0 0 1.3-12.864"/></svg>`,
|
||||||
|
"11n": `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="currentColor" d="M21 30a1 1 0 0 1-.894-1.447l2-4a1 1 0 1 1 1.788.894l-2 4A1 1 0 0 1 21 30M9 32a1 1 0 0 1-.894-1.447l2-4a1 1 0 1 1 1.788.894l-2 4A1 1 0 0 1 9 32m6.901-1.504l-1.736-.992L17.31 24h-6l4.855-8.496l1.736.992L14.756 22h6.001z"/><path fill="currentColor" d="M24.8 9.136a8.994 8.994 0 0 0-17.6 0a6.493 6.493 0 0 0 .23 12.768l-1.324 2.649a1 1 0 1 0 1.789.894l2-4a1 1 0 0 0-.447-1.341A1 1 0 0 0 9 20.01V20h-.5a4.498 4.498 0 0 1-.356-8.981l.816-.064l.099-.812a6.994 6.994 0 0 1 13.883 0l.099.812l.815.064A4.498 4.498 0 0 1 23.5 20H23v2h.5a6.497 6.497 0 0 0 1.3-12.864"/></svg>`,
|
||||||
|
"13d": `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="currentColor" d="M23.5 22h-15A6.5 6.5 0 0 1 7.2 9.14a9 9 0 0 1 17.6 0A6.5 6.5 0 0 1 23.5 22M16 4a7 7 0 0 0-6.94 6.14L9 11h-.86a4.5 4.5 0 0 0 .36 9h15a4.5 4.5 0 0 0 .36-9H23l-.1-.82A7 7 0 0 0 16 4m-4 21.05L10.95 24L9.5 25.45L8.05 24L7 25.05l1.45 1.45L7 27.95L8.05 29l1.45-1.45L10.95 29L12 27.95l-1.45-1.45zm14 0L24.95 24l-1.45 1.45L22.05 24L21 25.05l1.45 1.45L21 27.95L22.05 29l1.45-1.45L24.95 29L26 27.95l-1.45-1.45zm-7 2L17.95 26l-1.45 1.45L15.05 26L14 27.05l1.45 1.45L14 29.95L15.05 31l1.45-1.45L17.95 31L19 29.95l-1.45-1.45z"/></svg>`,
|
||||||
|
"13n": `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="currentColor" d="M23.5 22h-15A6.5 6.5 0 0 1 7.2 9.14a9 9 0 0 1 17.6 0A6.5 6.5 0 0 1 23.5 22M16 4a7 7 0 0 0-6.94 6.14L9 11h-.86a4.5 4.5 0 0 0 .36 9h15a4.5 4.5 0 0 0 .36-9H23l-.1-.82A7 7 0 0 0 16 4m-4 21.05L10.95 24L9.5 25.45L8.05 24L7 25.05l1.45 1.45L7 27.95L8.05 29l1.45-1.45L10.95 29L12 27.95l-1.45-1.45zm14 0L24.95 24l-1.45 1.45L22.05 24L21 25.05l1.45 1.45L21 27.95L22.05 29l1.45-1.45L24.95 29L26 27.95l-1.45-1.45zm-7 2L17.95 26l-1.45 1.45L15.05 26L14 27.05l1.45 1.45L14 29.95L15.05 31l1.45-1.45L17.95 31L19 29.95l-1.45-1.45z"/></svg>`,
|
||||||
|
"50d": `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="currentColor" d="M24.8 11.138a8.994 8.994 0 0 0-17.6 0A6.53 6.53 0 0 0 2 17.5V19a1 1 0 0 0 1 1h12a1 1 0 0 0 0-2H4v-.497a4.52 4.52 0 0 1 4.144-4.482l.816-.064l.099-.812a6.994 6.994 0 0 1 13.883 0l.099.813l.815.063A4.496 4.496 0 0 1 23.5 22H7a1 1 0 0 0 0 2h16.5a6.496 6.496 0 0 0 1.3-12.862"/><rect width="18" height="2" x="2" y="26" fill="currentColor" rx="1"/></svg>`,
|
||||||
|
"50n": `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="currentColor" d="M24.8 11.138a8.994 8.994 0 0 0-17.6 0A6.53 6.53 0 0 0 2 17.5V19a1 1 0 0 0 1 1h12a1 1 0 0 0 0-2H4v-.497a4.52 4.52 0 0 1 4.144-4.482l.816-.064l.099-.812a6.994 6.994 0 0 1 13.883 0l.099.813l.815.063A4.496 4.496 0 0 1 23.5 22H7a1 1 0 0 0 0 2h16.5a6.496 6.496 0 0 0 1.3-12.862"/><rect width="18" height="2" x="2" y="26" fill="currentColor" rx="1"/></svg>`,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
if err := godotenv.Load(); err != nil {
|
||||||
|
fmt.Println("No .env file found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if err := os.MkdirAll("public/uploads", 0755); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
app, err := NewApp("passport.db")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
viewsDir, err := fs.Sub(viewsFS, "views")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
engine := handlebars.NewFileSystem(http.FS(viewsDir), ".hbs")
|
||||||
|
|
||||||
|
router := fiber.New(fiber.Config{
|
||||||
|
Views: engine,
|
||||||
|
})
|
||||||
|
|
||||||
|
router.Use("/", static.New("./public"))
|
||||||
|
|
||||||
|
router.Use("/fonts", static.New("", static.Config{
|
||||||
|
FS: fontsFS,
|
||||||
|
}))
|
||||||
|
|
||||||
|
router.Get("/", func(c fiber.Ctx) error {
|
||||||
|
categories, err := app.GetCategories()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
weather := app.WeatherCache.GetWeather()
|
||||||
|
|
||||||
|
return c.Render("index", fiber.Map{
|
||||||
|
"SearchProvider": os.Getenv("PASSPORT_SEARCH_PROVIDER"),
|
||||||
|
"WeatherData": fiber.Map{
|
||||||
|
"Temp": weather.Temperature,
|
||||||
|
"Desc": weather.WeatherText,
|
||||||
|
"Icon": WeatherIcons[weather.Icon],
|
||||||
|
},
|
||||||
|
"Categories": categories,
|
||||||
|
}, "layouts/main")
|
||||||
|
})
|
||||||
|
|
||||||
|
router.Get("/admin/login", func(c fiber.Ctx) error {
|
||||||
|
return c.Render("admin/login", fiber.Map{}, "layouts/main")
|
||||||
|
})
|
||||||
|
|
||||||
|
router.Post("/admin/login", func(c fiber.Ctx) error {
|
||||||
|
var loginData struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
if err := c.Bind().JSON(&loginData); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if loginData.Username != os.Getenv("PASSPORT_ADMIN_USERNAME") || loginData.Password != os.Getenv("PASSPORT_ADMIN_PASSWORD") {
|
||||||
|
return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"message": "Invalid username or password"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new session
|
||||||
|
sessionID := uuid.NewString()
|
||||||
|
expiresAt := time.Now().Add(time.Hour * 24 * 7)
|
||||||
|
_, err := app.db.Exec(`
|
||||||
|
INSERT INTO sessions (session_id, expires_at)
|
||||||
|
VALUES (?, ?)
|
||||||
|
`, sessionID, expiresAt)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set cookie
|
||||||
|
c.Cookie(&fiber.Cookie{
|
||||||
|
Name: "SessionToken",
|
||||||
|
Value: sessionID,
|
||||||
|
Expires: expiresAt,
|
||||||
|
})
|
||||||
|
|
||||||
|
return c.Status(http.StatusOK).JSON(fiber.Map{"message": "Logged in successfully"})
|
||||||
|
})
|
||||||
|
|
||||||
|
router.Use(middleware.AdminMiddleware(app.db))
|
||||||
|
|
||||||
|
router.Get("/admin", func(c fiber.Ctx) error {
|
||||||
|
categories, err := app.GetCategories()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Render("admin/index", fiber.Map{
|
||||||
|
"Categories": categories,
|
||||||
|
}, "layouts/main")
|
||||||
|
})
|
||||||
|
|
||||||
|
api := router.Group("/api")
|
||||||
|
{
|
||||||
|
api.Post("/categories", CreateCategory(app.db))
|
||||||
|
api.Post("/links", CreateLink(app.db))
|
||||||
|
|
||||||
|
api.Delete("/links/:id", func(c fiber.Ctx) error {
|
||||||
|
id := c.Params("id")
|
||||||
|
_, err := app.db.Exec("DELETE FROM links WHERE id = ?", id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.SendStatus(fiber.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
api.Delete("/categories/:id", func(c fiber.Ctx) error {
|
||||||
|
id := c.Params("id")
|
||||||
|
_, err := app.db.Exec("DELETE FROM categories WHERE id = ?", id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = app.db.Exec("DELETE FROM links WHERE category_id = ?", id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.SendStatus(fiber.StatusOK)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
router.Listen(":3000")
|
||||||
|
}
|
||||||
44
middleware/admin.go
Normal file
44
middleware/admin.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Session struct {
|
||||||
|
SessionID string `json:"session_id"`
|
||||||
|
ExpiresAt string `json:"expires_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func AdminMiddleware(db *sql.DB) func(c fiber.Ctx) error {
|
||||||
|
return func(c fiber.Ctx) error {
|
||||||
|
sessionToken := c.Cookies("SessionToken")
|
||||||
|
if sessionToken == "" {
|
||||||
|
return c.Redirect().To("/admin/login")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if session exists
|
||||||
|
var session Session
|
||||||
|
err := db.QueryRow(`
|
||||||
|
SELECT session_id, expires_at
|
||||||
|
FROM sessions
|
||||||
|
WHERE session_id = ?
|
||||||
|
`, sessionToken).Scan(&session.SessionID, &session.ExpiresAt)
|
||||||
|
if err != nil {
|
||||||
|
return c.Redirect().To("/admin/login")
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionExpiry, err := time.Parse("2006-01-02 15:04:05-07:00", session.ExpiresAt)
|
||||||
|
if err != nil {
|
||||||
|
return c.Redirect().To("/admin/login")
|
||||||
|
}
|
||||||
|
|
||||||
|
if sessionExpiry.Before(time.Now()) {
|
||||||
|
return c.Redirect().To("/admin/login")
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
BIN
public/leaves.jpg
Normal file
BIN
public/leaves.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 151 KiB |
20
schema.sql
Normal file
20
schema.sql
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
CREATE TABLE categories (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
icon TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE links (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
category_id INTEGER NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
description TEXT NOT NULL,
|
||||||
|
icon TEXT NOT NULL,
|
||||||
|
url TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE sessions (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
session_id TEXT NOT NULL,
|
||||||
|
expires_at TEXT NOT NULL
|
||||||
|
);
|
||||||
175
views/admin/index.hbs
Normal file
175
views/admin/index.hbs
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
<section class="flex justify-center w-full">
|
||||||
|
<div class="w-full sm:w-8/10 p-2.5">
|
||||||
|
{{#each Categories}}
|
||||||
|
<div class="flex items-center">
|
||||||
|
<img class="object-contain mr-2 select-none" width="32" height="32" draggable="false" src="{{this.Icon}}" />
|
||||||
|
<h2 class="capitalize">{{this.Name}}</h2>
|
||||||
|
<button onclick="deleteCategory({{this.ID}})" class="w-fit h-fit flex p-0.5 bg-[#1C1C21] border-solid border-[#211F23] rounded-md hover:bg-[#29292e] cursor-pointer"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path fill="none" stroke="#ff1919" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7h16m-10 4v6m4-6v6M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2l1-12M9 7V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v3"/></svg></button>
|
||||||
|
</div>
|
||||||
|
<div class="p-2.5 grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-2">
|
||||||
|
{{#each this.Links}}
|
||||||
|
<div class="rounded-2xl bg-[#211F23] p-2.5 flex flex-row items-center shadow-md hover:shadow-xl transition-[shadow,transform] ease-[cubic-bezier(0.16,1,0.3,1)] relative">
|
||||||
|
<img class="object-contain mr-2 select-none rounded-md" width="64" height="64" draggable="false" src="{{this.Icon}}" alt="{{this.Name}}" />
|
||||||
|
<div>
|
||||||
|
<h3>{{this.Name}}</h3>
|
||||||
|
<p class="text-[#D7D7D7]">{{this.Description}}</p>
|
||||||
|
</div>
|
||||||
|
<button onclick="deleteLink({{this.ID}})" class="w-fit h-fit flex p-0.5 bg-[#1C1C21] border-solid border-[#211F23] rounded-md hover:bg-[#29292e] cursor-pointer absolute right-1 top-1"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path fill="none" stroke="#ff1919" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7h16m-10 4v6m4-6v6M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2l1-12M9 7V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v3"/></svg></button>
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
<div id="link-add" onclick="addLink({{this.ID}})" class="rounded-2xl border border-dashed border-[#656565] p-2.5 flex flex-row items-center shadow-md hover:shadow-xl transition-[shadow,transform] ease-[cubic-bezier(0.16,1,0.3,1)] pointer-cursor select-none cursor-pointer">
|
||||||
|
<svg class="mr-2" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 5v14m-7-7h14"/></svg>
|
||||||
|
<div>
|
||||||
|
<h3>Add a link</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
<div class="flex items-center">
|
||||||
|
<svg class="mr-2" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 5v14m-7-7h14"/></svg>
|
||||||
|
<h2 id="add-category" class="text-[#656565] underline decoration-dashed cursor-pointer">Add a new category</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<div id="linkModal" class="hidden absolute top-0 left-0 bottom-0 right-0 bg-[#00000070] justify-center items-center">
|
||||||
|
<div class="bg-[#151316] rounded-xl overflow-hidden w-fit p-4">
|
||||||
|
<h3>Add A link</h3>
|
||||||
|
<form id="link-form" action="/api/links" method="post" class="flex flex-col gap-y-3 my-2">
|
||||||
|
<div>
|
||||||
|
<label for="linkName">Name</label>
|
||||||
|
<input class="px-4 py-2 rounded-md w-full bg-[#1C1C21] border border-[rgb(86,86,91)/30] text-white" type="text" name="name" placeholder="Name" id="linkName" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="linkDesc">Description</label>
|
||||||
|
<input class="px-4 py-2 rounded-md w-full bg-[#1C1C21] border border-[rgb(86,86,91)/30] text-white" type="text" name="description" placeholder="Description" id="linkDesc" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="linkURL">URL</label>
|
||||||
|
<input class="px-4 py-2 rounded-md w-full bg-[#1C1C21] border border-[rgb(86,86,91)/30] text-white" type="text" name="url" placeholder="URL" id="linkURL" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="linkIcon">Icon</label>
|
||||||
|
<input class="w-full text-white" type="file" name="icon" id="linkIcon" accept="image/*" />
|
||||||
|
</div>
|
||||||
|
<button class="px-4 py-2 rounded-md w-full bg-[#8A42FF] text-white border-0" type="submit">Add</button>
|
||||||
|
</form>
|
||||||
|
<span id="link-message"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="categoryModal" class="hidden absolute top-0 left-0 bottom-0 right-0 bg-[#00000070] justify-center items-center">
|
||||||
|
<div class="bg-[#151316] rounded-xl overflow-hidden w-fit p-4">
|
||||||
|
<h3>Create A category</h3>
|
||||||
|
<form id="category-form" action="/api/categories" method="post" class="flex flex-col gap-y-3 my-2">
|
||||||
|
<div>
|
||||||
|
<label for="categoryName">Name</label>
|
||||||
|
<input class="px-4 py-2 rounded-md w-full bg-[#1C1C21] border border-[rgb(86,86,91)/30] text-white" type="text" name="name" placeholder="Name" id="categoryName" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="linkIcon">Icon</label>
|
||||||
|
<input class="w-full text-white" type="file" name="icon" id="linkIcon" accept=".svg" />
|
||||||
|
</div>
|
||||||
|
<button class="px-4 py-2 rounded-md w-full bg-[#8A42FF] text-white border-0" type="submit">Create</button>
|
||||||
|
</form>
|
||||||
|
<span id="category-message"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// idfk what this variable capitalization is, it's a mess
|
||||||
|
let linkAdd = document.getElementById("link-add");
|
||||||
|
let linkModal = document.getElementById("linkModal");
|
||||||
|
let categoryModal = document.getElementById("categoryModal");
|
||||||
|
let categoryAdd = document.getElementById("add-category");
|
||||||
|
let targetCategoryID = null;
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
window.location.href = "/admin";
|
||||||
|
}
|
||||||
|
|
||||||
|
categoryAdd.addEventListener("click", () => {
|
||||||
|
categoryModal.classList.remove("hidden");
|
||||||
|
categoryModal.classList.add("flex");
|
||||||
|
});
|
||||||
|
|
||||||
|
function addLink(categoryID) {
|
||||||
|
targetCategoryID = categoryID;
|
||||||
|
linkModal.classList.remove("hidden");
|
||||||
|
linkModal.classList.add("flex");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteLink(linkID) {
|
||||||
|
let res = await fetch(`/api/links/${linkID}`, {
|
||||||
|
method: "DELETE"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 200) {
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteCategory(categoryID) {
|
||||||
|
let res = await fetch(`/api/categories/${categoryID}`, {
|
||||||
|
method: "DELETE"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 200) {
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("link-form").addEventListener("submit", async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
let data = new FormData(event.target);
|
||||||
|
|
||||||
|
data.append("category_id", targetCategoryID);
|
||||||
|
|
||||||
|
let res = await fetch(`/api/links`, {
|
||||||
|
method: "POST",
|
||||||
|
body: data
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 201) {
|
||||||
|
linkModal.classList.add("hidden");
|
||||||
|
linkModal.classList.remove("flex");
|
||||||
|
refresh();
|
||||||
|
} else {
|
||||||
|
let json = await res.json();
|
||||||
|
document.getElementById("category-message").innerText = json.message;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById("category-form").addEventListener("submit", async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
let data = new FormData(event.target);
|
||||||
|
|
||||||
|
let res = await fetch(`/api/categories`, {
|
||||||
|
method: "POST",
|
||||||
|
body: data
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 201) {
|
||||||
|
categoryModal.classList.add("hidden");
|
||||||
|
categoryModal.classList.remove("flex");
|
||||||
|
refresh();
|
||||||
|
} else {
|
||||||
|
let json = await res.json();
|
||||||
|
document.getElementById("link-message").innerText = json.message;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
linkModal.addEventListener("click", (event) => {
|
||||||
|
if (event.target === linkModal) {
|
||||||
|
targetCategoryID = null;
|
||||||
|
linkModal.classList.add("hidden");
|
||||||
|
linkModal.classList.remove("flex");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
categoryModal.addEventListener("click", (event) => {
|
||||||
|
if (event.target === categoryModal) {
|
||||||
|
targetCategoryID = null;
|
||||||
|
categoryModal.classList.add("hidden");
|
||||||
|
categoryModal.classList.remove("flex");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
45
views/admin/login.hbs
Normal file
45
views/admin/login.hbs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<main class="flex justify-center items-center h-100vh relative bg-[#0E0A0E]">
|
||||||
|
<div class="flex bg-[#151316] rounded-xl overflow-hidden">
|
||||||
|
<img src="/leaves.jpg" class="h-96 w-64 object-cover" />
|
||||||
|
<div class="flex flex-col p-4 text-center">
|
||||||
|
<h2 class="text-2xl">
|
||||||
|
Login
|
||||||
|
</h2>
|
||||||
|
<form action="/admin/login" method="post" class="flex flex-col gap-y-3 my-2">
|
||||||
|
<input class="px-4 py-2 rounded-md w-full bg-[#1C1C21] border border-[rgb(86,86,91)/30] text-white" type="text" name="username" placeholder="Username" />
|
||||||
|
<input class="px-4 py-2 rounded-md w-full bg-[#1C1C21] border border-[rgb(86,86,91)/30] text-white" type="password" name="password" placeholder="Password" />
|
||||||
|
<button class="px-4 py-2 rounded-md w-full bg-[#8A42FF] text-white border-0" type="submit">Login</button>
|
||||||
|
</form>
|
||||||
|
<span id="message"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let message = document.getElementById("message");
|
||||||
|
let form = document.querySelector("form");
|
||||||
|
form.addEventListener("submit", async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
let data = {
|
||||||
|
"username": form.username.value,
|
||||||
|
"password": form.password.value
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(data);
|
||||||
|
|
||||||
|
let res = await fetch("/admin/login", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 200) {
|
||||||
|
window.location.href = "/admin";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.innerText = (await res.json()).message;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
54
views/index.hbs
Normal file
54
views/index.hbs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<main class="flex justify-center items-center h-100vh relative bg-[#0E0A0E]">
|
||||||
|
<div class="absolute top-2.5 left-2.5">
|
||||||
|
<div class="text-[#BABABA] flex items-center">
|
||||||
|
<span class="mr-2 flex items-center">
|
||||||
|
{{{WeatherData.Icon}}}
|
||||||
|
</span>
|
||||||
|
<div class="font-semibold">
|
||||||
|
<p>{{WeatherData.Temp}}°C</p>
|
||||||
|
<p>{{WeatherData.Desc}}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-center w-full mx-6">
|
||||||
|
<div class="flex items-center pb-2.5">
|
||||||
|
<svg class="mr-3" width="60" height="60" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="12.1483" y="24.7693" width="70" height="47" rx="12" transform="rotate(14.63 12.1483 24.7693)" fill="url(#paint0_linear_20_10)"/><path fill-rule="evenodd" clip-rule="evenodd" d="M52.7386 13.4812C46.8869 10.3698 39.6209 12.5913 36.5096 18.4429L32.7819 25.4537L68.4322 34.7599C77.5166 37.1313 82.9586 46.418 80.5872 55.5025L74.7779 77.7567C74.7752 77.7674 74.7724 77.778 74.7696 77.7886C79.7728 78.7022 85.0029 76.3441 87.518 71.6138L98.3159 51.306C101.427 45.4543 99.2058 38.1883 93.3542 35.0769L52.7386 13.4812Z" fill="url(#paint1_linear_20_10)"/><defs><linearGradient id="paint0_linear_20_10" x1="12.359" y1="44.8681" x2="82.491" y2="48.2607" gradientUnits="userSpaceOnUse"><stop stop-color="#F0389B"/><stop offset="1" stop-color="#EEE740"/></linearGradient><linearGradient id="paint1_linear_20_10" x1="33.8935" y1="25.6926" x2="94.2236" y2="61.6131" gradientUnits="userSpaceOnUse"><stop stop-color="#AA38F0"/><stop offset="1" stop-color="#EE406A"/></linearGradient></defs></svg>
|
||||||
|
<h1>Passport</h1>
|
||||||
|
</div>
|
||||||
|
<input id="search-input" class="w-full max-w-3xl bg-[#1C1C21] border border-[rgb(86,86,91)/30] rounded-full px-3 py-1 text-white h-7" placeholder="Search..." />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<section class="flex justify-center w-full">
|
||||||
|
<div class="w-full sm:w-8/10 p-2.5">
|
||||||
|
{{#each Categories}}
|
||||||
|
<div class="flex items-center w-fit">
|
||||||
|
<img class="object-contain mr-2 select-none" width="32" height="32" draggable="false" src="{{this.Icon}}" />
|
||||||
|
<h2 class="capitalize w-fit">{{this.Name}}</h2>
|
||||||
|
</div>
|
||||||
|
<div class="p-2.5 grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-2">
|
||||||
|
{{#each this.Links}}
|
||||||
|
<a href="{{this.URL}}" class="underline-none text-unset" draggable="false" target="_blank" rel="noopener noreferrer">
|
||||||
|
<div class="rounded-2xl bg-[#211F23] p-2.5 flex flex-row items-center shadow-md hover:shadow-xl transition-[shadow,transform] ease-[cubic-bezier(0.16,1,0.3,1)] hover:-translate-y-1">
|
||||||
|
<img class="object-contain mr-2 select-none rounded-md" width="64" height="64" draggable="false" src="{{this.Icon}}" alt="{{this.Name}}" />
|
||||||
|
<div>
|
||||||
|
<h3>{{this.Name}}</h3>
|
||||||
|
<p class="text-[#D7D7D7]">{{this.Description}}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let search_input = document.getElementById("search-input");
|
||||||
|
let search_provider = "{{ SearchProvider }}";
|
||||||
|
// on enter key press
|
||||||
|
search_input.addEventListener("keyup", function (event) {
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
window.location.href = search_provider.replace("%s", search_input.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
452
views/layouts/main.hbs
Normal file
452
views/layouts/main.hbs
Normal file
@@ -0,0 +1,452 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Passport</title>
|
||||||
|
<link rel="favicon" href="/favicon.ico">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<style>
|
||||||
|
@font-face {
|
||||||
|
font-family: "Instrument Sans";
|
||||||
|
src: url("/fonts/InstrumentSans-Regular.ttf");
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Instrument Sans";
|
||||||
|
src: url("/fonts/InstrumentSans-SemiBold.ttf");
|
||||||
|
font-weight: 600;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Instrument Sans";
|
||||||
|
src: url("/fonts/InstrumentSans-Italic.ttf");
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background-color: #151316;
|
||||||
|
color: white;
|
||||||
|
font-family: "Instrument Sans", sans-serif;
|
||||||
|
font-optical-sizing: auto;
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
font-variation-settings:
|
||||||
|
"wdth" 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-semibold {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-transparent {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-\[\#211F23\] {
|
||||||
|
background-color: #211F23;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-\[\#0E0A0E\] {
|
||||||
|
background-color: #0E0A0E;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-\[\#1C1C21\] {
|
||||||
|
background-color: #1C1C21;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover\:bg-\[\#29292e\]:hover {
|
||||||
|
background-color: #29292e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-\[\#151316\] {
|
||||||
|
background-color: #151316;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-\[\#8A42FF\] {
|
||||||
|
background-color: #8A42FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-\[\#00000070\] {
|
||||||
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-0 {
|
||||||
|
border-width: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border {
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-solid {
|
||||||
|
border-style: solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-dashed {
|
||||||
|
border-style: dashed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-\[rgb\(86\,86\,91\)\/30\] {
|
||||||
|
border-color: rgba(86, 86, 91, 0.30);
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-\[\#656565\] {
|
||||||
|
border-color: #656565;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-\[\#211F23\] {
|
||||||
|
border-color: #211F23;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rounded-full {
|
||||||
|
border-radius: 9999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.relative {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.absolute {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-cols-1 {
|
||||||
|
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.sm\:grid-cols-2 {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1280px) {
|
||||||
|
.xl\:grid-cols-3 {
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.gap-2 {
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gap-y-3 {
|
||||||
|
row-gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.justify-center {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items-center {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h-100vh {
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h-7 {
|
||||||
|
height: 1.752rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h-96 {
|
||||||
|
height: 24rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h-fit {
|
||||||
|
height: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-64 {
|
||||||
|
width: 16rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-full {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-fit {
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.sm\:w-8\/10 {
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.max-w-3xl {
|
||||||
|
max-width: 48rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-col {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mr-3 {
|
||||||
|
margin-right: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mr-2 {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.my-2 {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx-6 {
|
||||||
|
margin-left: 1.5rem;
|
||||||
|
margin-right: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-0\.5 {
|
||||||
|
padding: 0.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.px-2 {
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
padding-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.px-3 {
|
||||||
|
padding-left: 0.75rem;
|
||||||
|
padding-right: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.px-4 {
|
||||||
|
padding-left: 1rem;
|
||||||
|
padding-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.py-2 {
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.py-1 {
|
||||||
|
padding-top: 0.25rem;
|
||||||
|
padding-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pb-2\.5 {
|
||||||
|
padding-bottom: 0.625rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-2\.5 {
|
||||||
|
padding: 0.625rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-4 {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h-2 {
|
||||||
|
height: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-0 {
|
||||||
|
top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-0 {
|
||||||
|
left: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-0 {
|
||||||
|
bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-0 {
|
||||||
|
right: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-1 {
|
||||||
|
top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-1 {
|
||||||
|
right: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-2\.5 {
|
||||||
|
top: 0.625rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-2\.5 {
|
||||||
|
left: 0.625rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-\[\#BABABA\] {
|
||||||
|
color: #BABABA;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-\[\#D7D7D7\] {
|
||||||
|
color: #D7D7D7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-\[\#656565\] {
|
||||||
|
color: #656565;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-white {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-4xl {
|
||||||
|
font-size: 2.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.underline-none {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.underline {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.decoration-dashed {
|
||||||
|
text-decoration-style: dashed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-unset {
|
||||||
|
color: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-sm {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rounded-md {
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rounded-xl {
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rounded-2xl {
|
||||||
|
border-radius: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overflow-hidden {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shadow-md {
|
||||||
|
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover\:shadow-xl:hover {
|
||||||
|
box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.transition-\[shadow\,transform\] {
|
||||||
|
transition-property: box-shadow,transform;
|
||||||
|
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
transition-duration: 150ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ease-\[cubic-bezier\(0\.16\,1\,0\.3\,1\)\] {
|
||||||
|
transition-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.capitalize {
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover\:-translate-y-1:hover {
|
||||||
|
transform: translateY(-0.25rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.object-contain {
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.object-cover {
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-none {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cursor-pointer {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
textarea,
|
||||||
|
select {
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input::placeholder {
|
||||||
|
color: #656565;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
{{embed}}
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
425
views/styles/main.css
Normal file
425
views/styles/main.css
Normal file
@@ -0,0 +1,425 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: "Instrument Sans";
|
||||||
|
src: url("/fonts/InstrumentSans-Regular.ttf");
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Instrument Sans";
|
||||||
|
src: url("/fonts/InstrumentSans-SemiBold.ttf");
|
||||||
|
font-weight: 600;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Instrument Sans";
|
||||||
|
src: url("/fonts/InstrumentSans-Italic.ttf");
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background-color: #151316;
|
||||||
|
color: white;
|
||||||
|
font-family: "Instrument Sans", sans-serif;
|
||||||
|
font-optical-sizing: auto;
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
font-variation-settings:
|
||||||
|
"wdth" 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-semibold {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-transparent {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-\[\#211F23\] {
|
||||||
|
background-color: #211F23;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-\[\#0E0A0E\] {
|
||||||
|
background-color: #0E0A0E;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-\[\#1C1C21\] {
|
||||||
|
background-color: #1C1C21;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover\:bg-\[\#29292e\]:hover {
|
||||||
|
background-color: #29292e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-\[\#151316\] {
|
||||||
|
background-color: #151316;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-\[\#8A42FF\] {
|
||||||
|
background-color: #8A42FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-\[\#00000070\] {
|
||||||
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-0 {
|
||||||
|
border-width: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border {
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-solid {
|
||||||
|
border-style: solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-dashed {
|
||||||
|
border-style: dashed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-\[rgb\(86\,86\,91\)\/30\] {
|
||||||
|
border-color: rgba(86, 86, 91, 0.30);
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-\[\#656565\] {
|
||||||
|
border-color: #656565;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-\[\#211F23\] {
|
||||||
|
border-color: #211F23;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rounded-full {
|
||||||
|
border-radius: 9999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.relative {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.absolute {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-cols-3 {
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.gap-2 {
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gap-y-3 {
|
||||||
|
row-gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.justify-center {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items-center {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h-100vh {
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h-7 {
|
||||||
|
height: 1.752rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h-96 {
|
||||||
|
height: 24rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h-fit {
|
||||||
|
height: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-64 {
|
||||||
|
width: 16rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-full {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-fit {
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-\[700px\] {
|
||||||
|
width: 700px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.min-w-\[50vw\] {
|
||||||
|
min-width: 50vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.max-w-\[80vw\] {
|
||||||
|
max-width: 80vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-col {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mr-3 {
|
||||||
|
margin-right: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mr-2 {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.my-2 {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-0\.5 {
|
||||||
|
padding: 0.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.px-2 {
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
padding-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.px-3 {
|
||||||
|
padding-left: 0.75rem;
|
||||||
|
padding-right: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.px-4 {
|
||||||
|
padding-left: 1rem;
|
||||||
|
padding-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.py-2 {
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.py-1 {
|
||||||
|
padding-top: 0.25rem;
|
||||||
|
padding-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pb-2\.5 {
|
||||||
|
padding-bottom: 0.625rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-2\.5 {
|
||||||
|
padding: 0.625rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-4 {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h-2 {
|
||||||
|
height: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-8\/10 {
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-0 {
|
||||||
|
top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-0 {
|
||||||
|
left: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-0 {
|
||||||
|
bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-0 {
|
||||||
|
right: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-1 {
|
||||||
|
top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-1 {
|
||||||
|
right: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-2\.5 {
|
||||||
|
top: 0.625rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-2\.5 {
|
||||||
|
left: 0.625rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-\[\#BABABA\] {
|
||||||
|
color: #BABABA;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-\[\#D7D7D7\] {
|
||||||
|
color: #D7D7D7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-\[\#656565\] {
|
||||||
|
color: #656565;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-white {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-4xl {
|
||||||
|
font-size: 2.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.underline-none {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.underline {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.decoration-dashed {
|
||||||
|
text-decoration-style: dashed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-unset {
|
||||||
|
color: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-sm {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rounded-md {
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rounded-xl {
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rounded-2xl {
|
||||||
|
border-radius: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overflow-hidden {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shadow-md {
|
||||||
|
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover\:shadow-xl:hover {
|
||||||
|
box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.transition-\[shadow\,transform\] {
|
||||||
|
transition-property: box-shadow,transform;
|
||||||
|
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
transition-duration: 150ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ease-\[cubic-bezier\(0\.16\,1\,0\.3\,1\)\] {
|
||||||
|
transition-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.capitalize {
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover\:-translate-y-1:hover {
|
||||||
|
transform: translateY(-0.25rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.object-contain {
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.object-cover {
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-none {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cursor-pointer {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
textarea,
|
||||||
|
select {
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input::placeholder {
|
||||||
|
color: #656565;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user