Compare commits
4 Commits
3e4380d9f8
...
ff0ab4c2c9
| Author | SHA1 | Date | |
|---|---|---|---|
|
ff0ab4c2c9
|
|||
|
77975c8f9d
|
|||
|
6daaaaa6fd
|
|||
|
bf817a14c7
|
@@ -151,9 +151,9 @@ func (app *app) setup() error {
|
||||
return fmt.Errorf("app: failed to start token repository: %w", err)
|
||||
}
|
||||
|
||||
projectRepository, err := repository.NewProject(app.ctx, app.db, app.logger.WithGroup("repository.project"), app.assert)
|
||||
publicationRepository, err := repository.NewPublication(app.ctx, app.db, app.logger.WithGroup("repository.publication"), app.assert)
|
||||
if err != nil {
|
||||
return fmt.Errorf("app: failed to start project repository: %w", err)
|
||||
return fmt.Errorf("app: failed to start publication repository: %w", err)
|
||||
}
|
||||
|
||||
permissionRepository, err := repository.NewPermissions(app.ctx, app.db, app.logger.WithGroup("repository.permission"), app.assert)
|
||||
@@ -169,12 +169,12 @@ func (app *app) setup() error {
|
||||
Logger: app.logger.WithGroup("service.token"),
|
||||
Assertions: app.assert,
|
||||
})
|
||||
projectService := service.NewProject(projectRepository, permissionRepository, app.logger.WithGroup("service.project"), app.assert)
|
||||
publicationService := service.NewPublication(publicationRepository, permissionRepository, app.logger.WithGroup("service.publication"), app.assert)
|
||||
|
||||
app.handler, err = router.New(router.Config{
|
||||
UserService: userService,
|
||||
TokenService: tokenService,
|
||||
ProjectService: projectService,
|
||||
PublicationService: publicationService,
|
||||
|
||||
Templates: app.templates,
|
||||
DisableCache: app.developmentMode,
|
||||
|
||||
3
editor/go.mod
Normal file
3
editor/go.mod
Normal file
@@ -0,0 +1,3 @@
|
||||
module code.capytal.cc/capytal/comicverse/editor
|
||||
|
||||
go 1.25.2
|
||||
0
editor/go.sum
Normal file
0
editor/go.sum
Normal file
8
internals/randname/COPYRIGHT
Normal file
8
internals/randname/COPYRIGHT
Normal file
@@ -0,0 +1,8 @@
|
||||
Adjectives and names list files were copied from Dustin Kirkland's <dustin.kirkland@gmail.com>
|
||||
petname project at Github, specifically from these files:
|
||||
- https://github.com/dustinkirkland/petname/blob/5a79a2954e94ddbe6dc47b35c8b08ed931fc0e7f/usr/share/petname/small/adjectives.txt
|
||||
- https://github.com/dustinkirkland/petname/blob/5a79a2954e94ddbe6dc47b35c8b08ed931fc0e7f/usr/share/petname/small/names.txt
|
||||
|
||||
The original files are provided and released under the Apache License version 2,
|
||||
which a copy is available at
|
||||
https://github.com/dustinkirkland/petname/blob/5a79a2954e94ddbe6dc47b35c8b08ed931fc0e7f/LICENSE
|
||||
452
internals/randname/adjectives.txt
Normal file
452
internals/randname/adjectives.txt
Normal file
@@ -0,0 +1,452 @@
|
||||
ox
|
||||
ant
|
||||
ape
|
||||
asp
|
||||
bat
|
||||
bee
|
||||
boa
|
||||
bug
|
||||
cat
|
||||
cod
|
||||
cow
|
||||
cub
|
||||
doe
|
||||
dog
|
||||
eel
|
||||
eft
|
||||
elf
|
||||
elk
|
||||
emu
|
||||
ewe
|
||||
fly
|
||||
fox
|
||||
gar
|
||||
gnu
|
||||
hen
|
||||
hog
|
||||
imp
|
||||
jay
|
||||
kid
|
||||
kit
|
||||
koi
|
||||
lab
|
||||
man
|
||||
owl
|
||||
pig
|
||||
pug
|
||||
pup
|
||||
ram
|
||||
rat
|
||||
ray
|
||||
yak
|
||||
bass
|
||||
bear
|
||||
bird
|
||||
boar
|
||||
buck
|
||||
bull
|
||||
calf
|
||||
chow
|
||||
clam
|
||||
colt
|
||||
crab
|
||||
crow
|
||||
dane
|
||||
deer
|
||||
dodo
|
||||
dory
|
||||
dove
|
||||
drum
|
||||
duck
|
||||
fawn
|
||||
fish
|
||||
flea
|
||||
foal
|
||||
fowl
|
||||
frog
|
||||
gnat
|
||||
goat
|
||||
grub
|
||||
gull
|
||||
hare
|
||||
hawk
|
||||
ibex
|
||||
joey
|
||||
kite
|
||||
kiwi
|
||||
lamb
|
||||
lark
|
||||
lion
|
||||
loon
|
||||
lynx
|
||||
mako
|
||||
mink
|
||||
mite
|
||||
mole
|
||||
moth
|
||||
mule
|
||||
mutt
|
||||
newt
|
||||
orca
|
||||
oryx
|
||||
pika
|
||||
pony
|
||||
puma
|
||||
seal
|
||||
shad
|
||||
slug
|
||||
sole
|
||||
stag
|
||||
stud
|
||||
swan
|
||||
tahr
|
||||
teal
|
||||
tick
|
||||
toad
|
||||
tuna
|
||||
wasp
|
||||
wolf
|
||||
worm
|
||||
wren
|
||||
yeti
|
||||
adder
|
||||
akita
|
||||
alien
|
||||
aphid
|
||||
bison
|
||||
boxer
|
||||
bream
|
||||
bunny
|
||||
burro
|
||||
camel
|
||||
chimp
|
||||
civet
|
||||
cobra
|
||||
coral
|
||||
corgi
|
||||
crane
|
||||
dingo
|
||||
drake
|
||||
eagle
|
||||
egret
|
||||
filly
|
||||
finch
|
||||
gator
|
||||
gecko
|
||||
ghost
|
||||
ghoul
|
||||
goose
|
||||
guppy
|
||||
heron
|
||||
hippo
|
||||
horse
|
||||
hound
|
||||
husky
|
||||
hyena
|
||||
koala
|
||||
krill
|
||||
leech
|
||||
lemur
|
||||
liger
|
||||
llama
|
||||
louse
|
||||
macaw
|
||||
midge
|
||||
molly
|
||||
moose
|
||||
moray
|
||||
mouse
|
||||
panda
|
||||
perch
|
||||
prawn
|
||||
quail
|
||||
racer
|
||||
raven
|
||||
rhino
|
||||
robin
|
||||
satyr
|
||||
shark
|
||||
sheep
|
||||
shrew
|
||||
skink
|
||||
skunk
|
||||
sloth
|
||||
snail
|
||||
snake
|
||||
snipe
|
||||
squid
|
||||
stork
|
||||
swift
|
||||
tapir
|
||||
tetra
|
||||
tiger
|
||||
troll
|
||||
trout
|
||||
viper
|
||||
wahoo
|
||||
whale
|
||||
zebra
|
||||
alpaca
|
||||
amoeba
|
||||
baboon
|
||||
badger
|
||||
beagle
|
||||
bedbug
|
||||
beetle
|
||||
bengal
|
||||
bobcat
|
||||
caiman
|
||||
cattle
|
||||
cicada
|
||||
collie
|
||||
condor
|
||||
cougar
|
||||
coyote
|
||||
dassie
|
||||
dragon
|
||||
earwig
|
||||
falcon
|
||||
feline
|
||||
ferret
|
||||
gannet
|
||||
gibbon
|
||||
glider
|
||||
goblin
|
||||
gopher
|
||||
grouse
|
||||
guinea
|
||||
hermit
|
||||
hornet
|
||||
iguana
|
||||
impala
|
||||
insect
|
||||
jackal
|
||||
jaguar
|
||||
jennet
|
||||
kitten
|
||||
kodiak
|
||||
lizard
|
||||
locust
|
||||
maggot
|
||||
magpie
|
||||
mammal
|
||||
mantis
|
||||
marlin
|
||||
marmot
|
||||
marten
|
||||
martin
|
||||
mayfly
|
||||
minnow
|
||||
monkey
|
||||
mullet
|
||||
muskox
|
||||
ocelot
|
||||
oriole
|
||||
osprey
|
||||
oyster
|
||||
parrot
|
||||
pigeon
|
||||
piglet
|
||||
poodle
|
||||
possum
|
||||
python
|
||||
quagga
|
||||
rabbit
|
||||
raptor
|
||||
rodent
|
||||
roughy
|
||||
salmon
|
||||
sawfly
|
||||
serval
|
||||
shiner
|
||||
shrimp
|
||||
spider
|
||||
sponge
|
||||
tarpon
|
||||
thrush
|
||||
tomcat
|
||||
toucan
|
||||
turkey
|
||||
turtle
|
||||
urchin
|
||||
vervet
|
||||
walrus
|
||||
weasel
|
||||
weevil
|
||||
wombat
|
||||
anchovy
|
||||
anemone
|
||||
bluejay
|
||||
buffalo
|
||||
bulldog
|
||||
buzzard
|
||||
caribou
|
||||
catfish
|
||||
chamois
|
||||
cheetah
|
||||
chicken
|
||||
chigger
|
||||
cowbird
|
||||
crappie
|
||||
crawdad
|
||||
cricket
|
||||
dogfish
|
||||
dolphin
|
||||
firefly
|
||||
garfish
|
||||
gazelle
|
||||
gelding
|
||||
giraffe
|
||||
gobbler
|
||||
gorilla
|
||||
goshawk
|
||||
grackle
|
||||
griffon
|
||||
grizzly
|
||||
grouper
|
||||
haddock
|
||||
hagfish
|
||||
halibut
|
||||
hamster
|
||||
herring
|
||||
javelin
|
||||
jawfish
|
||||
jaybird
|
||||
katydid
|
||||
ladybug
|
||||
lamprey
|
||||
lemming
|
||||
leopard
|
||||
lioness
|
||||
lobster
|
||||
macaque
|
||||
mallard
|
||||
mammoth
|
||||
manatee
|
||||
mastiff
|
||||
meerkat
|
||||
mollusk
|
||||
monarch
|
||||
mongrel
|
||||
monitor
|
||||
monster
|
||||
mudfish
|
||||
muskrat
|
||||
mustang
|
||||
narwhal
|
||||
oarfish
|
||||
octopus
|
||||
opossum
|
||||
ostrich
|
||||
panther
|
||||
peacock
|
||||
pegasus
|
||||
pelican
|
||||
penguin
|
||||
phoenix
|
||||
piranha
|
||||
polecat
|
||||
primate
|
||||
quetzal
|
||||
raccoon
|
||||
rattler
|
||||
redbird
|
||||
redfish
|
||||
reptile
|
||||
rooster
|
||||
sawfish
|
||||
sculpin
|
||||
seagull
|
||||
skylark
|
||||
snapper
|
||||
spaniel
|
||||
sparrow
|
||||
sunbeam
|
||||
sunbird
|
||||
sunfish
|
||||
tadpole
|
||||
terrier
|
||||
unicorn
|
||||
vulture
|
||||
wallaby
|
||||
walleye
|
||||
warthog
|
||||
whippet
|
||||
wildcat
|
||||
aardvark
|
||||
airedale
|
||||
albacore
|
||||
anteater
|
||||
antelope
|
||||
arachnid
|
||||
barnacle
|
||||
basilisk
|
||||
blowfish
|
||||
bluebird
|
||||
bluegill
|
||||
bonefish
|
||||
bullfrog
|
||||
cardinal
|
||||
chipmunk
|
||||
cockatoo
|
||||
crayfish
|
||||
dinosaur
|
||||
doberman
|
||||
duckling
|
||||
elephant
|
||||
escargot
|
||||
flamingo
|
||||
flounder
|
||||
foxhound
|
||||
glowworm
|
||||
goldfish
|
||||
grubworm
|
||||
hedgehog
|
||||
honeybee
|
||||
hookworm
|
||||
humpback
|
||||
kangaroo
|
||||
killdeer
|
||||
kingfish
|
||||
labrador
|
||||
lacewing
|
||||
ladybird
|
||||
lionfish
|
||||
longhorn
|
||||
mackerel
|
||||
malamute
|
||||
marmoset
|
||||
mastodon
|
||||
moccasin
|
||||
mongoose
|
||||
monkfish
|
||||
mosquito
|
||||
pangolin
|
||||
parakeet
|
||||
pheasant
|
||||
pipefish
|
||||
platypus
|
||||
polliwog
|
||||
porpoise
|
||||
reindeer
|
||||
ringtail
|
||||
sailfish
|
||||
scorpion
|
||||
seahorse
|
||||
seasnail
|
||||
sheepdog
|
||||
shepherd
|
||||
silkworm
|
||||
squirrel
|
||||
stallion
|
||||
starfish
|
||||
starling
|
||||
stingray
|
||||
stinkbug
|
||||
sturgeon
|
||||
terrapin
|
||||
titmouse
|
||||
tortoise
|
||||
treefrog
|
||||
werewolf
|
||||
woodcock
|
||||
449
internals/randname/names.txt
Normal file
449
internals/randname/names.txt
Normal file
@@ -0,0 +1,449 @@
|
||||
able
|
||||
above
|
||||
absolute
|
||||
accepted
|
||||
accurate
|
||||
ace
|
||||
active
|
||||
actual
|
||||
adapted
|
||||
adapting
|
||||
adequate
|
||||
adjusted
|
||||
advanced
|
||||
alert
|
||||
alive
|
||||
allowed
|
||||
allowing
|
||||
amazed
|
||||
amazing
|
||||
ample
|
||||
amused
|
||||
amusing
|
||||
apparent
|
||||
apt
|
||||
arriving
|
||||
artistic
|
||||
assured
|
||||
assuring
|
||||
awaited
|
||||
awake
|
||||
aware
|
||||
balanced
|
||||
becoming
|
||||
beloved
|
||||
better
|
||||
big
|
||||
blessed
|
||||
bold
|
||||
boss
|
||||
brave
|
||||
brief
|
||||
bright
|
||||
bursting
|
||||
busy
|
||||
calm
|
||||
capable
|
||||
capital
|
||||
careful
|
||||
caring
|
||||
casual
|
||||
causal
|
||||
central
|
||||
certain
|
||||
champion
|
||||
charmed
|
||||
charming
|
||||
cheerful
|
||||
chief
|
||||
choice
|
||||
civil
|
||||
classic
|
||||
clean
|
||||
clear
|
||||
clever
|
||||
climbing
|
||||
close
|
||||
closing
|
||||
coherent
|
||||
comic
|
||||
communal
|
||||
complete
|
||||
composed
|
||||
concise
|
||||
concrete
|
||||
content
|
||||
cool
|
||||
correct
|
||||
cosmic
|
||||
crack
|
||||
creative
|
||||
credible
|
||||
crisp
|
||||
crucial
|
||||
cuddly
|
||||
cunning
|
||||
curious
|
||||
current
|
||||
cute
|
||||
daring
|
||||
darling
|
||||
dashing
|
||||
dear
|
||||
decent
|
||||
deciding
|
||||
deep
|
||||
definite
|
||||
delicate
|
||||
desired
|
||||
destined
|
||||
devoted
|
||||
direct
|
||||
discrete
|
||||
distinct
|
||||
diverse
|
||||
divine
|
||||
dominant
|
||||
driven
|
||||
driving
|
||||
dynamic
|
||||
eager
|
||||
easy
|
||||
electric
|
||||
elegant
|
||||
emerging
|
||||
eminent
|
||||
enabled
|
||||
enabling
|
||||
endless
|
||||
engaged
|
||||
engaging
|
||||
enhanced
|
||||
enjoyed
|
||||
enormous
|
||||
enough
|
||||
epic
|
||||
equal
|
||||
equipped
|
||||
eternal
|
||||
ethical
|
||||
evident
|
||||
evolved
|
||||
evolving
|
||||
exact
|
||||
excited
|
||||
exciting
|
||||
exotic
|
||||
expert
|
||||
factual
|
||||
fair
|
||||
faithful
|
||||
famous
|
||||
fancy
|
||||
fast
|
||||
feasible
|
||||
fine
|
||||
finer
|
||||
firm
|
||||
first
|
||||
fit
|
||||
fitting
|
||||
fleet
|
||||
flexible
|
||||
flowing
|
||||
fluent
|
||||
flying
|
||||
fond
|
||||
frank
|
||||
free
|
||||
fresh
|
||||
full
|
||||
fun
|
||||
funky
|
||||
funny
|
||||
game
|
||||
generous
|
||||
gentle
|
||||
genuine
|
||||
giving
|
||||
glad
|
||||
glorious
|
||||
glowing
|
||||
golden
|
||||
good
|
||||
gorgeous
|
||||
grand
|
||||
grateful
|
||||
great
|
||||
growing
|
||||
grown
|
||||
guided
|
||||
guiding
|
||||
handy
|
||||
happy
|
||||
hardy
|
||||
harmless
|
||||
healthy
|
||||
helped
|
||||
helpful
|
||||
helping
|
||||
heroic
|
||||
hip
|
||||
holy
|
||||
honest
|
||||
hopeful
|
||||
hot
|
||||
huge
|
||||
humane
|
||||
humble
|
||||
humorous
|
||||
ideal
|
||||
immense
|
||||
immortal
|
||||
immune
|
||||
improved
|
||||
in
|
||||
included
|
||||
infinite
|
||||
informed
|
||||
innocent
|
||||
inspired
|
||||
integral
|
||||
intense
|
||||
intent
|
||||
internal
|
||||
intimate
|
||||
inviting
|
||||
joint
|
||||
just
|
||||
keen
|
||||
key
|
||||
kind
|
||||
knowing
|
||||
known
|
||||
large
|
||||
lasting
|
||||
leading
|
||||
learning
|
||||
legal
|
||||
legible
|
||||
lenient
|
||||
liberal
|
||||
light
|
||||
liked
|
||||
literate
|
||||
live
|
||||
living
|
||||
logical
|
||||
loved
|
||||
loving
|
||||
loyal
|
||||
lucky
|
||||
magical
|
||||
magnetic
|
||||
main
|
||||
major
|
||||
many
|
||||
massive
|
||||
master
|
||||
mature
|
||||
maximum
|
||||
measured
|
||||
meet
|
||||
merry
|
||||
mighty
|
||||
mint
|
||||
model
|
||||
modern
|
||||
modest
|
||||
moral
|
||||
more
|
||||
moved
|
||||
moving
|
||||
musical
|
||||
mutual
|
||||
national
|
||||
native
|
||||
natural
|
||||
nearby
|
||||
neat
|
||||
needed
|
||||
neutral
|
||||
new
|
||||
next
|
||||
nice
|
||||
noble
|
||||
normal
|
||||
notable
|
||||
noted
|
||||
novel
|
||||
obliging
|
||||
on
|
||||
one
|
||||
open
|
||||
optimal
|
||||
optimum
|
||||
organic
|
||||
oriented
|
||||
outgoing
|
||||
patient
|
||||
peaceful
|
||||
perfect
|
||||
pet
|
||||
picked
|
||||
pleasant
|
||||
pleased
|
||||
pleasing
|
||||
poetic
|
||||
polished
|
||||
polite
|
||||
popular
|
||||
positive
|
||||
possible
|
||||
powerful
|
||||
precious
|
||||
precise
|
||||
premium
|
||||
prepared
|
||||
present
|
||||
pretty
|
||||
primary
|
||||
prime
|
||||
pro
|
||||
probable
|
||||
profound
|
||||
promoted
|
||||
prompt
|
||||
proper
|
||||
proud
|
||||
proven
|
||||
pumped
|
||||
pure
|
||||
quality
|
||||
quick
|
||||
quiet
|
||||
rapid
|
||||
rare
|
||||
rational
|
||||
ready
|
||||
real
|
||||
refined
|
||||
regular
|
||||
related
|
||||
relative
|
||||
relaxed
|
||||
relaxing
|
||||
relevant
|
||||
relieved
|
||||
renewed
|
||||
renewing
|
||||
resolved
|
||||
rested
|
||||
rich
|
||||
right
|
||||
robust
|
||||
romantic
|
||||
ruling
|
||||
sacred
|
||||
safe
|
||||
saved
|
||||
saving
|
||||
secure
|
||||
select
|
||||
selected
|
||||
sensible
|
||||
set
|
||||
settled
|
||||
settling
|
||||
sharing
|
||||
sharp
|
||||
shining
|
||||
simple
|
||||
sincere
|
||||
singular
|
||||
skilled
|
||||
smart
|
||||
smashing
|
||||
smiling
|
||||
smooth
|
||||
social
|
||||
solid
|
||||
sought
|
||||
sound
|
||||
special
|
||||
splendid
|
||||
square
|
||||
stable
|
||||
star
|
||||
steady
|
||||
sterling
|
||||
still
|
||||
stirred
|
||||
stirring
|
||||
striking
|
||||
strong
|
||||
stunning
|
||||
subtle
|
||||
suitable
|
||||
suited
|
||||
summary
|
||||
sunny
|
||||
super
|
||||
superb
|
||||
supreme
|
||||
sure
|
||||
sweeping
|
||||
sweet
|
||||
talented
|
||||
teaching
|
||||
tender
|
||||
thankful
|
||||
thorough
|
||||
tidy
|
||||
tight
|
||||
together
|
||||
tolerant
|
||||
top
|
||||
topical
|
||||
tops
|
||||
touched
|
||||
touching
|
||||
tough
|
||||
true
|
||||
trusted
|
||||
trusting
|
||||
trusty
|
||||
ultimate
|
||||
unbiased
|
||||
uncommon
|
||||
unified
|
||||
unique
|
||||
united
|
||||
up
|
||||
upright
|
||||
upward
|
||||
usable
|
||||
useful
|
||||
valid
|
||||
valued
|
||||
vast
|
||||
verified
|
||||
viable
|
||||
vital
|
||||
vocal
|
||||
wanted
|
||||
warm
|
||||
wealthy
|
||||
welcome
|
||||
welcomed
|
||||
well
|
||||
whole
|
||||
willing
|
||||
winning
|
||||
wired
|
||||
wise
|
||||
witty
|
||||
wondrous
|
||||
workable
|
||||
working
|
||||
worthy
|
||||
33
internals/randname/randname.go
Normal file
33
internals/randname/randname.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package randname
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// TODO: Make generator be based on fantasy, sci-fi and other literature
|
||||
// and artistic names.
|
||||
|
||||
//go:embed adjectives.txt
|
||||
var adjectives string
|
||||
|
||||
//go:embed names.txt
|
||||
var names string
|
||||
|
||||
var (
|
||||
adjectivesList = strings.Split(adjectives, "\n")
|
||||
namesList = strings.Split(names, "\n")
|
||||
)
|
||||
|
||||
func New(sep ...string) string {
|
||||
if len(sep) == 0 {
|
||||
sep = append(sep, " ")
|
||||
}
|
||||
|
||||
a := adjectivesList[rand.Intn(len(adjectivesList))]
|
||||
n := namesList[rand.Intn(len(namesList))]
|
||||
|
||||
return fmt.Sprintf("%s%s%s", a, sep[0], n)
|
||||
}
|
||||
@@ -120,7 +120,7 @@ const (
|
||||
PermissionAuthor Permissions = 0x1111111111111111 // "author"
|
||||
PermissionAdminDelete Permissions = 0x1000000000000000 // "admin.delete" -----
|
||||
PermissionAdminAll Permissions = 0x0111110000000001 // "admin.all"
|
||||
PermissionAdminProject Permissions = 0x0100000000000000 // "admin.project"
|
||||
PermissionAdminPublication Permissions = 0x0100000000000000 // "admin.publication"
|
||||
PermissionAdminMembers Permissions = 0x0010000000000000 // "admin.members"
|
||||
PermissionEditAll Permissions = 0x0000001111111111 // "edit.all" ---------
|
||||
PermissionEditPages Permissions = 0x0000000100000000 // "edit.pages"
|
||||
@@ -134,7 +134,7 @@ const (
|
||||
var PermissionLabels = map[Permissions]string{
|
||||
PermissionAuthor: "author",
|
||||
PermissionAdminDelete: "admin.delete",
|
||||
PermissionAdminProject: "admin.project",
|
||||
PermissionAdminPublication: "admin.publication",
|
||||
PermissionAdminMembers: "admin.members",
|
||||
PermissionEditPages: "edit.pages",
|
||||
PermissionEditInteractions: "edit.interactions",
|
||||
|
||||
@@ -6,16 +6,16 @@ import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Project struct {
|
||||
type Publication struct {
|
||||
ID uuid.UUID // Must be unique, represented as base64 string in URLs
|
||||
Title string // Must not be empty
|
||||
DateCreated time.Time
|
||||
DateUpdated time.Time
|
||||
}
|
||||
|
||||
var _ Model = (*Project)(nil)
|
||||
var _ Model = (*Publication)(nil)
|
||||
|
||||
func (p Project) Validate() error {
|
||||
func (p Publication) Validate() error {
|
||||
errs := []error{}
|
||||
if len(p.ID) == 0 {
|
||||
errs = append(errs, ErrZeroValue{Name: "UUID"})
|
||||
@@ -31,7 +31,7 @@ func (p Project) Validate() error {
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return ErrInvalidModel{Name: "Project", Errors: errs}
|
||||
return ErrInvalidModel{Name: "Publication", Errors: errs}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -17,7 +17,7 @@ type Permissions struct {
|
||||
baseRepostiory
|
||||
}
|
||||
|
||||
// Must be initiated after [User] and [Project]
|
||||
// Must be initiated after [User] and [Publication]
|
||||
func NewPermissions(
|
||||
ctx context.Context,
|
||||
db *sql.DB,
|
||||
@@ -32,17 +32,17 @@ func NewPermissions(
|
||||
}
|
||||
|
||||
q := fmt.Sprintf(`
|
||||
CREATE TABLE IF NOT EXISTS project_permissions (
|
||||
project_id TEXT NOT NULL,
|
||||
CREATE TABLE IF NOT EXISTS publication_permissions (
|
||||
publication_id TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
permissions_value INTEGER NOT NULL DEFAULT '0',
|
||||
_permissions_text TEXT NOT NULL DEFAULT '', -- For display purposes only, may not always be up-to-date
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
|
||||
PRIMARY KEY(project_id, user_id)
|
||||
FOREIGN KEY(project_id)
|
||||
REFERENCES projects (id)
|
||||
PRIMARY KEY(publication_id, user_id)
|
||||
FOREIGN KEY(publication_id)
|
||||
REFERENCES publications (id)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE RESTRICT,
|
||||
FOREIGN KEY(user_id)
|
||||
@@ -58,13 +58,13 @@ func NewPermissions(
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, errors.Join(errors.New("unable to create project tables"), err)
|
||||
return nil, errors.Join(errors.New("unable to create publication tables"), err)
|
||||
}
|
||||
|
||||
return &Permissions{baseRepostiory: b}, nil
|
||||
}
|
||||
|
||||
func (repo Permissions) Create(project, user uuid.UUID, permissions model.Permissions) error {
|
||||
func (repo Permissions) Create(publication, user uuid.UUID, permissions model.Permissions) error {
|
||||
repo.assert.NotNil(repo.db)
|
||||
repo.assert.NotNil(repo.ctx)
|
||||
repo.assert.NotNil(repo.ctx)
|
||||
@@ -75,21 +75,21 @@ func (repo Permissions) Create(project, user uuid.UUID, permissions model.Permis
|
||||
}
|
||||
|
||||
q := `
|
||||
INSERT INTO project_permissions (project_id, user_id, permissions_value, _permissions_text, created_at, updated_at)
|
||||
VALUES (:project_id, :user_id, :permissions_value, :permissions_text, :created_at, :updated_at)
|
||||
INSERT INTO publication_permissions (publication_id, user_id, permissions_value, _permissions_text, created_at, updated_at)
|
||||
VALUES (:publication_id, :user_id, :permissions_value, :permissions_text, :created_at, :updated_at)
|
||||
`
|
||||
|
||||
now := time.Now()
|
||||
|
||||
log := repo.log.With(slog.String("project_id", project.String()),
|
||||
log := repo.log.With(slog.String("publication_id", publication.String()),
|
||||
slog.String("user_id", user.String()),
|
||||
slog.String("permissions", fmt.Sprintf("%d", permissions)),
|
||||
slog.String("permissions_text", permissions.String()),
|
||||
slog.String("query", q))
|
||||
log.DebugContext(repo.ctx, "Inserting new project permissions")
|
||||
log.DebugContext(repo.ctx, "Inserting new publication permissions")
|
||||
|
||||
_, err = tx.ExecContext(repo.ctx, q,
|
||||
sql.Named("project_id", project),
|
||||
sql.Named("publication_id", publication),
|
||||
sql.Named("user_id", user),
|
||||
sql.Named("permissions_value", permissions),
|
||||
sql.Named("permissions_text", permissions.String()),
|
||||
@@ -97,7 +97,7 @@ func (repo Permissions) Create(project, user uuid.UUID, permissions model.Permis
|
||||
sql.Named("updated_at", now.Format(dateFormat)),
|
||||
)
|
||||
if err != nil {
|
||||
log.ErrorContext(repo.ctx, "Failed to insert project permissions", slog.String("error", err.Error()))
|
||||
log.ErrorContext(repo.ctx, "Failed to insert publication permissions", slog.String("error", err.Error()))
|
||||
return errors.Join(ErrExecuteQuery, err)
|
||||
}
|
||||
|
||||
@@ -109,24 +109,24 @@ func (repo Permissions) Create(project, user uuid.UUID, permissions model.Permis
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repo Permissions) GetByID(project uuid.UUID, user uuid.UUID) (model.Permissions, error) {
|
||||
func (repo Permissions) GetByID(publication uuid.UUID, user uuid.UUID) (model.Permissions, error) {
|
||||
repo.assert.NotNil(repo.db)
|
||||
repo.assert.NotNil(repo.ctx)
|
||||
repo.assert.NotNil(repo.log)
|
||||
|
||||
q := `
|
||||
SELECT permissions_value FROM project_permissions
|
||||
WHERE project_id = :project_id
|
||||
SELECT permissions_value FROM publication_permissions
|
||||
WHERE publication_id = :publication_id
|
||||
AND user_id = :user_id
|
||||
`
|
||||
|
||||
log := repo.log.With(slog.String("projcet_id", project.String()),
|
||||
log := repo.log.With(slog.String("projcet_id", publication.String()),
|
||||
slog.String("user_id", user.String()),
|
||||
slog.String("query", q))
|
||||
log.DebugContext(repo.ctx, "Getting by ID")
|
||||
|
||||
row := repo.db.QueryRowContext(repo.ctx, q,
|
||||
sql.Named("project_id", user),
|
||||
sql.Named("publication_id", user),
|
||||
sql.Named("user_id", user))
|
||||
|
||||
var p model.Permissions
|
||||
@@ -138,7 +138,7 @@ func (repo Permissions) GetByID(project uuid.UUID, user uuid.UUID) (model.Permis
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// GetByUserID returns a project_id-to-permissions map containing all projects and permissions that said userID
|
||||
// GetByUserID returns a publication_id-to-permissions map containing all publications and permissions that said userID
|
||||
// has relation to.
|
||||
func (repo Permissions) GetByUserID(user uuid.UUID) (permissions map[uuid.UUID]model.Permissions, err error) {
|
||||
repo.assert.NotNil(repo.db)
|
||||
@@ -152,7 +152,7 @@ func (repo Permissions) GetByUserID(user uuid.UUID) (permissions map[uuid.UUID]m
|
||||
}
|
||||
|
||||
q := `
|
||||
SELECT project_id, permissions_value FROM project_permissions
|
||||
SELECT publication_id, permissions_value FROM publication_permissions
|
||||
WHERE user_id = :user_id
|
||||
`
|
||||
|
||||
@@ -176,16 +176,16 @@ func (repo Permissions) GetByUserID(user uuid.UUID) (permissions map[uuid.UUID]m
|
||||
ps := map[uuid.UUID]model.Permissions{}
|
||||
|
||||
for rows.Next() {
|
||||
var project uuid.UUID
|
||||
var publication uuid.UUID
|
||||
var permissions model.Permissions
|
||||
|
||||
err := rows.Scan(&project, &permissions)
|
||||
err := rows.Scan(&publication, &permissions)
|
||||
if err != nil {
|
||||
log.ErrorContext(repo.ctx, "Failed to scan permissions of user id", slog.String("error", err.Error()))
|
||||
return nil, errors.Join(ErrInvalidOutput, err)
|
||||
}
|
||||
|
||||
ps[project] = permissions
|
||||
ps[publication] = permissions
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
@@ -196,7 +196,7 @@ func (repo Permissions) GetByUserID(user uuid.UUID) (permissions map[uuid.UUID]m
|
||||
return ps, nil
|
||||
}
|
||||
|
||||
func (repo Permissions) Update(project, user uuid.UUID, permissions model.Permissions) error {
|
||||
func (repo Permissions) Update(publication, user uuid.UUID, permissions model.Permissions) error {
|
||||
repo.assert.NotNil(repo.db)
|
||||
repo.assert.NotNil(repo.ctx)
|
||||
repo.assert.NotNil(repo.log)
|
||||
@@ -207,20 +207,20 @@ func (repo Permissions) Update(project, user uuid.UUID, permissions model.Permis
|
||||
}
|
||||
|
||||
q := `
|
||||
UPDATE project_permissions
|
||||
UPDATE publication_permissions
|
||||
SET permissions_value = :permissions_value
|
||||
_permissions_text = :permissions_text
|
||||
updated_at = :updated_at
|
||||
WHERE project_uuid = :project_uuid
|
||||
WHERE publication_uuid = :publication_uuid
|
||||
AND user_uuid = :user_uuid
|
||||
`
|
||||
|
||||
log := repo.log.With(slog.String("project_id", project.String()),
|
||||
log := repo.log.With(slog.String("publication_id", publication.String()),
|
||||
slog.String("user_id", user.String()),
|
||||
slog.String("permissions", fmt.Sprintf("%d", permissions)),
|
||||
slog.String("permissions_text", permissions.String()),
|
||||
slog.String("query", q))
|
||||
log.DebugContext(repo.ctx, "Updating project permissions")
|
||||
log.DebugContext(repo.ctx, "Updating publication permissions")
|
||||
|
||||
now := time.Now()
|
||||
|
||||
@@ -228,11 +228,11 @@ func (repo Permissions) Update(project, user uuid.UUID, permissions model.Permis
|
||||
sql.Named("permissions_value", permissions),
|
||||
sql.Named("permissions_text", permissions.String()),
|
||||
sql.Named("updated_at", now.Format(dateFormat)),
|
||||
sql.Named("project_id", project),
|
||||
sql.Named("publication_id", publication),
|
||||
sql.Named("user_id", user),
|
||||
)
|
||||
if err != nil {
|
||||
log.ErrorContext(repo.ctx, "Failed to update project permissions", slog.String("error", err.Error()))
|
||||
log.ErrorContext(repo.ctx, "Failed to update publication permissions", slog.String("error", err.Error()))
|
||||
return errors.Join(ErrExecuteQuery, err)
|
||||
}
|
||||
|
||||
@@ -244,7 +244,7 @@ func (repo Permissions) Update(project, user uuid.UUID, permissions model.Permis
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repo Permissions) Delete(project, user uuid.UUID) error {
|
||||
func (repo Permissions) Delete(publication, user uuid.UUID) error {
|
||||
repo.assert.NotNil(repo.db)
|
||||
repo.assert.NotNil(repo.ctx)
|
||||
repo.assert.NotNil(repo.ctx)
|
||||
@@ -255,22 +255,22 @@ func (repo Permissions) Delete(project, user uuid.UUID) error {
|
||||
}
|
||||
|
||||
q := `
|
||||
DELETE FROM project_permissions
|
||||
WHERE project_id = :project_id
|
||||
DELETE FROM publication_permissions
|
||||
WHERE publication_id = :publication_id
|
||||
AND user_id = :user_id
|
||||
`
|
||||
|
||||
log := repo.log.With(slog.String("project_id", project.String()),
|
||||
log := repo.log.With(slog.String("publication_id", publication.String()),
|
||||
slog.String("user_id", user.String()),
|
||||
slog.String("query", q))
|
||||
log.DebugContext(repo.ctx, "Deleting project permissions")
|
||||
log.DebugContext(repo.ctx, "Deleting publication permissions")
|
||||
|
||||
_, err = tx.ExecContext(repo.ctx, q,
|
||||
sql.Named("project_id", project),
|
||||
sql.Named("publication_id", publication),
|
||||
sql.Named("user_id", user),
|
||||
)
|
||||
if err != nil {
|
||||
log.ErrorContext(repo.ctx, "Failed to delete project permissions", slog.String("error", err.Error()))
|
||||
log.ErrorContext(repo.ctx, "Failed to delete publication permissions", slog.String("error", err.Error()))
|
||||
return errors.Join(ErrExecuteQuery, err)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,11 +14,11 @@ import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Project struct {
|
||||
type Publication struct {
|
||||
baseRepostiory
|
||||
}
|
||||
|
||||
func NewProject(ctx context.Context, db *sql.DB, log *slog.Logger, assert tinyssert.Assertions) (*Project, error) {
|
||||
func NewPublication(ctx context.Context, db *sql.DB, log *slog.Logger, assert tinyssert.Assertions) (*Publication, error) {
|
||||
b := newBaseRepostiory(ctx, db, log, assert)
|
||||
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
@@ -27,7 +27,7 @@ func NewProject(ctx context.Context, db *sql.DB, log *slog.Logger, assert tinyss
|
||||
}
|
||||
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
CREATE TABLE IF NOT EXISTS projects (
|
||||
CREATE TABLE IF NOT EXISTS publications (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
@@ -38,13 +38,13 @@ func NewProject(ctx context.Context, db *sql.DB, log *slog.Logger, assert tinyss
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, errors.Join(errors.New("unable to create project tables"), err)
|
||||
return nil, errors.Join(errors.New("unable to create publication tables"), err)
|
||||
}
|
||||
|
||||
return &Project{baseRepostiory: b}, nil
|
||||
return &Publication{baseRepostiory: b}, nil
|
||||
}
|
||||
|
||||
func (repo Project) Create(p model.Project) error {
|
||||
func (repo Publication) Create(p model.Publication) error {
|
||||
repo.assert.NotNil(repo.db)
|
||||
repo.assert.NotNil(repo.ctx)
|
||||
repo.assert.NotNil(repo.ctx)
|
||||
@@ -59,12 +59,12 @@ func (repo Project) Create(p model.Project) error {
|
||||
}
|
||||
|
||||
q := `
|
||||
INSERT INTO projects (id, title, created_at, updated_at)
|
||||
INSERT INTO publications (id, title, created_at, updated_at)
|
||||
VALUES (:id, :title, :created_at, :updated_at)
|
||||
`
|
||||
|
||||
log := repo.log.With(slog.String("id", p.ID.String()), slog.String("query", q))
|
||||
log.DebugContext(repo.ctx, "Inserting new project")
|
||||
log.DebugContext(repo.ctx, "Inserting new publication")
|
||||
|
||||
_, err = tx.ExecContext(repo.ctx, q,
|
||||
sql.Named("id", p.ID),
|
||||
@@ -73,7 +73,7 @@ func (repo Project) Create(p model.Project) error {
|
||||
sql.Named("updated_at", p.DateUpdated.Format(dateFormat)),
|
||||
)
|
||||
if err != nil {
|
||||
log.ErrorContext(repo.ctx, "Failed to insert project", slog.String("error", err.Error()))
|
||||
log.ErrorContext(repo.ctx, "Failed to insert publication", slog.String("error", err.Error()))
|
||||
return errors.Join(ErrExecuteQuery, err)
|
||||
}
|
||||
|
||||
@@ -85,20 +85,20 @@ func (repo Project) Create(p model.Project) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repo Project) GetByID(projectID uuid.UUID) (project model.Project, err error) {
|
||||
func (repo Publication) GetByID(publicationID uuid.UUID) (publication model.Publication, err error) {
|
||||
repo.assert.NotNil(repo.db)
|
||||
repo.assert.NotNil(repo.ctx)
|
||||
repo.assert.NotNil(repo.log)
|
||||
|
||||
q := `
|
||||
SELECT id, title, created_at, updated_at FROM projects
|
||||
SELECT id, title, created_at, updated_at FROM publications
|
||||
WHERE id = :id
|
||||
`
|
||||
|
||||
log := repo.log.With(slog.String("query", q), slog.String("id", projectID.String()))
|
||||
log.DebugContext(repo.ctx, "Getting project by ID")
|
||||
log := repo.log.With(slog.String("query", q), slog.String("id", publicationID.String()))
|
||||
log.DebugContext(repo.ctx, "Getting publication by ID")
|
||||
|
||||
row := repo.db.QueryRowContext(repo.ctx, q, sql.Named("id", projectID))
|
||||
row := repo.db.QueryRowContext(repo.ctx, q, sql.Named("id", publicationID))
|
||||
|
||||
var id uuid.UUID
|
||||
var title string
|
||||
@@ -106,23 +106,23 @@ func (repo Project) GetByID(projectID uuid.UUID) (project model.Project, err err
|
||||
|
||||
err = row.Scan(&id, &title, &dateCreatedStr, &dateUpdatedStr)
|
||||
if err != nil {
|
||||
log.ErrorContext(repo.ctx, "Failed to scan projects with IDs", slog.String("error", err.Error()))
|
||||
return model.Project{}, errors.Join(ErrInvalidOutput, err)
|
||||
log.ErrorContext(repo.ctx, "Failed to scan publications with IDs", slog.String("error", err.Error()))
|
||||
return model.Publication{}, errors.Join(ErrInvalidOutput, err)
|
||||
}
|
||||
|
||||
dateCreated, err := time.Parse(dateFormat, dateCreatedStr)
|
||||
if err != nil {
|
||||
log.ErrorContext(repo.ctx, "Failed to scan projects with IDs", slog.String("error", err.Error()))
|
||||
return model.Project{}, errors.Join(ErrInvalidOutput, err)
|
||||
log.ErrorContext(repo.ctx, "Failed to scan publications with IDs", slog.String("error", err.Error()))
|
||||
return model.Publication{}, errors.Join(ErrInvalidOutput, err)
|
||||
}
|
||||
|
||||
dateUpdated, err := time.Parse(dateFormat, dateUpdatedStr)
|
||||
if err != nil {
|
||||
log.ErrorContext(repo.ctx, "Failed to scan projects with IDs", slog.String("error", err.Error()))
|
||||
return model.Project{}, errors.Join(ErrInvalidOutput, err)
|
||||
log.ErrorContext(repo.ctx, "Failed to scan publications with IDs", slog.String("error", err.Error()))
|
||||
return model.Publication{}, errors.Join(ErrInvalidOutput, err)
|
||||
}
|
||||
|
||||
return model.Project{
|
||||
return model.Publication{
|
||||
ID: id,
|
||||
Title: title,
|
||||
DateCreated: dateCreated,
|
||||
@@ -130,7 +130,7 @@ func (repo Project) GetByID(projectID uuid.UUID) (project model.Project, err err
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (repo Project) GetByIDs(ids []uuid.UUID) (projects []model.Project, err error) {
|
||||
func (repo Publication) GetByIDs(ids []uuid.UUID) (publications []model.Publication, err error) {
|
||||
repo.assert.NotNil(repo.db)
|
||||
repo.assert.NotNil(repo.ctx)
|
||||
repo.assert.NotNil(repo.log)
|
||||
@@ -147,16 +147,16 @@ func (repo Project) GetByIDs(ids []uuid.UUID) (projects []model.Project, err err
|
||||
}
|
||||
|
||||
q := fmt.Sprintf(`
|
||||
SELECT id, title, created_at, updated_at FROM projects
|
||||
SELECT id, title, created_at, updated_at FROM publications
|
||||
WHERE %s
|
||||
`, strings.Join(c, " OR "))
|
||||
|
||||
log := repo.log.With(slog.String("query", q))
|
||||
log.DebugContext(repo.ctx, "Getting projects by IDs")
|
||||
log.DebugContext(repo.ctx, "Getting publications by IDs")
|
||||
|
||||
rows, err := tx.QueryContext(repo.ctx, q)
|
||||
if err != nil {
|
||||
log.ErrorContext(repo.ctx, "Failed to get projects by IDs", slog.String("error", err.Error()))
|
||||
log.ErrorContext(repo.ctx, "Failed to get publications by IDs", slog.String("error", err.Error()))
|
||||
return nil, errors.Join(ErrExecuteQuery, err)
|
||||
}
|
||||
|
||||
@@ -167,7 +167,7 @@ func (repo Project) GetByIDs(ids []uuid.UUID) (projects []model.Project, err err
|
||||
}
|
||||
}()
|
||||
|
||||
ps := []model.Project{}
|
||||
ps := []model.Publication{}
|
||||
|
||||
for rows.Next() {
|
||||
var id uuid.UUID
|
||||
@@ -176,23 +176,23 @@ func (repo Project) GetByIDs(ids []uuid.UUID) (projects []model.Project, err err
|
||||
|
||||
err := rows.Scan(&id, &title, &dateCreatedStr, &dateUpdatedStr)
|
||||
if err != nil {
|
||||
log.ErrorContext(repo.ctx, "Failed to scan projects with IDs", slog.String("error", err.Error()))
|
||||
log.ErrorContext(repo.ctx, "Failed to scan publications with IDs", slog.String("error", err.Error()))
|
||||
return nil, errors.Join(ErrInvalidOutput, err)
|
||||
}
|
||||
|
||||
dateCreated, err := time.Parse(dateFormat, dateCreatedStr)
|
||||
if err != nil {
|
||||
log.ErrorContext(repo.ctx, "Failed to scan projects with IDs", slog.String("error", err.Error()))
|
||||
log.ErrorContext(repo.ctx, "Failed to scan publications with IDs", slog.String("error", err.Error()))
|
||||
return nil, errors.Join(ErrInvalidOutput, err)
|
||||
}
|
||||
|
||||
dateUpdated, err := time.Parse(dateFormat, dateUpdatedStr)
|
||||
if err != nil {
|
||||
log.ErrorContext(repo.ctx, "Failed to scan projects with IDs", slog.String("error", err.Error()))
|
||||
log.ErrorContext(repo.ctx, "Failed to scan publications with IDs", slog.String("error", err.Error()))
|
||||
return nil, errors.Join(ErrInvalidOutput, err)
|
||||
}
|
||||
|
||||
ps = append(ps, model.Project{
|
||||
ps = append(ps, model.Publication{
|
||||
ID: id,
|
||||
Title: title,
|
||||
DateCreated: dateCreated,
|
||||
@@ -208,7 +208,7 @@ func (repo Project) GetByIDs(ids []uuid.UUID) (projects []model.Project, err err
|
||||
return ps, nil
|
||||
}
|
||||
|
||||
func (repo Project) Update(p model.Project) error {
|
||||
func (repo Publication) Update(p model.Publication) error {
|
||||
repo.assert.NotNil(repo.db)
|
||||
repo.assert.NotNil(repo.ctx)
|
||||
repo.assert.NotNil(repo.ctx)
|
||||
@@ -223,14 +223,14 @@ func (repo Project) Update(p model.Project) error {
|
||||
}
|
||||
|
||||
q := `
|
||||
UPDATE projects
|
||||
UPDATE publications
|
||||
SET title = :title
|
||||
updated_at = :updated_at
|
||||
WHERE id = :id
|
||||
`
|
||||
|
||||
log := repo.log.With(slog.String("id", p.ID.String()), slog.String("query", q))
|
||||
log.DebugContext(repo.ctx, "Updating project")
|
||||
log.DebugContext(repo.ctx, "Updating publication")
|
||||
|
||||
_, err = tx.ExecContext(repo.ctx, q,
|
||||
sql.Named("title", p.Title),
|
||||
@@ -238,7 +238,7 @@ func (repo Project) Update(p model.Project) error {
|
||||
sql.Named("id", p.ID),
|
||||
)
|
||||
if err != nil {
|
||||
log.ErrorContext(repo.ctx, "Failed to insert project", slog.String("error", err.Error()))
|
||||
log.ErrorContext(repo.ctx, "Failed to insert publication", slog.String("error", err.Error()))
|
||||
return errors.Join(ErrExecuteQuery, err)
|
||||
}
|
||||
|
||||
@@ -250,7 +250,7 @@ func (repo Project) Update(p model.Project) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repo Project) DeleteByID(id uuid.UUID) error {
|
||||
func (repo Publication) DeleteByID(id uuid.UUID) error {
|
||||
repo.assert.NotNil(repo.db)
|
||||
repo.assert.NotNil(repo.ctx)
|
||||
repo.assert.NotNil(repo.ctx)
|
||||
@@ -261,15 +261,15 @@ func (repo Project) DeleteByID(id uuid.UUID) error {
|
||||
}
|
||||
|
||||
q := `
|
||||
DELETE FROM projects WHERE id = :id
|
||||
DELETE FROM publications WHERE id = :id
|
||||
`
|
||||
|
||||
log := repo.log.With(slog.String("id", id.String()), slog.String("query", q))
|
||||
log.DebugContext(repo.ctx, "Deleting project")
|
||||
log.DebugContext(repo.ctx, "Deleting publication")
|
||||
|
||||
_, err = tx.ExecContext(repo.ctx, q, sql.Named("id", id))
|
||||
if err != nil {
|
||||
log.ErrorContext(repo.ctx, "Failed to delete project", slog.String("error", err.Error()))
|
||||
log.ErrorContext(repo.ctx, "Failed to delete publication", slog.String("error", err.Error()))
|
||||
return errors.Join(ErrExecuteQuery, err)
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ func newBaseRepostiory(ctx context.Context, db *sql.DB, log *slog.Logger, assert
|
||||
|
||||
var (
|
||||
// TODO: Change all ErrDatabaseConn to ErrCloseConn
|
||||
// TODO: Change error to be agnostic to underlying storage type
|
||||
ErrDatabaseConn = errors.New("repository: failed to begin transaction/connection with database")
|
||||
ErrCloseConn = errors.New("repository: failed to close/commit connection")
|
||||
ErrExecuteQuery = errors.New("repository: failed to execute query")
|
||||
|
||||
@@ -14,27 +14,27 @@ import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type projectController struct {
|
||||
projectSvc *service.Project
|
||||
type publicationController struct {
|
||||
publicationSvc *service.Publication
|
||||
|
||||
templates templates.ITemplate
|
||||
|
||||
assert tinyssert.Assertions
|
||||
}
|
||||
|
||||
func newProjectController(
|
||||
projectService *service.Project,
|
||||
func newPublicationController(
|
||||
publicationService *service.Publication,
|
||||
templates templates.ITemplate,
|
||||
assertions tinyssert.Assertions,
|
||||
) *projectController {
|
||||
return &projectController{
|
||||
projectSvc: projectService,
|
||||
) *publicationController {
|
||||
return &publicationController{
|
||||
publicationSvc: publicationService,
|
||||
templates: templates,
|
||||
assert: assertions,
|
||||
}
|
||||
}
|
||||
|
||||
func (ctrl projectController) dashboard(w http.ResponseWriter, r *http.Request) {
|
||||
func (ctrl publicationController) dashboard(w http.ResponseWriter, r *http.Request) {
|
||||
userCtx := NewUserContext(r.Context())
|
||||
|
||||
userID, ok := userCtx.GetUserID()
|
||||
@@ -43,7 +43,7 @@ func (ctrl projectController) dashboard(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
projects, err := ctrl.projectSvc.GetUserProjects(userID)
|
||||
publications, err := ctrl.publicationSvc.ListOwnedBy(userID)
|
||||
if err != nil {
|
||||
problem.NewInternalServerError(err).ServeHTTP(w, r)
|
||||
return
|
||||
@@ -52,15 +52,15 @@ func (ctrl projectController) dashboard(w http.ResponseWriter, r *http.Request)
|
||||
ps := make([]struct {
|
||||
ID string
|
||||
Title string
|
||||
}, len(projects))
|
||||
}, len(publications))
|
||||
|
||||
for i, project := range projects {
|
||||
for i, publication := range publications {
|
||||
ps[i] = struct {
|
||||
ID string
|
||||
Title string
|
||||
}{
|
||||
ID: base64.URLEncoding.EncodeToString([]byte(project.ID.String())),
|
||||
Title: project.Title,
|
||||
ID: base64.URLEncoding.EncodeToString([]byte(publication.ID.String())),
|
||||
Title: publication.Title,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,24 +70,24 @@ func (ctrl projectController) dashboard(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
}
|
||||
|
||||
func (ctrl projectController) getProject(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO: Handle private projects
|
||||
func (ctrl publicationController) getPublication(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO: Handle private publications
|
||||
|
||||
shortProjectID := r.PathValue("projectID")
|
||||
shortPublicationID := r.PathValue("publicationID")
|
||||
|
||||
id, err := base64.URLEncoding.DecodeString(shortProjectID)
|
||||
id, err := base64.URLEncoding.DecodeString(shortPublicationID)
|
||||
if err != nil {
|
||||
problem.NewBadRequest(fmt.Sprintf("Incorrectly encoded project ID: %s", err.Error())).ServeHTTP(w, r)
|
||||
problem.NewBadRequest(fmt.Sprintf("Incorrectly encoded publication ID: %s", err.Error())).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
projectID, err := uuid.ParseBytes(id)
|
||||
publicationID, err := uuid.ParseBytes(id)
|
||||
if err != nil {
|
||||
problem.NewBadRequest("Project ID is not a valid UUID").ServeHTTP(w, r)
|
||||
problem.NewBadRequest("Publication ID is not a valid UUID").ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
project, err := ctrl.projectSvc.GetProject(projectID)
|
||||
publication, err := ctrl.publicationSvc.Get(publicationID)
|
||||
if errors.Is(err, service.ErrNotFound) {
|
||||
problem.NewNotFound().ServeHTTP(w, r)
|
||||
return
|
||||
@@ -96,8 +96,8 @@ func (ctrl projectController) getProject(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Return project template
|
||||
b, err := json.Marshal(project)
|
||||
// TODO: Return publication template
|
||||
b, err := json.Marshal(publication)
|
||||
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
if _, err := w.Write(b); err != nil {
|
||||
@@ -106,7 +106,7 @@ func (ctrl projectController) getProject(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
}
|
||||
|
||||
func (ctrl projectController) createProject(w http.ResponseWriter, r *http.Request) {
|
||||
func (ctrl publicationController) createPublication(w http.ResponseWriter, r *http.Request) {
|
||||
userCtx := NewUserContext(r.Context())
|
||||
|
||||
userID, ok := userCtx.GetUserID()
|
||||
@@ -121,12 +121,12 @@ func (ctrl projectController) createProject(w http.ResponseWriter, r *http.Reque
|
||||
return
|
||||
}
|
||||
|
||||
project, err := ctrl.projectSvc.Create(title, userID)
|
||||
publication, err := ctrl.publicationSvc.Create(title, userID)
|
||||
if err != nil {
|
||||
problem.NewInternalServerError(err).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("/p/%s/", base64.URLEncoding.EncodeToString([]byte(project.ID.String())))
|
||||
path := fmt.Sprintf("/publication/%s/", base64.URLEncoding.EncodeToString([]byte(publication.ID.String())))
|
||||
http.Redirect(w, r, path, http.StatusSeeOther)
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"code.capytal.cc/capytal/comicverse/service"
|
||||
"code.capytal.cc/capytal/comicverse/templates"
|
||||
@@ -19,7 +18,7 @@ import (
|
||||
type router struct {
|
||||
userService *service.User
|
||||
tokenService *service.Token
|
||||
projectService *service.Project
|
||||
publicationService *service.Publication
|
||||
|
||||
templates templates.ITemplate
|
||||
assets fs.FS
|
||||
@@ -36,8 +35,8 @@ func New(cfg Config) (http.Handler, error) {
|
||||
if cfg.TokenService == nil {
|
||||
return nil, errors.New("token service is nil")
|
||||
}
|
||||
if cfg.ProjectService == nil {
|
||||
return nil, errors.New("project service is nil")
|
||||
if cfg.PublicationService == nil {
|
||||
return nil, errors.New("publication service is nil")
|
||||
}
|
||||
if cfg.Templates == nil {
|
||||
return nil, errors.New("templates is nil")
|
||||
@@ -55,7 +54,7 @@ func New(cfg Config) (http.Handler, error) {
|
||||
r := &router{
|
||||
userService: cfg.UserService,
|
||||
tokenService: cfg.TokenService,
|
||||
projectService: cfg.ProjectService,
|
||||
publicationService: cfg.PublicationService,
|
||||
|
||||
templates: cfg.Templates,
|
||||
assets: cfg.Assets,
|
||||
@@ -71,7 +70,7 @@ func New(cfg Config) (http.Handler, error) {
|
||||
type Config struct {
|
||||
UserService *service.User
|
||||
TokenService *service.Token
|
||||
ProjectService *service.Project
|
||||
PublicationService *service.Publication
|
||||
|
||||
Templates templates.ITemplate
|
||||
Assets fs.FS
|
||||
@@ -89,8 +88,16 @@ func (router *router) setup() http.Handler {
|
||||
|
||||
log.Debug("Initializing router")
|
||||
|
||||
mux := multiplexer.New()
|
||||
mux = multiplexer.WithFormMethod(mux, "x-method")
|
||||
mux = multiplexer.WithPatternRules(mux,
|
||||
multiplexer.EnsureMethod(),
|
||||
multiplexer.EnsureStrictEnd(),
|
||||
multiplexer.EnsureTrailingSlash(),
|
||||
)
|
||||
|
||||
r := smalltrip.NewRouter(
|
||||
smalltrip.WithAssertions(router.assert),
|
||||
smalltrip.WithMultiplexer(mux),
|
||||
smalltrip.WithLogger(log.WithGroup("smalltrip")),
|
||||
)
|
||||
|
||||
@@ -114,17 +121,17 @@ func (router *router) setup() http.Handler {
|
||||
Templates: router.templates,
|
||||
Assert: router.assert,
|
||||
})
|
||||
projectController := newProjectController(router.projectService, router.templates, router.assert)
|
||||
publicationController := newPublicationController(router.publicationService, router.templates, router.assert)
|
||||
|
||||
r.Handle("/assets/", http.StripPrefix("/assets/", http.FileServerFS(router.assets)))
|
||||
r.Handle("GET /assets/{_file...}", http.StripPrefix("/assets/", http.FileServerFS(router.assets)))
|
||||
|
||||
r.Use(userController.userMiddleware)
|
||||
|
||||
r.HandleFunc("/{$}", func(w http.ResponseWriter, r *http.Request) {
|
||||
r.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO: Add a way to the user to bypass this check and see the landing page.
|
||||
// Probably a query parameter to bypass like "?landing=true"
|
||||
if _, ok := NewUserContext(r.Context()).GetUserID(); ok {
|
||||
projectController.dashboard(w, r)
|
||||
publicationController.dashboard(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -134,23 +141,14 @@ func (router *router) setup() http.Handler {
|
||||
}
|
||||
})
|
||||
|
||||
r.HandleFunc("/login/{$}", userController.login)
|
||||
r.HandleFunc("/register/{$}", userController.register)
|
||||
r.HandleFunc("GET /login/{$}", userController.login)
|
||||
r.HandleFunc("POST /login/{$}", userController.login)
|
||||
r.HandleFunc("GET /register/{$}", userController.register)
|
||||
r.HandleFunc("POST /register/{$}", userController.register)
|
||||
|
||||
// TODO: Provide/redirect short project-id paths to long paths with the project title as URL /projects/title-of-the-project-<start of uuid>
|
||||
r.HandleFunc("GET /p/{projectID}/{$}", projectController.getProject)
|
||||
r.HandleFunc("POST /p/{$}", projectController.createProject)
|
||||
// TODO: Provide/redirect short publication-id paths to long paths with the publication title as URL /publications/title-of-the-publication-<start of uuid>
|
||||
r.HandleFunc("GET /publication/{publicationID}/{$}", publicationController.getPublication)
|
||||
r.HandleFunc("POST /publication/{$}", publicationController.createPublication)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// getMethod is a helper function to get the HTTP method of request, tacking precedence
|
||||
// the "x-method" argument sent by requests via form or query values.
|
||||
func getMethod(r *http.Request) string {
|
||||
m := r.FormValue("x-method")
|
||||
if m != "" {
|
||||
return strings.ToUpper(m)
|
||||
}
|
||||
|
||||
return strings.ToUpper(r.Method)
|
||||
}
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"code.capytal.cc/capytal/comicverse/model"
|
||||
"code.capytal.cc/capytal/comicverse/repository"
|
||||
"code.capytal.cc/loreddev/x/tinyssert"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Project struct {
|
||||
projectRepo *repository.Project
|
||||
permissionRepo *repository.Permissions
|
||||
|
||||
log *slog.Logger
|
||||
assert tinyssert.Assertions
|
||||
}
|
||||
|
||||
func NewProject(
|
||||
project *repository.Project,
|
||||
permissions *repository.Permissions,
|
||||
logger *slog.Logger,
|
||||
assertions tinyssert.Assertions,
|
||||
) *Project {
|
||||
return &Project{
|
||||
projectRepo: project,
|
||||
permissionRepo: permissions,
|
||||
|
||||
log: logger,
|
||||
assert: assertions,
|
||||
}
|
||||
}
|
||||
|
||||
func (svc Project) Create(title string, ownerUserID ...uuid.UUID) (model.Project, error) {
|
||||
log := svc.log.With(slog.String("title", title))
|
||||
log.Info("Creating project")
|
||||
defer log.Info("Finished creating project")
|
||||
|
||||
id, err := uuid.NewV7()
|
||||
if err != nil {
|
||||
return model.Project{}, fmt.Errorf("service: failed to generate id: %w", err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
p := model.Project{
|
||||
ID: id,
|
||||
Title: title,
|
||||
DateCreated: now,
|
||||
DateUpdated: now,
|
||||
}
|
||||
|
||||
err = svc.projectRepo.Create(p)
|
||||
if err != nil {
|
||||
return model.Project{}, fmt.Errorf("service: failed to create project: %w", err)
|
||||
}
|
||||
|
||||
if len(ownerUserID) > 0 {
|
||||
err := svc.SetAuthor(p.ID, ownerUserID[0])
|
||||
if err != nil {
|
||||
return model.Project{}, err
|
||||
}
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (svc Project) SetAuthor(projectID uuid.UUID, userID uuid.UUID) error {
|
||||
log := svc.log.With(slog.String("project", projectID.String()), slog.String("user", userID.String()))
|
||||
log.Info("Setting project owner")
|
||||
defer log.Info("Finished setting project owner")
|
||||
|
||||
if _, err := svc.permissionRepo.GetByID(projectID, userID); err == nil {
|
||||
err := svc.permissionRepo.Update(projectID, userID, model.PermissionAuthor)
|
||||
if err != nil {
|
||||
return fmt.Errorf("service: failed to update project author: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
p := model.PermissionAuthor
|
||||
|
||||
err := svc.permissionRepo.Create(projectID, userID, p)
|
||||
if err != nil {
|
||||
return fmt.Errorf("service: failed to set project owner: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc Project) GetUserProjects(userID uuid.UUID) ([]model.Project, error) {
|
||||
perms, err := svc.permissionRepo.GetByUserID(userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("service: failed to get user permissions: %w", err)
|
||||
}
|
||||
|
||||
ids := []uuid.UUID{}
|
||||
for project, permissions := range perms {
|
||||
if permissions.Has(model.PermissionRead) {
|
||||
ids = append(ids, project)
|
||||
}
|
||||
}
|
||||
|
||||
if len(ids) == 0 {
|
||||
return []model.Project{}, nil
|
||||
}
|
||||
|
||||
projects, err := svc.projectRepo.GetByIDs(ids)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("service: failed to get projects: %w", err)
|
||||
}
|
||||
|
||||
return projects, nil
|
||||
}
|
||||
|
||||
func (svc Project) GetProject(projectID uuid.UUID) (model.Project, error) {
|
||||
p, err := svc.projectRepo.GetByID(projectID)
|
||||
if err != nil {
|
||||
return model.Project{}, fmt.Errorf("service: failed to get project: %w", err)
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
124
service/publication.go
Normal file
124
service/publication.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"code.capytal.cc/capytal/comicverse/model"
|
||||
"code.capytal.cc/capytal/comicverse/repository"
|
||||
"code.capytal.cc/loreddev/x/tinyssert"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Publication struct {
|
||||
publicationRepo *repository.Publication
|
||||
permissionRepo *repository.Permissions
|
||||
|
||||
log *slog.Logger
|
||||
assert tinyssert.Assertions
|
||||
}
|
||||
|
||||
func NewPublication(
|
||||
publication *repository.Publication,
|
||||
permissions *repository.Permissions,
|
||||
logger *slog.Logger,
|
||||
assertions tinyssert.Assertions,
|
||||
) *Publication {
|
||||
return &Publication{
|
||||
publicationRepo: publication,
|
||||
permissionRepo: permissions,
|
||||
|
||||
log: logger,
|
||||
assert: assertions,
|
||||
}
|
||||
}
|
||||
|
||||
func (svc Publication) Get(publicationID uuid.UUID) (model.Publication, error) {
|
||||
p, err := svc.publicationRepo.GetByID(publicationID)
|
||||
if err != nil {
|
||||
return model.Publication{}, fmt.Errorf("service: failed to get publication: %w", err)
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (svc Publication) Create(title string, ownerUserID ...uuid.UUID) (model.Publication, error) {
|
||||
log := svc.log.With(slog.String("title", title))
|
||||
log.Info("Creating publication")
|
||||
defer log.Info("Finished creating publication")
|
||||
|
||||
id, err := uuid.NewV7()
|
||||
if err != nil {
|
||||
return model.Publication{}, fmt.Errorf("service: failed to generate id: %w", err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
p := model.Publication{
|
||||
ID: id,
|
||||
Title: title,
|
||||
DateCreated: now,
|
||||
DateUpdated: now,
|
||||
}
|
||||
|
||||
err = svc.publicationRepo.Create(p)
|
||||
if err != nil {
|
||||
return model.Publication{}, fmt.Errorf("service: failed to create publication: %w", err)
|
||||
}
|
||||
|
||||
if len(ownerUserID) > 0 {
|
||||
err := svc.SetAuthor(p.ID, ownerUserID[0])
|
||||
if err != nil {
|
||||
return model.Publication{}, err
|
||||
}
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (svc Publication) SetAuthor(publicationID uuid.UUID, userID uuid.UUID) error {
|
||||
log := svc.log.With(slog.String("publication", publicationID.String()), slog.String("user", userID.String()))
|
||||
log.Info("Setting publication owner")
|
||||
defer log.Info("Finished setting publication owner")
|
||||
|
||||
if _, err := svc.permissionRepo.GetByID(publicationID, userID); err == nil {
|
||||
err := svc.permissionRepo.Update(publicationID, userID, model.PermissionAuthor)
|
||||
if err != nil {
|
||||
return fmt.Errorf("service: failed to update publication author: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
p := model.PermissionAuthor
|
||||
|
||||
err := svc.permissionRepo.Create(publicationID, userID, p)
|
||||
if err != nil {
|
||||
return fmt.Errorf("service: failed to set publication owner: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc Publication) ListOwnedBy(userID uuid.UUID) ([]model.Publication, error) {
|
||||
perms, err := svc.permissionRepo.GetByUserID(userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("service: failed to get user permissions: %w", err)
|
||||
}
|
||||
|
||||
ids := []uuid.UUID{}
|
||||
for publication, permissions := range perms {
|
||||
if permissions.Has(model.PermissionRead) {
|
||||
ids = append(ids, publication)
|
||||
}
|
||||
}
|
||||
|
||||
if len(ids) == 0 {
|
||||
return []model.Publication{}, nil
|
||||
}
|
||||
|
||||
publications, err := svc.publicationRepo.GetByIDs(ids)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("service: failed to get publications: %w", err)
|
||||
}
|
||||
|
||||
return publications, nil
|
||||
}
|
||||
@@ -4,12 +4,12 @@
|
||||
{{if and (ne . nil) (ne (len .) 0)}}
|
||||
<section class="flex h-64 flex-col gap-5">
|
||||
<div class="flex justify-between">
|
||||
<h2 class="text-2xl">Projects</h2>
|
||||
<form action="/p/" method="post">
|
||||
<h2 class="text-2xl">Publications</h2>
|
||||
<form action="/publication/" method="post">
|
||||
<button
|
||||
class="rounded-full bg-slate-700 p-1 px-3 text-sm text-slate-100"
|
||||
>
|
||||
New project
|
||||
New publication
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -20,11 +20,11 @@
|
||||
<div class="w-38 grid h-full grid-rows-2 bg-slate-500">
|
||||
<div class="bg-blue-500 p-2">Image</div>
|
||||
<div class="p-2">
|
||||
<a href="/p/{{.ID}}/">
|
||||
<a href="/publication/{{.ID}}/">
|
||||
<h3>{{.Title}}</h3>
|
||||
<p class="hidden">{{.ID}}</p>
|
||||
</a>
|
||||
<form action="/p/{{.ID}}/" method="post">
|
||||
<form action="/publication/{{.ID}}/" method="post">
|
||||
<input type="hidden" name="x-method" value="delete" />
|
||||
<button
|
||||
class="rounded-full bg-red-700 p-1 px-3 text-sm text-slate-100"
|
||||
@@ -41,16 +41,21 @@
|
||||
<div
|
||||
class="fixed flex h-screen w-full items-center justify-center top-0 left-0"
|
||||
>
|
||||
<form action="/p/" method="post" class="bg-slate-300 rounded-full">
|
||||
<form
|
||||
action="/publication/"
|
||||
method="post"
|
||||
class="bg-slate-300 rounded-full"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
name="title"
|
||||
placeholder="Project title"
|
||||
placeholder="Publication title"
|
||||
value="{{randomName}}"
|
||||
required
|
||||
class="pl-5"
|
||||
/>
|
||||
<button class="rounded-full bg-slate-700 p-2 px-5 text-slate-100">
|
||||
New project
|
||||
New publication
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
{{define "project"}}
|
||||
{{template "layout-page-start" (args "Title" .Title)}}
|
||||
<div class="fixed w-full h-full bg-green-500 grid grid-cols-4 grid-rows-1">
|
||||
<nav class="bg-red-500 h-full">
|
||||
<h1>{{.Title}}</h1>
|
||||
<p>{{.ID}}</p>
|
||||
</nav>
|
||||
<main class="overflow-y-scroll flex justify-center col-span-3 py-20">
|
||||
<div class="flex flex-col gap-10 h-fit">
|
||||
{{range $page := .Pages}}
|
||||
<section id="{{$page.ID}}" class="w-fit">
|
||||
<!--
|
||||
INFO: The interaction form could be another page that is shown
|
||||
when "Add Interaction" is clicked. Said page could be also a partial
|
||||
than can replace the current image using htmx, so it is
|
||||
compatible with JavaScript enabled or not.
|
||||
-->
|
||||
<div class="flex flex-row">
|
||||
<form action="/projects/{{$.ID}}/pages/{{$page.ID}}/interactions/" method="post" class="w-100">
|
||||
<div class="flex">
|
||||
{{if (gt (len $page.Interactions) 0)}}
|
||||
<div class="relative flex">
|
||||
<div class="absolute z-2 w-full h-full top-0 left-0">
|
||||
{{range $interactionID, $interaction := $page.Interactions}}
|
||||
<a class="absolute" href="{{$interaction.URL}}"
|
||||
style="top:{{$interaction.Y}}%;left:{{$interaction.X}}%;">
|
||||
<span
|
||||
class="bg-red-200 opacity-10 block w-10 h-10 transform -translate-x-[50%] -translate-y-[50%]"></span>
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
<img src="/projects/{{$.ID}}/pages/{{$page.ID}}/" class="z-1 relative">
|
||||
</div>
|
||||
{{else}}
|
||||
<img src="/projects/{{$.ID}}/pages/{{$page.ID}}/" class="z-1 relative">
|
||||
{{end}}
|
||||
<input type="range" min="0" max="100" name="y" style="writing-mode: vertical-lr;">
|
||||
</div>
|
||||
<input type="range" min="0" max="100" name="x" class="w-full">
|
||||
<input type="url" required name="link" class="bg-slate-300" placeholder="url of interaction">
|
||||
<button class="rounded-full bg-blue-700 p-1 px-3 text-sm text-slate-100">
|
||||
Add interaction
|
||||
</button>
|
||||
</form>
|
||||
{{if (gt (len $page.Interactions) 0)}}
|
||||
<div class="flex flex-col gap-2">
|
||||
{{range $interactionID, $interaction := $page.Interactions}}
|
||||
<form action="/projects/{{$.ID}}/pages/{{$page.ID}}/interactions/{{$interactionID}}/"
|
||||
method="post">
|
||||
<input type="hidden" name="x-method" value="delete">
|
||||
<button class="rounded-full bg-red-700 p-1 px-3 text-sm text-slate-100">
|
||||
🗑️{{$interaction.URL}}
|
||||
</button>
|
||||
</form>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<form action="/projects/{{$.ID}}/pages/{{$page.ID}}/" method="post">
|
||||
<input type="hidden" name="x-method" value="delete">
|
||||
<button class="rounded-full bg-red-700 p-1 px-3 text-sm text-slate-100">
|
||||
Delete
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
{{end}}
|
||||
<form action="/projects/{{.ID}}/pages/" method="post" enctype="multipart/form-data">
|
||||
<input type="file" name="image" required>
|
||||
<button>Add new page</button>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
{{template "layout-page-end"}}
|
||||
{{end}}
|
||||
@@ -1 +0,0 @@
|
||||
{{define "projects"}} {{end}}
|
||||
111
templates/publication.html
Normal file
111
templates/publication.html
Normal file
@@ -0,0 +1,111 @@
|
||||
{{define "publication"}} {{template "layout-page-start" (args "Title" .Title)}}
|
||||
<div class="fixed w-full h-full bg-green-500 grid grid-cols-4 grid-rows-1">
|
||||
<nav class="bg-red-500 h-full">
|
||||
<h1>{{.Title}}</h1>
|
||||
<p>{{.ID}}</p>
|
||||
</nav>
|
||||
<main class="overflow-y-scroll flex justify-center col-span-3 py-20">
|
||||
<div class="flex flex-col gap-10 h-fit">
|
||||
{{range $page := .Pages}}
|
||||
<section id="{{$page.ID}}" class="w-fit">
|
||||
<!--
|
||||
INFO: The interaction form could be another page that is shown
|
||||
when "Add Interaction" is clicked. Said page could be also a partial
|
||||
than can replace the current image using htmx, so it is
|
||||
compatible with JavaScript enabled or not.
|
||||
-->
|
||||
<div class="flex flex-row">
|
||||
<form
|
||||
action="/publications/{{$.ID}}/pages/{{$page.ID}}/interactions/"
|
||||
method="post"
|
||||
class="w-100"
|
||||
>
|
||||
<div class="flex">
|
||||
{{if (gt (len $page.Interactions) 0)}}
|
||||
<div class="relative flex">
|
||||
<div class="absolute z-2 w-full h-full top-0 left-0">
|
||||
{{range $interactionID, $interaction := $page.Interactions}}
|
||||
<a
|
||||
class="absolute"
|
||||
href="{{$interaction.URL}}"
|
||||
style="top:{{$interaction.Y}}%;left:{{$interaction.X}}%;"
|
||||
>
|
||||
<span
|
||||
class="bg-red-200 opacity-10 block w-10 h-10 transform -translate-x-[50%] -translate-y-[50%]"
|
||||
></span>
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
<img
|
||||
src="/publications/{{$.ID}}/pages/{{$page.ID}}/"
|
||||
class="z-1 relative"
|
||||
/>
|
||||
</div>
|
||||
{{else}}
|
||||
<img
|
||||
src="/publications/{{$.ID}}/pages/{{$page.ID}}/"
|
||||
class="z-1 relative"
|
||||
/>
|
||||
{{end}}
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
name="y"
|
||||
style="writing-mode: vertical-lr"
|
||||
/>
|
||||
</div>
|
||||
<input type="range" min="0" max="100" name="x" class="w-full" />
|
||||
<input
|
||||
type="url"
|
||||
required
|
||||
name="link"
|
||||
class="bg-slate-300"
|
||||
placeholder="url of interaction"
|
||||
/>
|
||||
<button
|
||||
class="rounded-full bg-blue-700 p-1 px-3 text-sm text-slate-100"
|
||||
>
|
||||
Add interaction
|
||||
</button>
|
||||
</form>
|
||||
{{if (gt (len $page.Interactions) 0)}}
|
||||
<div class="flex flex-col gap-2">
|
||||
{{range $interactionID, $interaction := $page.Interactions}}
|
||||
<form
|
||||
action="/publications/{{$.ID}}/pages/{{$page.ID}}/interactions/{{$interactionID}}/"
|
||||
method="post"
|
||||
>
|
||||
<input type="hidden" name="x-method" value="delete" />
|
||||
<button
|
||||
class="rounded-full bg-red-700 p-1 px-3 text-sm text-slate-100"
|
||||
>
|
||||
🗑️{{$interaction.URL}}
|
||||
</button>
|
||||
</form>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<form action="/publications/{{$.ID}}/pages/{{$page.ID}}/" method="post">
|
||||
<input type="hidden" name="x-method" value="delete" />
|
||||
<button
|
||||
class="rounded-full bg-red-700 p-1 px-3 text-sm text-slate-100"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
{{end}}
|
||||
<form
|
||||
action="/publications/{{.ID}}/pages/"
|
||||
method="post"
|
||||
enctype="multipart/form-data"
|
||||
>
|
||||
<input type="file" name="image" required />
|
||||
<button>Add new page</button>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
{{template "layout-page-end"}} {{end}}
|
||||
1
templates/publications.html
Normal file
1
templates/publications.html
Normal file
@@ -0,0 +1 @@
|
||||
{{define "publications"}} {{end}}
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"html/template"
|
||||
"io"
|
||||
"io/fs"
|
||||
|
||||
"code.capytal.cc/capytal/comicverse/internals/randname"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -32,6 +34,9 @@ var (
|
||||
|
||||
return m, nil
|
||||
},
|
||||
"randomName": func() string {
|
||||
return randname.New()
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user