mirror of
https://github.com/ordinary-dev/phoenix
synced 2024-09-19 19:30:28 +05:00
feat: export and import
The list of links can now be exported to a json file, and later imported back.
This commit is contained in:
parent
9a9a1af63e
commit
6396c160f2
|
@ -19,7 +19,8 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
input,
|
input,
|
||||||
button {
|
button,
|
||||||
|
textarea {
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
|
@ -35,11 +36,17 @@ button {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
input:focus,
|
input:focus,
|
||||||
input:hover,
|
input:hover,
|
||||||
button:active,
|
button:active,
|
||||||
button:hover,
|
button:hover,
|
||||||
button:focus {
|
button:focus,
|
||||||
|
textarea:focus,
|
||||||
|
textarea:hover {
|
||||||
border-color: #812abd;
|
border-color: #812abd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
31
assets/css/import.css
vendored
Normal file
31
assets/css/import.css
vendored
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
body {
|
||||||
|
padding: 2em 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 800px) {
|
||||||
|
body {
|
||||||
|
padding: 2em 10em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
max-width: 400px;
|
||||||
|
margin-top: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
margin-top: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
form textarea {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
form button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
|
@ -2,6 +2,21 @@ body {
|
||||||
padding: 2em 1em;
|
padding: 2em 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions a {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions a img {
|
||||||
|
filter: invert(100%);
|
||||||
|
}
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
package database
|
package database
|
||||||
|
|
||||||
type Group struct {
|
type Group struct {
|
||||||
ID int
|
ID int `json:"id"`
|
||||||
Name string
|
Name string `json:"name"`
|
||||||
Links []Link
|
Links []Link `json:"links"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetGroupsWithLinks() ([]Group, error) {
|
func GetGroupsWithLinks() ([]Group, error) {
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
package database
|
package database
|
||||||
|
|
||||||
type Link struct {
|
type Link struct {
|
||||||
ID int
|
ID int `json:"id"`
|
||||||
Name string
|
Name string `json:"name"`
|
||||||
Href string
|
Href string `json:"href"`
|
||||||
GroupID int
|
GroupID int `json:"-"`
|
||||||
Icon *string
|
Icon *string `json:"icon,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetLinksFromGroup(groupID int) ([]Link, error) {
|
func GetLinksFromGroup(groupID int) ([]Link, error) {
|
||||||
|
|
27
templates/import.html.tmpl
Normal file
27
templates/import.html.tmpl
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
{{template "head" .}}
|
||||||
|
<link rel="stylesheet" href="assets/css/import.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Import</h1>
|
||||||
|
|
||||||
|
<a href="/settings">Settings</a>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Importing does not erase existing links, but may create duplicates.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form action="/import" method="POST">
|
||||||
|
<label for="exportFile">JSON data</label>
|
||||||
|
<textarea
|
||||||
|
id="exportFile"
|
||||||
|
name="exportFile"
|
||||||
|
placeholder="{ groups: [] }"
|
||||||
|
required
|
||||||
|
></textarea>
|
||||||
|
<button type="submit">Submit</button>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -6,7 +6,18 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Settings</h1>
|
<h1>Settings</h1>
|
||||||
<a href="/">Main page</a>
|
|
||||||
|
<div class="actions">
|
||||||
|
<a href="/">
|
||||||
|
<img src="/assets/icons/solid/house.svg" width="20" height="20" /> Main page
|
||||||
|
</a>
|
||||||
|
<a href="/export">
|
||||||
|
<img src="/assets/icons/solid/file-export.svg" width="20" height="20" /> Export links
|
||||||
|
</a>
|
||||||
|
<a href="/import">
|
||||||
|
<img src="/assets/icons/solid/file-import.svg" width="20" height="20" /> Import links
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
{{range .groups}}
|
{{range .groups}}
|
||||||
<h2 id="group-{{.ID}}">Group "{{.Name}}"</h2>
|
<h2 id="group-{{.ID}}">Group "{{.Name}}"</h2>
|
||||||
|
|
29
views/pages/export.go
Normal file
29
views/pages/export.go
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
package pages
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/ordinary-dev/phoenix/database"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ExportFile struct {
|
||||||
|
Groups []database.Group `json:"groups"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func Export(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
groups, err := database.GetGroupsWithLinks()
|
||||||
|
if err != nil {
|
||||||
|
ShowError(w, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Header().Set("Content-Disposition", "attachment; filename=phoenix.json")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
|
||||||
|
enc := json.NewEncoder(w)
|
||||||
|
enc.Encode(&ExportFile{
|
||||||
|
Groups: groups,
|
||||||
|
})
|
||||||
|
}
|
38
views/pages/import.go
Normal file
38
views/pages/import.go
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
package pages
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/ordinary-dev/phoenix/database"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ImportPage(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
Render("import.html.tmpl", w, map[string]any{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Import(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var exportFile ExportFile
|
||||||
|
data := []byte(r.FormValue("exportFile"))
|
||||||
|
if err := json.Unmarshal(data, &exportFile); err != nil {
|
||||||
|
ShowError(w, http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, g := range exportFile.Groups {
|
||||||
|
if err := database.CreateGroup(&g); err != nil {
|
||||||
|
ShowError(w, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, l := range g.Links {
|
||||||
|
l.GroupID = g.ID
|
||||||
|
if err := database.CreateLink(&l); err != nil {
|
||||||
|
ShowError(w, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Redirect(w, r, "/", http.StatusFound)
|
||||||
|
}
|
|
@ -53,6 +53,11 @@ func GetHttpServer() (*http.Server, error) {
|
||||||
// Delete link.
|
// Delete link.
|
||||||
protectedMux.HandleFunc("POST /links/{id}/delete", pages.DeleteLink)
|
protectedMux.HandleFunc("POST /links/{id}/delete", pages.DeleteLink)
|
||||||
|
|
||||||
|
// Import-export
|
||||||
|
protectedMux.HandleFunc("GET /export", pages.Export)
|
||||||
|
protectedMux.HandleFunc("GET /import", pages.ImportPage)
|
||||||
|
protectedMux.HandleFunc("POST /import", pages.Import)
|
||||||
|
|
||||||
return &http.Server{
|
return &http.Server{
|
||||||
Addr: ":8080",
|
Addr: ":8080",
|
||||||
Handler: middleware.LoggingMiddleware(
|
Handler: middleware.LoggingMiddleware(
|
||||||
|
|
Loading…
Reference in a new issue