Cartella con file per la frontend, un makefile per costruire tutto e lista utenti con fuzzy search
This commit is contained in:
@@ -6,3 +6,10 @@ tags
|
|||||||
|
|
||||||
# Local files
|
# Local files
|
||||||
*.local*
|
*.local*
|
||||||
|
|
||||||
|
# NodeJS
|
||||||
|
dist/
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Don't version generated files
|
||||||
|
public/js/*.min.js
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
|
||||||
|
JS_SOURCES = $(wildcard frontend/src/*.js)
|
||||||
|
JS_OUTPUTS = $(patsubst frontend/src/%.js, public/js/%.min.js, $(JS_SOURCES))
|
||||||
|
SERVER_EXECUTABLE = phc-website-server
|
||||||
|
|
||||||
|
.PHONY: all
|
||||||
|
all: js go
|
||||||
|
|
||||||
|
#
|
||||||
|
# Build Frontend
|
||||||
|
#
|
||||||
|
|
||||||
|
.PHONY: js
|
||||||
|
js: $(JS_OUTPUTS)
|
||||||
|
@echo "Compiled Frontend"
|
||||||
|
|
||||||
|
public/js/%.min.js: frontend/src/%.js
|
||||||
|
cd frontend; rollup -c rollup.config.js
|
||||||
|
cp $(patsubst frontend/src/%.js, frontend/dist/%.min.js, $<) $@
|
||||||
|
|
||||||
|
#
|
||||||
|
# Build Server
|
||||||
|
#
|
||||||
|
|
||||||
|
.PHONY: go
|
||||||
|
go: $(SERVER_EXECUTABLE)
|
||||||
|
@echo "Compiled Server"
|
||||||
|
|
||||||
|
$(SERVER_EXECUTABLE):
|
||||||
|
go build -o $(SERVER_EXECUTABLE) .
|
||||||
|
|
||||||
|
.PHONY: debug
|
||||||
|
debug:
|
||||||
|
@echo "JS_SOURCES = $(JS_SOURCES)"
|
||||||
|
@echo "JS_OUTPUTS = $(JS_OUTPUTS)"
|
||||||
|
@echo "SERVER_EXECUTABLE = $(SERVER_EXECUTABLE)"
|
||||||
Generated
+1009
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"devDependencies": {
|
||||||
|
"@rollup/plugin-node-resolve": "^13.3.0",
|
||||||
|
"rollup": "^2.75.3",
|
||||||
|
"rollup-plugin-terser": "^7.0.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"alpinejs": "^3.10.2",
|
||||||
|
"fuse.js": "^6.6.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { defineConfig } from 'rollup'
|
||||||
|
|
||||||
|
import { nodeResolve } from '@rollup/plugin-node-resolve'
|
||||||
|
import { terser } from 'rollup-plugin-terser'
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
{
|
||||||
|
input: 'src/utenti.js',
|
||||||
|
output: {
|
||||||
|
file: 'dist/utenti.min.js',
|
||||||
|
format: 'es',
|
||||||
|
},
|
||||||
|
plugins: [terser(), nodeResolve()],
|
||||||
|
},
|
||||||
|
])
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import Alpine from 'alpinejs'
|
||||||
|
import Fuse from 'fuse.js'
|
||||||
|
|
||||||
|
const SHOW_MORE_INCREMENT = 15
|
||||||
|
const FUSE_OPTIONS = {
|
||||||
|
includeScore: true,
|
||||||
|
keys: [
|
||||||
|
'nome',
|
||||||
|
'cognome',
|
||||||
|
{ name: 'nomeCompleto', getFn: user => `${user.nome} ${user.cognome}` },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const SORT_MODES = {
|
||||||
|
chronological: () => 0,
|
||||||
|
name: (a, b) => (a.nome < b.nome ? -1 : 1),
|
||||||
|
surname: (a, b) => (a.cognome < b.cognome ? -1 : 1),
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSortedUserList(original, mode) {
|
||||||
|
return [...original].sort(SORT_MODES[mode])
|
||||||
|
}
|
||||||
|
|
||||||
|
Alpine.data('utenti', () => ({
|
||||||
|
searchField: '', // two-way binding for the search input field
|
||||||
|
sortMode: 'chronological', // two-way binding for the sorting mode
|
||||||
|
fetchedUsers: [], // hold complete user list
|
||||||
|
sortedUserBuffer: [], // Yet another buffer of the user list for the sort mode
|
||||||
|
fuse: new Fuse([], FUSE_OPTIONS), // current fuse instance, used to filter the list above
|
||||||
|
searchResultsBuffer: [], // stores the full current search
|
||||||
|
searchResults: [], // list to renderer on screen with a subset of the whole search results buffer
|
||||||
|
async init() {
|
||||||
|
// Get user list from server
|
||||||
|
const response = await fetch('/api/utenti')
|
||||||
|
this.fetchedUsers = await response.json()
|
||||||
|
|
||||||
|
// This will call the function "showMore()" when the user is near the end of the list
|
||||||
|
new IntersectionObserver(entries => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
console.log('Near the bottom of the page')
|
||||||
|
this.showMore()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}).observe(this.$refs.spinner)
|
||||||
|
|
||||||
|
// Initialize with an empty query
|
||||||
|
this.updateSortMode()
|
||||||
|
this.updateSearch()
|
||||||
|
},
|
||||||
|
showMore() {
|
||||||
|
// setTimeout(() => {
|
||||||
|
// Updates the final "searchResults" list with more items from the previous buffer
|
||||||
|
const newCount = this.searchResults.length + SHOW_MORE_INCREMENT
|
||||||
|
this.searchResults = this.searchResultsBuffer.slice(0, newCount)
|
||||||
|
// }, 250) // For fun
|
||||||
|
},
|
||||||
|
setResults(list) {
|
||||||
|
this.searchResultsBuffer = list.filter(
|
||||||
|
entry => entry.score === undefined || entry.score <= 0.25
|
||||||
|
)
|
||||||
|
this.searchResults = this.searchResultsBuffer.slice(0, SHOW_MORE_INCREMENT)
|
||||||
|
},
|
||||||
|
updateSortMode() {
|
||||||
|
this.sortedUserBuffer = getSortedUserList(this.fetchedUsers, this.sortMode)
|
||||||
|
this.fuse.setCollection(this.sortedUserBuffer)
|
||||||
|
this.updateSearch()
|
||||||
|
},
|
||||||
|
updateSearch() {
|
||||||
|
console.time('search')
|
||||||
|
if (this.searchField.trim().length === 0) {
|
||||||
|
// Reset the result list
|
||||||
|
this.setResults(this.sortedUserBuffer.map(user => ({ item: user })))
|
||||||
|
} else {
|
||||||
|
// Update the result list with the new results
|
||||||
|
this.setResults(this.fuse.search(this.searchField))
|
||||||
|
}
|
||||||
|
console.timeEnd('search')
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
Alpine.start()
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
// window.addEventListener('DOMContentLoaded', () => {
|
|
||||||
|
|
||||||
// })
|
|
||||||
+24
-1
@@ -713,11 +713,34 @@ form .field-set input {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.page-utenti .user-item .icon {
|
.page-utenti .user-item .icon {
|
||||||
width: 1.5rem;
|
width: 1.75rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
place-content: center;
|
place-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page-utenti .spinner {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
height: 5rem;
|
||||||
|
|
||||||
|
color: var(--fg);
|
||||||
|
|
||||||
|
animation: rotate 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotate {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.search {
|
.search {
|
||||||
margin: 2rem 0;
|
margin: 2rem 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type UserInfo struct {
|
type UserInfo struct {
|
||||||
Uid string `string:"uid"`
|
Uid string `json:"uid"`
|
||||||
Nome string `json:"nome"`
|
Nome string `json:"nome"`
|
||||||
Cognome string `json:"cognome"`
|
Cognome string `json:"cognome"`
|
||||||
IsMacchinista bool `json:"macchinista,omitempty"`
|
IsMacchinista bool `json:"macchinista,omitempty"`
|
||||||
|
|||||||
+23
-19
@@ -1,5 +1,5 @@
|
|||||||
{{template "base" .}} {{define "title"}}Utenti • PHC{{end}} {{define "body"}}
|
{{template "base" .}} {{define "title"}}Utenti • PHC{{end}} {{define "body"}}
|
||||||
<section>
|
<section x-data="utenti">
|
||||||
<h2>
|
<h2>
|
||||||
<i class="fas fa-users"></i>
|
<i class="fas fa-users"></i>
|
||||||
Lista degli Utenti
|
Lista degli Utenti
|
||||||
@@ -11,43 +11,47 @@
|
|||||||
</p>
|
</p>
|
||||||
<div class="search">
|
<div class="search">
|
||||||
<div class="compound padded">
|
<div class="compound padded">
|
||||||
<div class="icon">
|
<div class="icon" title="Ordina per">
|
||||||
<i class="fas fa-sort"></i>
|
<i class="fas fa-sort"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<select>
|
<select x-model="sortMode" @click="updateSortMode()">
|
||||||
<option value="name">Cronologico</option>
|
<option value="chronological">Cronologico</option>
|
||||||
<option value="name">Nome</option>
|
<option value="name">Nome</option>
|
||||||
<option value="name">Cognome</option>
|
<option value="surname">Cognome</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="compound">
|
<div class="compound">
|
||||||
<input type="text" id="search-field" placeholder="Cerca..." autocomplete="off" />
|
<input type="text" x-model="searchField" @input="updateSearch()" placeholder="Cerca..." autocomplete="off" />
|
||||||
<button class="icon">
|
<button class="icon" title="In realtà questo tasto è finto">
|
||||||
<i class="fas fa-search"></i>
|
<i class="fas fa-search"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="user-list card-list">
|
<div class="user-list card-list">
|
||||||
{{ range .Utenti }}
|
<template x-for="entry in searchResults" :key="entry.item.uid">
|
||||||
<div class="user-item card">
|
<div class="user-item card">
|
||||||
<a class="full-name" href="{{ $.UserPagesBaseUrl }}{{ .Uid }}">
|
<a class="full-name" :href="'{{ $.Config.UserPagesBaseUrl }}' + entry.item.uid" x-text="
|
||||||
{{ .Nome }} {{ .Cognome }}
|
sortMode === 'surname' ?
|
||||||
</a>
|
entry.item.cognome + ' ' + entry.item.nome :
|
||||||
{{ if .IsMacchinista }}
|
entry.item.nome + ' ' + entry.item.cognome
|
||||||
|
"></a>
|
||||||
|
<template x-if="entry.item.macchinista">
|
||||||
<div class="icon" title="Macchinista">
|
<div class="icon" title="Macchinista">
|
||||||
<i class="fas fa-wrench"></i>
|
<i class="fas fa-wrench"></i>
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
</template>
|
||||||
|
<!-- <template x-if="entry.score">
|
||||||
|
<span x-text="entry.score"></span>
|
||||||
|
</template> -->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="spinner" x-ref="spinner" x-show="searchResults.length < searchResultsBuffer.length">
|
||||||
|
<i class="fas fa-hourglass"></i>
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!--
|
<script type="module" src="/public/js/utenti.min.js"></script>
|
||||||
<script src="//unpkg.com/alpinejs" defer></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/fuse.js/dist/fuse.js" defer></script>
|
|
||||||
|
|
||||||
<script type="module" src="/public/js/utenti.js"></script>
|
|
||||||
-->
|
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
Reference in New Issue
Block a user