Merge branch 'profile-feature'

This commit is contained in:
a-ill
2023-08-01 15:58:04 +03:00
127 changed files with 9576 additions and 680 deletions

View File

@@ -25,7 +25,11 @@
<!--<loadscreen-component></loadscreen-component>-->
<div id="content">
<navbar-component></navbar-component>
<% if authenticated() %>
<navbar-logged></navbar-logged>
<% else %>
<navbar-not-logged></navbar-not-logged>
<% end %>
<%@yield%>

View File

@@ -0,0 +1,193 @@
module AuthenticationController
using Genie, Genie.Requests, Genie.Renderer, Genie.Renderer.Json, Genie.Renderer.Html, GenieSession, SearchLight, GenieAuthentication, GenieAuthorisation
using Logging
using JSON3, Random, Base64, HTTP, Dates
using Server.Users, Server.EmailSupport, Server.TemplateEditor, Server.Cookies, Server.DatabaseSupport
import Server.TemplateEditor.generate_layout_html
import Server.DatabaseSupport.select_from_table
using JWTs
#---Helpers----------------------------------------------------------
const keyset = JWKSet("https://www.googleapis.com/oauth2/v3/certs")
refresh!(keyset)
current_user() = findone(Users.User, id = get_authentication())
function send_signup_confirmation_email(receiver,confirmation_code)
subject,message = ["Sign-up confirmation","Hello!\r\nYour confirmation code is "*confirmation_code*"\r\n"]
message = "Content-Type: text/html\r\n"*message
return send_email(receiver,subject,message)
end
function register_google()
jws = rawpayload()
jws_split = split(jws,".")
payload_encoded = jws_split[2]
rem = length(payload_encoded)%4
if rem!= 0
payload_encoded = payload_encoded* "="^(4-rem)
end
payload = String(base64decode(payload_encoded))
json = JSON3.read(payload)
sub = json[:sub]
email = json[:email]
user = findone(User, email = email)
if isnothing(user)
# ENABLE WHEN IN PRODUCTION
user = User(email = email,google_id = sub) |> save!
authenticate(user.id, GenieSession.session(params()))
assign_role(user, findone(Role, name = "free"))
save(user)
return true
return 0
else
jwt = JWT(payload="")
jwt.header = jws_split[1]
jwt.payload = jws_split[2]
jwt.signature = jws_split[3]
if validate!(jwt, keyset)
if user.google_id==""
user.google_id = sub
save(user)
authenticate(user.id, GenieSession.session(params()))
return 3
elseif user.google_id==sub
authenticate(user.id, GenieSession.session(params()))
return 3
else
return 0
end
else
return 0
end
end
end
function get_locale()
data = payload()
if :locale in keys(data)
return data[:locale]
else
return "en"
end
end
const auth_info = Dict(
"en" => Dict(
:title => "LibSoc - Login/Sign Up",
:description => ""
),
"ru" => Dict(
:title => "LibSoc - Логин/Регистрация",
:description => ""
)
)
#---Routing functions---------------------------------------------------
controller = "authentication"
const dict_layouts = Dict(
:auth => generate_layout_html("main",controller,"auth",libraries=["GoogleAuth"]),
:profile => generate_layout_html("main",controller,"profile",libraries=["Leaflet"]),
:email_confirmation => generate_layout_html("main",controller,"email_confirmation"),
)
function auth()
locale = get_locale()
set_cookies(params())
html(:authentication,:auth, layout = dict_layouts[:auth], context = @__MODULE__,
title = auth_info[locale][:title],
description = auth_info[locale][:description]
)
end
function profile()
set_cookies(params())
html(:authentication,:profile, layout = dict_layouts[:profile], context = @__MODULE__,
title = "Chiron | Profile",
description = ""
)
end
function email_confirmation()
set_cookies(params())
html(:authentication,:email_confirmation, layout = dict_layouts[:email_confirmation], context = @__MODULE__,
title = "Chiron | Email Confirmation",
description = ""
)
end
function confirm_email()
code = rawpayload()
user = current_user()
if code==user.confirmation_code
GenieAuthorisation.Relationship!(user, findone(Role, name = "unconfirmed")) |> delete
assign_role(user, findone(Role, name = "free"))
return true
else
return false
end
end
function register()
data = jsonpayload()
user = findone(User, email = data["email"])
if isnothing(user)
user = User(email = data["email"],
password = data["password"] |> Users.hash_password,
) |> save!
authenticate(user.id, GenieSession.session(params()))
assign_role(user, findone(Role, name = "free"))
confirmation_code = randstring('0':'9', 5)
user.confirmation_code = confirmation_code
save(user)
#send_signup_confirmation_email(data["email"],confirmation_code)
return true
else
return false
end
end
function login()
data = jsonpayload()
user = findone(User, email = data["email"])
if isnothing(user)
return 0
else
if (user.password==Users.hash_password(data["password"]))
authenticate(user.id, GenieSession.session(params()))
return 2
else
return 1
end
end
end
function logout()
deauthenticate(GenieSession.session(params()))
return
end
function change_user()
data = jsonpayload()
user = findone(Users.User, id = get_authentication())
for (field,value) in data
setfield!(user, Symbol(field), value)
end
save(user)
return JSON3.write(true)
end
function get_user()
try
user = findone(Users.User, id = get_authentication())
return JSON3.write(user)
catch ex
return JSON3.write(false)
end
end
end

View File

@@ -0,0 +1,2 @@
<auth-component></auth-component>

View File

@@ -0,0 +1 @@
<confirmation-component></confirmation-component>

View File

@@ -0,0 +1,2 @@
<profile-component></profile-component>

View File

@@ -1,6 +1,6 @@
module BasicController
using Genie, Genie.Renderer, Genie.Renderer.Html, Genie.Requests
using Genie, Genie.Renderer, Genie.Renderer.Html, Genie.Requests, GenieAuthentication
using JSON3
using SearchLight
using Server.DatabaseSupport, Server.TemplateEditor

View File

@@ -1,6 +1,6 @@
module CommunesController
using Genie, Genie.Renderer, Genie.Renderer.Html, Genie.Requests
using Genie, Genie.Renderer, Genie.Renderer.Html, Genie.Requests, GenieAuthentication
using JSON3
using SearchLight
using Server.DatabaseSupport, Server.TemplateEditor

View File

@@ -1,6 +1,6 @@
module CooperativesController
using Genie, Genie.Renderer, Genie.Renderer.Html, Genie.Requests
using Genie, Genie.Renderer, Genie.Renderer.Html, Genie.Requests, GenieAuthentication
using JSON3
using SearchLight
using Server.DatabaseSupport, Server.TemplateEditor

View File

@@ -1,9 +1,10 @@
module GroupsController
using Genie, Genie.Renderer, Genie.Renderer.Html, Genie.Requests
using Genie, Genie.Renderer, Genie.Renderer.Html, Genie.Requests, GenieAuthentication, DataFrames
using JSON3
using SearchLight
using Server.DatabaseSupport, Server.TemplateEditor
using SearchLight,SearchLightPostgreSQL, LibPQ, JSON3
using Server.DatabaseSupport, Server.TemplateEditor, Server.Users
import Server.DatabaseSupport: select_from_table, insert_into_table, delete_from_table, exist_in_table
controller = "groups"
dict_layouts = Dict(
@@ -33,8 +34,61 @@ function get_locale()
end
end
#---Helpers-----------------------------------------------------------
function table_to_json(name,df)
ar = []
for df_row in eachrow(df)
dict = Dict()
for id in names(df_row)
dict[id] = df_row[id]
end
push!(ar,dict)
end
open("public/assets/"*name*".json", "w") do io
JSON3.write(io, ar)
end
end
function compile(name)
df = select_from_table([name => ["*"]])
table_to_json(name,df)
end
function move_requests(name)
df_requests = select_from_table(["$(name)_requests" => ["*"]], where_data=["verified" => true, "added" => false])
df = select_from_table([name => ["*"]])
latitudes = df.latitude
longitudes = df.longitude
for df_row in eachrow(df_requests)
ind_id_given = ismissing(df_row.id_given) ? nothing : findfirst(df_row.id_given.==df.id)
if (!isnothing(ind_id_given))
id = df[ind_id_given,:id]
row_found = df[ind_id_given,Not(:id)]
dict = Dict(zip(names(row_found),values(row_found)))
dict["members"] += 1
update_table(name,dict, where_data=["id" => id])
else
id = df_row.id
dict_update = Dict("added" => true)
update_table("$(name)_requests",dict_update, where_data=["id" => id])
df_row_to_add = df_row[Not(:id_given)]
df_row_to_add = df_row_to_add[Not(:verified)]
df_row_to_add = df_row_to_add[Not(:added)]
df_row_to_add = df_row_to_add[Not(:id)]
dict = Dict(zip(names(df_row_to_add),values(df_row_to_add)))
dict["members"] = 1
insert_into_table(name,dict)
end
end
end
#---Functions---------------------------------------------------------
current_user() = findone(Users.User, id = get_authentication())
function groups()
locale = get_locale()
html(:groups,:groups, layout = dict_layouts[:groups], context = @__MODULE__,
@@ -52,8 +106,156 @@ function groups_add()
end
function groups_add_post()
data = jsonpayload()
insert_into_table("groups_requests",data)
data = copy(jsonpayload())
mode = data["mode"]
delete!(data,"mode")
user = current_user()
user_id = user.id
if mode==0 # Create
if user.verified
existing_user_group_data = select_from_table(["users_groups" => ["*"]], where_data=["user_id" => user_id])
has_group = !isempty(existing_user_group_data)
delete!(data,"group_id")
group_id = insert_into_table("groups",data, "RETURNING id")[1,1]
if has_group
user_groups_id = existing_user_group_data[1,"id"]
prev_group_id = existing_user_group_data[1,"group_id"]
update_table("users_groups",Dict("group_id" => group_id), where_data=["id" => user_groups_id])
members = select_from_table(["groups" => ["members"]], where_data=["id" => prev_group_id])[1,1]
if (members==1)
delete_from_table("groups",["id" => prev_group_id])
else
update_table("groups",Dict("members" => members - 1), where_data=["id" => id])
end
else
dict_users_groups = Dict("user_id" => user.id, "group_id" => group_id)
insert_into_table("users_groups",dict_users_groups)
end
compile("groups")
else
data["status"] = 0
data["user_id"] = user_id
insert_into_table("groups_requests",data)
end
elseif mode==1 # Join
data["user_id"] = user_id
if exist_in_table("users_groups",["group_id" => data["group_id"]])
if exist_in_table("groups_requests",["user_id" => user_id])
delete_from_table("groups_requests",["user_id" => user_id])
end
data["status"] = 0
insert_into_table("groups_requests",data)
else
group_id = data["group_id"]
members = select_from_table("groups" => ["members"], where_data = ["id" => group_id])[1,1]
dict = Dict("members" => members + 1)
update_table("groups",dict, where_data=["id" => group_id])
dict_users_groups = Dict("user_id" => user_id, "group_id" => group_id)
insert_into_table("users_groups",dict_users_groups)
end
elseif mode==2 # Move
existing_user_group_data = select_from_table(["users_groups" => ["*"]], where_data=["user_id" => user_id])
group_id = existing_user_group_data[1,"group_id"]
delete!(data,"group_id")
delete!(data,"members")
delete!(data,"contact")
update_table("groups",data, where_data=["id" => group_id])
compile("groups")
elseif mode==3 # Leave
existing_user_group_data = select_from_table(["users_groups" => ["*"]], where_data=["user_id" => user_id])
if size(existing_user_group_data,1)==0
if exist_in_table("groups_requests",["user_id" => user_id])
delete_from_table("groups_requests",["user_id" => user_id])
end
else
delete_from_table("users_groups",["user_id" => user_id])
end
end
return nothing
end
function get_user_groups()
local data_dicts
user_id = get_authentication()
groups_ids = select_from_table("users_groups" => ["group_id"], where_data = ["user_id" => user_id])[:,1]
group_id = isempty(groups_ids) ? nothing : groups_ids[1]
data_dicts = []
if isnothing(group_id)
local data
data = select_from_table("groups_requests" => ["*"], where_data = ["user_id" => user_id,"status" => 0])
if size(data,1)==0
data = select_from_table("groups_requests" => ["*"], where_data = ["user_id" => user_id,"status" => 2])
if size(data,1)!=0
data = data[[end],:]
end
end
for row in eachrow(data)
dict = Dict(zip(names(row),values(row)))
if (!ismissing(row["group_id"]))
extra_data = select_from_table("groups" => ["*"], where_data = ["id" => row["group_id"]])
merge!(dict, Dict(zip(names(extra_data[1,:]),values(extra_data[1,:]))))
end
push!(data_dicts, dict)
end
else
group_data = select_from_table("groups" => ["*"], where_data = ["id" => group_id])
ns = names(group_data)
data_dicts = map(x -> Dict(zip(ns,values(x))),eachrow(group_data))
end
return JSON3.write(data_dicts)
end
function get_group_requests()
user_id = get_authentication()
groups_ids = select_from_table("users_groups" => ["group_id"], where_data = ["user_id" => user_id])[:,1]
group_id = isempty(groups_ids) ? nothing : groups_ids[1]
data_dicts = []
if !isnothing(group_id)
user_ids = select_from_table("groups_requests" => ["user_id"], where_data = ["group_id" => group_id, "status" => 0])[:,1]
for user2_id in user_ids
email = select_from_table("users" => ["email"], where_data = ["id" => user2_id])[1,1]
push!(data_dicts,Dict("email" => email, "user_id" => user2_id))
end
end
return JSON3.write(data_dicts)
end
function approve_request()
data = copy(jsonpayload())
user_id = get_authentication()
groups_ids = select_from_table("users_groups" => ["group_id"], where_data = ["user_id" => user_id])[:,1]
group_id = isempty(groups_ids) ? nothing : groups_ids[1]
members = select_from_table("groups" => ["members"], where_data = ["id" => group_id])[1,1]
dict = Dict("members" => members + 1)
update_table("groups",dict, where_data=["id" => group_id])
update_table("groups_requests",Dict("status" => 1), where_data=["group_id" => group_id, "user_id" => data["user_id"]])
dict_users_groups = Dict("user_id" => data["user_id"], "group_id" => group_id)
insert_into_table("users_groups",dict_users_groups)
return nothing
end
function reject_request()
data = copy(jsonpayload())
user_id = get_authentication()
groups_ids = select_from_table("users_groups" => ["group_id"], where_data = ["user_id" => user_id])[:,1]
group_id = isempty(groups_ids) ? nothing : groups_ids[1]
update_table("groups_requests",Dict("status" => 2), where_data=["group_id" => group_id, "user_id" => data["user_id"]])
return nothing
end
function add_verified_groups()
groups_create_requests_verified = select_from_table("groups_requests" => ["*"], where_data = ["group_id" => nothing, "status" => 1])
data = Dict(zip(names(groups_create_requests_verified),groups_create_requests_verified[end,:]))
user_id = data["user_id"]
delete!(data,"group_id")
delete!(data,"user_id")
delete!(data,"id")
delete!(data,"status")
group_id = insert_into_table("groups",data, "RETURNING id")[1,1]
dict_users_groups = Dict("user_id" => user_id, "group_id" => group_id)
insert_into_table("users_groups",dict_users_groups)
delete_from_table("groups_requests",["user_id" => user_id])
end
end

View File

@@ -1,6 +1,6 @@
module PartiesController
using Genie, Genie.Renderer, Genie.Renderer.Html, Genie.Requests
using Genie, Genie.Renderer, Genie.Renderer.Html, Genie.Requests, GenieAuthentication
using JSON3
using SearchLight
using Server.DatabaseSupport, Server.TemplateEditor

View File

@@ -1,6 +1,6 @@
module PartnersController
using Genie, Genie.Renderer, Genie.Renderer.Html, Genie.Requests
using Genie, Genie.Renderer, Genie.Renderer.Html, Genie.Requests, GenieAuthentication
using JSON3
using SearchLight
using Server.DatabaseSupport, Server.TemplateEditor

View File

@@ -0,0 +1,29 @@
module Users
using SearchLight, SearchLight.Validation, Server.UsersValidator
using SHA
using Random
export User
Base.@kwdef mutable struct User <: AbstractModel
### FIELDS
id::DbId = DbId()
email::String = ""
password::String = ""
google_id::String = ""
confirmation_code::String = ""
verified::Bool = false
end
Validation.validator(u::Type{User}) = ModelValidator([
ValidationRule(:email, UsersValidator.not_empty),
ValidationRule(:email, UsersValidator.unique),
ValidationRule(:password, UsersValidator.not_empty)
])
function hash_password(password::String)
sha256(password) |> bytes2hex
end
end

View File

@@ -0,0 +1,21 @@
module UsersValidator
using SearchLight, SearchLight.Validation, SearchLight.QueryBuilder
function not_empty(field::Symbol, m::T, args::Vararg{Any})::ValidationResult where {T<:AbstractModel}
isempty(getfield(m, field)) && return ValidationResult(invalid, :not_empty, "should not be empty")
ValidationResult(valid)
end
function unique(field::Symbol, m::T, args::Vararg{Any})::ValidationResult where {T<:AbstractModel}
ispersisted(m) && return ValidationResult(valid) # don't validate updates
if SearchLight.count(typeof(m), where("$field = ?", getfield(m, field))) > 0
return ValidationResult(invalid, :unique, "is already used")
end
ValidationResult(valid)
end
end

View File

@@ -0,0 +1,180 @@
label {
font-size: 1.3rem;
font-family: var(--sans-serif);
}
.auth-pane {
position: relative;
padding: 3.4rem;
padding-top: 3.4rem;
padding-bottom: 3.4rem;
width: 30rem;
height: auto;
}
.auth-title {
position: relative;
top: 0.2rem;
margin-bottom: 2.7rem;
}
.auth-label {
display: inline-block;
margin-bottom: 0.3rem;
}
.authEmailInput, .authPasswordInput {
position: relative;
width: 100%;
border-radius: 0.34rem;
color: #353535;
height: 2.73rem;
padding-left: 0.34rem;
}
.authEmailInput {
margin-bottom: 0.682rem;
}
.auth-button {
margin-top: 1.365rem;
height: 3.412rem;
width: 100%;
font-family: var(--sans-serif,sans-serif);
font-size: 1.3rem;
color: white;
background-color: var(--red);
border-color: var(--red);
border-radius: 0.512rem;
filter: drop-shadow(0.068rem 0.136rem 0.068rem rgb(0 0 0 / 0.4));
}
.auth-button:active {
background-color: var(--darker-pink);
}
#email-msg,#password-msg {
display: inline;
color:red;
font-family: var(--sans-serif,sans-serif);
}
.auth-line {
margin-top: 1.5rem;
width: 100%;
height: 0.07rem;
border: 0;
border-radius: 0.1rem;
background: black;
}
.auth-methods-group {
display: grid;
grid-template-columns: auto ; /*auto auto*/
justify-content: center;
gap: 2.7rem;
margin-top: 2rem;
}
.auth-methods-group img {
height: auto;
width: 3.4rem;
}
.auth-methods-group> div {
position: relative;
border-radius: 6.8rem;
width: 3.4rem;
height: 3.4rem;
overflow: hidden;
}
#google-btn {
position: absolute;
top: -0.8rem;
left: -0.8rem;
}
#google-logo {
position: relative;
background: white;
pointer-events: none;
}
#google-btn-wrapper {
cursor: pointer;
}
#google-btn div {
position: absolute;
height: 5rem;
width: 5rem;
}
#google-btn iframe {
position: absolute;
height: 10rem;
width: 10rem;
}
.password-field {
position: relative;
}
.eye-icon {
display: block;
position: absolute;
cursor: pointer;
opacity: 0.25;
top: 2.6rem;
right: 0.8rem;
width: 1.7rem;
}
.eye-icon * {
pointer-events: none;
}
#forgot-password {
display: block;
position: relative;
margin-top: 0.5rem;
height: 2rem;
color:#5f5f5f;
margin-left: auto;
width: max-content;
font-family: var(--sans-serif);
font-size: 1.3rem;
}
#remember-me {
position: relative;
display: flex;
align-items: center;
margin-top: 1rem;
gap: 1rem;
}
#remember-me-checkbox {
min-height: 1.5rem;
min-width: 1.5rem;
flex: 0;
accent-color: var(--gray);
}
#remember-me label {
position: relative;
margin-top: -0.2rem;
color: #5f5f5f;
}
#content {
position: relative;
display: flex;
justify-content: space-between;
flex-direction: column;
height: 100%;
min-height: 100vh;
}

View File

@@ -1,21 +1,7 @@
:root {
--light-blue:hsl(195, 67%, 95%);
--darker-pink:hsl(344, 60%, 47%);
--pink:hsl(344, 73%, 57%);
--dark-green:hsl(176, 63%, 25%);
--green:hsl(147, 33%, 60%);
--orange:hsl(30, 97%, 72%);
--light-orange: hsl(19, 76%, 72%);
--dark-brown:hsl(23, 47%, 20%);
--brown:hsl(23, 47%, 30%);
--light-brown: hsl(23, 47%, 50%);
--dark-pink:hsl(343, 39%, 16%);
--red:hsl(359, 72%, 61%);
--dark-blue:hsl(217, 25%, 16%);
--grey-blue:hsl(223, 13%, 22%);
--cream:hsl(34, 43%, 90%);
--dark-cream:hsl(33, 26%, 84%);
--red:#c52a28;
--gray: #5B6970;
--sans-serif: "OpenSans";
--serif: "Lora";
}
@@ -48,12 +34,10 @@ body {
#content {
position: relative;
display: flex;
flex-direction: column;
justify-content: space-between;
display: grid;
grid-template-rows: max-content auto max-content;
height: 100%;
min-height: 100vh;
flex-grow: 1;
}
/*---Fonts---------------------------------------------------------*/
@@ -475,9 +459,8 @@ input[type=number]::-webkit-outer-spin-button {
.pane {
background: white;
border: 0;
border: 0.1rem solid rgb(187, 187, 187);
border-radius: 0.635rem;
box-shadow: 0 0 0.314rem rgb(187, 187, 187);
}
.pane-container {

View File

@@ -0,0 +1,268 @@
/* Header */
#navbar{
position: relative;
top: 0;
width: min(100%,116rem);
z-index: 1000000000;
height: 5.26rem;
padding-left: 0rem;
padding-right: 0rem;
}
#navbar * {
font-family: var(--sans-serif, sans-serif);
}
/* Logo */
#logo-container {
display: flex;
position: absolute;
margin-left: 1rem;
height: 100%;
max-height: 5.26rem;
color: black;
z-index: 1;
flex-direction: row;
flex-wrap: nowrap;
align-items: center;
}
#navbar-logo {
height: 3.5rem;
width: 3.5rem;
object-fit: contain;
border-radius: 10rem;
}
#navbar-logo-text {
position: relative;
word-wrap: normal;
height: 100%;
line-height: 400%;
font-size: 1.4rem;
color: #292222;
font-family: var(--sans-serif, sans-serif);
font-weight: 400;
padding-left: 1.2rem;
}
/* Nav menu */
#nav {
position: fixed;
width: 100%;
height: 100%;
background-color: white;
overflow: hidden;
z-index: 0;
}
#menu > li > a, .options-button {
display: block;
padding: 1.2rem;
padding-top: 1rem;
padding-bottom: 1rem;
color: black;
font-size: 1.4rem;
}
#menu > li > a:active{
}
#menu li {
list-style-type: none;
}
#nav{
max-height: 0;
/*transition: max-height .5s ease-out;*/
}
/* Menu Icon */
#hamb{
position: absolute;
cursor: pointer;
right: 0rem;
padding: 2.8rem 2rem;
z-index: 9999;
}/* Style label tag */
#hamb-line {
background: black;
display: block;
height: 2px;
position: relative;
width: 24px;
} /* Style span tag */
#hamb-line::before,
#hamb-line::after{
background: black;
content: '';
display: block;
height: 100%;
position: absolute;
transition: all .2s ease-out;
width: 100%;
}
#hamb-line::before{
top: 5px;
}
#hamb-line::after{
top: -5px;
}
#side-menu {
display: none;
} /* Hide checkbox */
/* Toggle menu icon */
#side-menu:checked ~ nav {
display: block;
max-height: 100%;
padding-top: 5.625rem;
}
#side-menu:checked ~ #logo-container {
position: fixed;
}
#side-menu:checked ~ #hamb {
position: fixed;
}
#side-menu:checked ~ #logo-container {
position: fixed;
}
#side-menu:checked ~ #hamb #hamb-line {
background: transparent;
}
#side-menu:checked ~ #hamb #hamb-line::before {
transform: rotate(-45deg);
top: 0;
}
#side-menu:checked ~ #hamb #hamb-line::after {
transform: rotate(45deg);
top: 0;
}
/* Options */
.options-dropdown {
position: absolute;
display: none;
top: 5.6rem;
right: 1.8rem;
border: #404040 solid 0.1rem;
background-color: white;
z-index: 10;
}
.options-dropdown button, .options-dropdown a {
display: block;
font-family: var(--sans-serif,sans-serif);
font-size: 1.2rem;
width: 100%;
padding: 1rem;
text-align: left;
}
.options-dropdown button:hover, .options-dropdown a:hover {
background-color: var(--red);
color: white;
}
.options-button {
width: 100%;
text-align: left;
}
/* Localization */
#locales {
position: relative;
}
#locales button {
width: 100%;
text-align: left;
height: 4rem;
}
#locales button:hover {
opacity: 0.5;
}
#locales-img {
position: relative;
top: 0rem;
height: 2rem;
margin-left: 1.2rem;
}
/*
#options-dropdown>:first-child {
padding-bottom: 0.5rem;
}
#options-dropdown>:nth-child(2) {
padding-top: 0.5rem;
}
*/
/* Responsiveness */
@media only screen and (min-width: 1200px) {
#navbar {
position: relative;
width: min(100%,116rem);
left: 50%;
transform: translateX(-50%);
padding-right: 4rem;
padding-left: 4rem;
}
#nav {
max-height: none;
top: 0;
position: relative;
float: right;
width: fit-content;
background-color: transparent;
overflow: visible;
}
#side-menu:checked ~ nav {
padding-top: 0;
}
#menu li {
float: left;
}
#menu > li > a:hover, .options-button:hover, #navbar-logo-text:hover {
color: rgb(127, 127, 127);
}
#menu > li > a, .options-button {
padding: 0.9rem;
padding-top: 1.9rem;
padding-bottom: 1.9rem;
}
#hamb {
display: none;
}
#locales {
position: relative;
margin-right: 1.8rem;
}
#locales-img {
top: 0.9rem;
}
}

View File

@@ -0,0 +1,55 @@
/*
#notifications-div>button {
cursor: pointer;
margin-left: 0.341rem;
}
#notifications-div>:nth-child(1) {
width: 1.706rem;
height: 1.706rem;
background-color: #ccc;
border-radius: 100%;
}
#notifications-div>:nth-child(2) {
width: 2.047rem;
height: 2.047rem;
background-color: #ccc;
border-radius: 100%;
}
#notifications-div>:nth-child(3) {
width: 2.389rem;
height: 2.389rem;
background-color: #ccc;
border-radius: 100%;
}
#notifications-div>button>div {
cursor: pointer;
margin: auto;
background-color: white;
border-radius: 100%;
}
#notifications-div>:nth-child(1)>div {
width: 1.228rem;
height: 1.228rem;
}
#notifications-div>:nth-child(2)>div {
width: 1.57rem;
height: 1.57rem;
}
#notifications-div>:nth-child(3)>div {
width: 1.843rem;
height: 1.843rem;
}
*/

View File

@@ -0,0 +1,163 @@
import {getData, sendData} from "/js/libraries/serverTools.js"
export function getUser(user,loaded,callbackOuter) {
let callback = function(response) {
Object.assign(user,JSON.parse(response))
if(callbackOuter!=undefined) {
callbackOuter()
}
loaded.update((val) => {
return val + 1
})
}
getData("/xx/get-user",callback)
}
export function changeUser(name,value,user) {
if (user[name]!=value && user[name]!=undefined) {
user[name] = value
let data = new Object();
data[name] = value
sendData("/xx/change-user",data)
}
}
export function changePasswordVisibility(button) {
let input = button.previousElementSibling
let type = input.type
if (type=="text") {
input.type = "password";
button.style.opacity = 0.25
}
else {
input.type = "text";
button.style.opacity = 1
}
}
export function checkEmail(email,msg) {
if (email.includes("@")) {
return true
}
else {
msg.innerHTML = "must contain '@'"
return false
}
}
export function checkPassword(password,msg) {
let passwordLength = password.length
if (passwordLength<8) {
msg.innerHTML = "must be 8 characters"
return false
}
let numNumbers = password.match(/\d/g)?.length || 0;
if (numNumbers<1) {
msg.innerHTML = "mush have digits"
return false
}
let numLetters = password.match(/\D/g)?.length || 0;
if (numLetters<2) {
msg.innerHTML = "must have letters"
return false
}
return true
}
export function redirectLogged() {
let callback = function(responseText) {
let response = JSON.parse(responseText)
if (response) {
window.location.href = "/";
}
}
getData("/xx/check-login",callback)
}
export function redirectNotLogged() {
let callback = function(responseText) {
let response = JSON.parse(responseText)
if (!response) {
window.location.href = "/";
}
}
getData("/xx/check-login",callback)
}
// Redirect to the landing page
export function toLandingPage(response) {
if (response!=0) {
window.location.href = "/";
}
}
// Redirect to the dashboard page
export function toDashboard() {
window.location.href = "/";
}
// Process log in
export function processLoginResponse(response,msgs,remember) {
if (response==0) {
msgs.email.innerHTML = "not found"
}
else if (response==1) {
msgs.password.innerHTML = "is wrong"
}
else {
if (remember) {
let date = new Date()
date.setMonth(date.getMonth()+1)
date = date.toUTCString()
document.cookie = "__genierememberme=; expires=" + date + "; path=/;SameSite=Lax";
}
toDashboard()
}
}
// Log in
export function login(msgs,inputs) {
msgs.email.innerHTML = ""
msgs.password.innerHTML = ""
let data = {email: inputs.email.value, password: inputs.password.value, remember: inputs.remember.checked}
sendData('/xx/login-post', data, (response) => processLoginResponse(response,msgs,inputs.remember.checked))
}
// Process sign in
function processSignupResponse(response,msgs) {
if (response) {
toDashboard()
}
else {
msgs.email.innerHTML = "already exists"
}
}
// Sign up
export function signup(msgs,inputs) {
msgs.email.innerHTML = ""
let email = inputs.email.value
let password = inputs.password.value
if (checkEmail(email,msgs.email)==false) {
return
}
if (checkPassword(password,msgs.password)==false) {
return
}
let data = {email: email, password: password}
sendData('/xx/signup-post', data, (response) => processSignupResponse(response,msgs))
}
export function confirmEmail(msg,code,callback) {
msg.innerHTML = ""
sendData('xx/confirm-email',code,callback)
}
// Log out
export function logout() {
var xmlHttp = new XMLHttpRequest();
xmlHttp.open( "GET", "/logout", false ); // false for synchronous request
xmlHttp.send( null );
window.location.href = "/";
}

View File

@@ -10,7 +10,7 @@ export function translate(content, x) {
}
}
function addMarkersToLayer(g,layer,content,locale,addPinContent,markerColor) {
function addMarkersToLayer(g,layer,content,locale,addPinContent,markerColor,options) {
let {text,coordinates} = addPinContent(g,content,locale)
var markerIcon = new L.Icon({
iconUrl: 'https://www.libsoc.org/img/common/markers/marker-' + markerColor + '.png',
@@ -21,57 +21,69 @@ function addMarkersToLayer(g,layer,content,locale,addPinContent,markerColor) {
shadowSize: [41, 41]
})
let marker = L.marker(coordinates, {icon: markerIcon})
marker.id = g.id
marker.members = g.members
marker.contact = g.contact
marker.addTo(layer).bindPopup(text)
if (options.pinCallback!=undefined) {
marker.on('click', (event) => options.pinCallback(marker,event))
}
}
export function addMarkersEntries(entries,entriesByCountry,map,content,locale,addPinContent,markerColor) {
export function addMarkersEntries(entries,entriesByCountry,map,content,locale,addPinContent,markerColor,options) {
let entriesMarkersLayer = L.layerGroup()
let entriesMarkersLayerOut = L.layerGroup()
let entriesMarkersLayerIn = L.layerGroup()
for (let g of entries) {
if (g.country!="Online" && g.country!="Worldwide") {
addMarkersToLayer(g,entriesMarkersLayerIn,content,locale,addPinContent,markerColor)
addMarkersToLayer(g,entriesMarkersLayerIn,content,locale,addPinContent,markerColor,options)
}
}
for (let gs of Object.values(entriesByCountry)) {
if (gs.length==1) {
let g = {...gs[0]}
g.country = [g.country]
if (g.country!="Online" && g.country!="Worldwide") {
addMarkersToLayer(g,entriesMarkersLayerOut,content,locale,addPinContent,markerColor)
if (options.enableCountryGrouping) {
for (let gs of Object.values(entriesByCountry)) {
if (gs.length==1) {
let g = {...gs[0]}
g.country = [g.country]
if (g.country!="Online" && g.country!="Worldwide") {
addMarkersToLayer(g,entriesMarkersLayerOut,content,locale,addPinContent,markerColor,options)
}
}
}
else {
if (gs[0].country!="Online" && gs[0].country!="Worldwide") {
let locationName = gs[0].country
let locationCoordinates = [0,0]
let members = 0
let contact = gs[0].contact
for (let g of gs) {
locationCoordinates[0] += g.latitude
locationCoordinates[1] += g.longitude
members += g.members
if (g.contact[0]!=gs[0].contact[0]) {
contact = contactGeneral
else {
if (gs[0].country!="Online" && gs[0].country!="Worldwide") {
let locationName = gs[0].country
let locationCoordinates = [0,0]
let members = 0
let contact = gs[0].contact
for (let g of gs) {
locationCoordinates[0] += g.latitude
locationCoordinates[1] += g.longitude
members += g.members
if (g.contact[0]!=gs[0].contact[0]) {
contact = contactGeneral
}
}
locationCoordinates[0] = locationCoordinates[0]/gs.length
locationCoordinates[1] = locationCoordinates[1]/gs.length
let gNew = {
country: locationName,
latitude: locationCoordinates[0],
longitude: locationCoordinates[1],
members: members,
contact: contact
}
addMarkersToLayer(gNew,entriesMarkersLayerOut,content,locale,addPinContent,markerColor,options)
}
locationCoordinates[0] = locationCoordinates[0]/gs.length
locationCoordinates[1] = locationCoordinates[1]/gs.length
let gNew = {
country: locationName,
latitude: locationCoordinates[0],
longitude: locationCoordinates[1],
members: members,
contact: contact
}
addMarkersToLayer(gNew,entriesMarkersLayerOut,content,locale,addPinContent,markerColor)
}
}
entriesMarkersLayerOut.addTo(entriesMarkersLayer)
map.on("zoomend", () => onZoomEnd(map,entriesMarkersLayer,entriesMarkersLayerOut,entriesMarkersLayerIn))
}
else {
entriesMarkersLayerIn.addTo(entriesMarkersLayer)
}
entriesMarkersLayerOut.addTo(entriesMarkersLayer)
entriesMarkersLayer.addTo(map)
map.on("zoomend", () => onZoomEnd(map,entriesMarkersLayer,entriesMarkersLayerOut,entriesMarkersLayerIn))
return entriesMarkersLayer
}

View File

@@ -7,5 +7,7 @@
"communes": "Communes",
"cooperatives": "Cooperatives",
"parties": "Parties",
"partners": "Partners"
"partners": "Partners",
"login": "Login",
"profile": "Profile"
}

View File

@@ -10,6 +10,7 @@ import watch from "rollup-plugin-watch";
const production = !process.env.ROLLUP_WATCH;
function serve() {
let server;

View File

@@ -0,0 +1,129 @@
<svelte:options tag="auth-component" />
<script>
// Import statements
import { onMount, setContext,getContext } from 'svelte'
import { sendText } from "/js/libraries/serverTools.js"
import * as AuthTools from "/js/libraries/authTools.js"
import "/js/components/login-component.js"
import "/js/components/signup-component.js"
// Main code
AuthTools.redirectLogged()
let loginComponent
let signupComponent
let context = {
googleInit: false
}
setContext("auth",context)
function switchFocus(component) {
if (component==loginComponent) {
loginComponent.focused = true
signupComponent.focused = false
}
else {
loginComponent.focused = false
signupComponent.focused = true
}
}
function callbackGoogle(data) {
console.log(data)
sendText("/signup-google",data.credential,(response) => AuthTools.processLoginResponse(response,context.msgs,context.remember.checked))
}
function initGoogle() {
if (typeof google != 'undefined') {
google.accounts.id.initialize({
client_id: '93612176787-sr8qjqem4e3kok4msrnj8s1illt85a9g.apps.googleusercontent.com',
callback: callbackGoogle,
auto_select: true,
context: "signin"
})
context.googleInit = true
}
else {
setTimeout(initGoogle,100)
}
}
initGoogle()
</script>
<div id="auth-group">
<div id="auth-grid-group">
<login-component bind:this={loginComponent} on:click={() => switchFocus(loginComponent)} on:keydown={() => ""}></login-component>
<signup-component bind:this={signupComponent} on:click={() => switchFocus(signupComponent)} on:keydown={() => ""}></signup-component>
</div>
<div id="auth-or" class="pane">
<span>OR</span>
</div>
</div>
<style>
@import '/css/common.css';
@import '/css/auth.css';
span {
font-size: 1.4rem;
font-family: var(--sans-serif,sans-serif);
}
#auth-group {
margin: auto;
width: auto;
margin-bottom: 3rem;
}
#auth-grid-group {
display: grid;
grid-template-columns: 30rem 30rem;
justify-content: center;
gap: 1.37rem;
width: 100%;
}
#auth-or {
display: flex;
position: absolute;
margin: auto;
top: 40%;/*40%;*/
left: 50%;
transform: translate(-50%, -50%);
width: 5.4rem;
height: 5.4rem;
border-radius: 6.8rem;
background-color: white;
align-items:center;
justify-content:center;
font-family: var(--sans-serif,sans-serif);
font-weight: 500;
}
@media only screen and (max-width: 1200px) {
#auth-grid-group {
display: grid;
grid-template-columns: 30rem;
grid-template-rows: auto auto;
justify-content: center;
gap: 1.37rem;
width: 100%;
}
#auth-or {
top: 40rem;/*46.4rem;*/
}
#auth-group {
margin-top: 2rem;
margin-bottom: 3rem;
}
}
</style>

View File

@@ -0,0 +1,131 @@
<svelte:options tag="confirmation-component" />
<script>
// Import statements
import { onMount } from 'svelte'
import * as AuthTools from "/js/libraries/authTools.js"
// Export statements
// Main code
let confirmationInputs = []
let confirmationMsg
let confirmationButton
function onlyNumberKey(ind,evt) {
// Only ASCII character in that range allowed
var value = evt.data
if (value in ["0","1","2","3","4","5","6","7","8","9"]) {
if (ind<4) {
confirmationInputs[ind+1].focus()
}
else {
AuthTools.confirmEmail(confirmationMsg,getCode(),callback)
}
}
else {
confirmationInputs[ind].value = ""
}
}
function getCode() {
let code = ""
for (let input of confirmationInputs) {
code += input.value
}
return parseInt(code)
}
function callback(response) {
if (response=="true") {
AuthTools.toDashboard()
}
else {
confirmationMsg.innerHTML = "Wrong code"
}
}
onMount(() => {
})
</script>
<div class="pane auth-pane">
<h2 class="auth-title title-highlight">CONFIRMATION CODE</h2>
<div id="confirmationInputs">
<input bind:this={confirmationInputs[0]} class="authConfirmationInput" type="text" maxlength="1" on:input={(evt) => onlyNumberKey(0,evt)}><span class="dash">-</span>
<input bind:this={confirmationInputs[1]} class="authConfirmationInput" type="text" maxlength="1" on:input={(evt) => onlyNumberKey(1,evt)}><span class="dash">-</span>
<input bind:this={confirmationInputs[2]} class="authConfirmationInput" type="text" maxlength="1" on:input={(evt) => onlyNumberKey(2,evt)}><span class="dash">-</span>
<input bind:this={confirmationInputs[3]} class="authConfirmationInput" type="text" maxlength="1" on:input={(evt) => onlyNumberKey(3,evt)}><span class="dash">-</span>
<input bind:this={confirmationInputs[4]} class="authConfirmationInput" type="text" maxlength="1" on:input={(evt) => onlyNumberKey(4,evt)}>
</div>
<span bind:this={confirmationMsg} id="confirmation-msg"></span>
<button bind:this={confirmationButton} class="auth-button" on:click="{() => AuthTools.confirmEmail(confirmationMsg,getCode(),callback)}">Confirm</button>
</div>
<style>
@import '/css/common.css';
.auth-pane {
position: relative;
padding: 3.4rem;
padding-top: 5.5rem;
padding-bottom: 5.5rem;
width: 33rem;
height: auto;
margin: auto;
}
.auth-title {
position: relative;
left: 0.7rem;
top: 0.2rem;
margin-bottom: 1.4rem;
}
.authConfirmationInput {
position: relative;
width: 3.16rem;
font-family: var(--serif,serif);
font-size: 3rem;
border-radius: 0.34rem;
margin-bottom: 0.7rem;
text-align: center;
padding-left: 0;
padding-bottom: 0.3 rem;
}
.dash {
display: block;
font-size: 3rem;
font-family: var(--serif,serif);
}
#confirmationInputs {
margin: auto;
display: grid;
justify-content: space-between;
grid-auto-flow: column;
}
.auth-button {
margin-top: 1.4rem;
height: 3.4rem;
width: 100%;
font-family: var(--sans-serif,sans-serif);
font-size: 1.6rem;
color: white;
background-color: var(--pink);
border-color: var(--pink);
border-radius: 0.5rem;
filter: drop-shadow(0.07rem 0.14rem 0.07rem rgb(0 0 0 / 0.4));
}
#confirmation-msg {
display: inline;
color:red;
}
</style>

View File

@@ -0,0 +1,110 @@
<svelte:options tag="login-component" />
<script>
// Import statements
import { onMount, getContext } from 'svelte'
import * as AuthTools from "/js/libraries/authTools.js"
// Export statements
export let focused = false
// Main code
let emailInput
let passwordInput
let inputs
let passwordVisibilityButton
let emailMsg
let passwordMsg
let msgs
let rememberMe
let googleButton
let parentProps = getContext("auth")
function renderGoogle() {
if (parentProps.googleInit) {
google.accounts.id.renderButton(googleButton,{
theme: 'outline',
size: 'large'
})
let iframe = googleButton.getElementsByTagName('iframe')[0]
iframe.style.height = "5rem"
iframe.style.width = "5rem"
}
else {
setTimeout(renderGoogle,100)
}
}
onMount(() => {
rememberMe.checked = true
inputs = {email: emailInput, password: passwordInput, remember: rememberMe}
msgs = {email: emailMsg, password: passwordMsg}
parentProps.msgs = msgs
parentProps.remember = rememberMe
parentProps.loginGoogle = googleButton
document.addEventListener("keypress", function(event) {
if (event.code == "Enter") {
if (focused) {
AuthTools.login(msgs,inputs)
}
}
})
renderGoogle()
})
</script>
<div id="login-group"class="pane auth-pane">
<h2 class="auth-title">LOG IN</h2>
<label class="auth-label" for="emailInput">Email&nbsp;</label><span bind:this={emailMsg} id="email-msg"></span>
<input bind:this={emailInput} id="emailInput" class="authEmailInput" type="email">
<div class="password-field">
<label class="auth-label" for="passwordInput">Password&nbsp;</label><span bind:this={passwordMsg} id="password-msg"></span>
<input bind:this={passwordInput} id="passwordInput" class="authPasswordInput" type="password">
<button bind:this={passwordVisibilityButton} class="eye-icon" on:click="{() => AuthTools.changePasswordVisibility(passwordVisibilityButton)}">
<object type="image/svg+xml" data="/img/auth/eye_icon.svg" title="eye icon"></object>
</button>
</div>
<div id="remember-me">
<input bind:this={rememberMe} type="checkbox" id="remember-me-checkbox"><label id="remember-me-label" for="passwordInput">remember me</label>
</div>
<button class="auth-button" on:click="{() => AuthTools.login(msgs,inputs)}">Log in</button>
<a id="forgot-password" href="forgot-password">Forgot password?</a>
<!--
<hr class="auth-line">
<div class="auth-methods-group">
<div id="google-btn-wrapper">
<div bind:this={googleButton} id="google-btn"></div>
<img src="/img/auth/google_icon.svg" id="google-logo" alt="google icon">
</div>
<button on:click={openGoogleWindow}>
<img src="img/auth/google_icon.svg" id="navbar-logo" alt="google icon">
</button>
<button onclick="">
<img src="img/auth/facebook_icon.svg" id="navbar-logo" alt="facebook icon">
</button>
<button onclick="">
<img src="img/auth/linkedin_icon.svg" id="navbar-logo" alt="linkedin icon">
</button>
</div>
-->
</div>
<style>
@import '/css/common.css';
@import '/css/auth.css';
</style>

View File

@@ -0,0 +1,243 @@
<svelte:options tag="signup-component" />
<script>
// Import statements
import { onMount, getContext } from 'svelte'
import * as AuthTools from "/js/libraries/authTools.js"
// Export statements
export let focused = false
// Main code
let signupGroup
let emailInput
let passwordInput
let passwordVisibilityButton
let inputs
let googleButton
let emailMsg
let passwordMsg
let msgs
let rememberMe
let dialog
let signUp
let signUpField
let parentProps = getContext("auth")
function removeMsg(msg) {
if (msg.innerHTML!="") {
msg.innerHTML = ""
}
}
function showDialog() {
dialog.style.display = "block"
}
function hide() {
if (dialog!=null) {
dialog.style.display = "none";
}
}
function sendEmail() {
let email = signUpField.value
if (email.includes("@")) {
sendText("/get-email",email)
signUpField.value = ""
signUpField.placeholder = "Subscribed!"
signUpField.style.setProperty("--c", "hsl(147, 33%, 60%)")
}
else {
signUpField.value = ""
signUpField.placeholder = "must contain '@'"
signUpField.style.setProperty("--c", "hsl(0, 100%, 60%)")
}
}
function clearField() {
signUpField.placeholder = ""
}
function renderGoogle() {
if (parentProps.googleInit) {
google.accounts.id.renderButton(googleButton,{
theme: 'outline',
size: 'large'
})
}
else {
setTimeout(renderGoogle,100)
}
}
onMount(() => {
rememberMe.checked = true
inputs = {email: emailInput, password: passwordInput}
msgs = {email: emailMsg, password: passwordMsg}
document.addEventListener("keypress", function(event) {
if (event.code == "Enter") {
if (focused) {
AuthTools.signup(msgs,inputs,toLandingPage)
}
}
})
//renderGoogle()
})
</script>
<div id="signup-group" class="pane auth-pane" bind:this={signupGroup}>
<h2 class="auth-title">SIGN UP</h2>
<label class="auth-label" for="emailInput">Email&nbsp;</label><span bind:this={emailMsg} id="email-msg" on:change={() => removeMsg(emailMsg)}></span>
<input bind:this={emailInput} id="emailInput" class="authEmailInput" type="email">
<div class="password-field">
<label class="auth-label" for="emailInput">Password&nbsp;</label><span bind:this={passwordMsg} id="password-msg"></span>
<input bind:this={passwordInput} id="passwordInput" class="authPasswordInput" type="password" on:change={() => removeMsg(passwordMsg)}>
<button bind:this={passwordVisibilityButton} class="eye-icon" on:click="{() => AuthTools.changePasswordVisibility(passwordVisibilityButton)}">
<object type="image/svg+xml" data="/img/auth/eye_icon.svg" title="eye-icon"></object>
</button>
</div>
<div id="remember-me">
<input bind:this={rememberMe} type="checkbox" id="remember-me-checkbox"><label id="remember-me-label" for="passwordInput">remember me</label>
</div>
<button class="auth-button" on:click="{showDialog}">Sign up</button> <!--() => AuthTools.signup(msgs,inputs,AuthTools.toLandingPage)-->
<p id="forgot-password"></p>
<!--
<hr class="auth-line">
<div class="auth-methods-group">
<button on:click="{showDialog}">
<img src="/img/auth/google_icon.svg" id="navbar-logo" alt="google icon">
</button>
<button onclick="">
<img src="img/auth/facebook_icon.svg" id="navbar-logo" alt="facebook icon">
</button>
<button onclick="">
<img src="img/auth/linkedin_icon.svg" id="navbar-logo" alt="linkedin icon">
</button>
</div>
-->
</div>
<div bind:this={dialog} id="dialog">
<button id="shadow" on:click={hide}></button>
<div id="wrapper" class="pane">
<h2>Registration is closed</h2>
<p>We are still in the process of opening.</p>
<p>Sign up for updates to know when it becomes available:</p>
<div id="newsletter-container">
<input bind:this={signUpField} on:click={clearField} id="newsletterEmailInput" type="text">
<button bind:this={signUp} on:click={sendEmail} id="newsletterEmailButton">sign up</button>
</div>
<button id="no-button" on:click={hide}>No thanks</button>
</div>
</div>
<style>
@import '/css/common.css';
@import '/css/auth.css';
#dialog {
display: none;
}
#wrapper p {
text-align: justify;
}
#wrapper h2 {
text-align: center;
margin-bottom: 1rem;
}
#shadow {
position: fixed;
cursor: default;
top: 50%; right: 50%;
transform: translate(50%,-50%);
width: 100vw;
height: 100vh;
background:rgb(0, 0, 0, 0.2);
z-index: 999999;
}
#newsletter-container {
position: relative;
height: 3rem;
width: 100%;
margin-top: 1rem;
display: flex;
flex-direction: row;
}
#newsletterEmailInput {
height: 2.5rem;
border-radius: 0.2rem 0 0 0.2rem;
filter: drop-shadow( 0.07rem 0.14rem 0.07rem rgb(0 0 0 / 0.4));
flex-grow: 1;
}
#newsletterEmailInput::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */
color: var(--c,gray);
opacity: 1; /* Firefox */
}
#newsletterEmailButton {
width: 6.8rem;
height: 2.5rem;
background: var(--pink);
color: #ffffff;
font-family: var(--sans-serif,sans-serif);
font-size: 1.4rem;
border-radius: 0 0.2rem 0.2rem 0;
filter: drop-shadow( 0.07rem 0.14rem 0.07rem rgb(0 0 0 / 0.4));
}
#newsletterEmailButton:active {
background: var(--darker-pink);
}
#wrapper {
top: 50%; right: 50%;
transform: translate(50%,-50%);
position: fixed;
max-width: 36rem;
width: 90vw;
padding: 2rem 4rem;
z-index: 1999999;
}
#wrapper * {
font-family: var(--sans-serif);
}
#no-button {
position: relative;
left: 50%;
transform: translateX(-50%);
width: 13rem;
height: 3rem;
margin-top: 2rem;
margin-bottom: 0.5rem;
background: #ffffff;
border: 0.2rem solid var(--pink);
font-family: var(--sans-serif,sans-serif);
font-size: 1.4rem;
border-radius: 0.5rem;
filter: drop-shadow( 0.07rem 0.14rem 0.07rem rgb(0 0 0 / 0.4));
}
#no-button:active {
background: hsl(343, 23%, 82%);
}
</style>

View File

@@ -43,7 +43,10 @@
function mapCallback(createMap,content,locale) {
let map = createMap([22, 0],2)
addMarkersEntries(entries,entriesByCountry,map,content,locale,addCommunePinContent,"red")
let options = {
enableCountryGrouping: true,
}
addMarkersEntries(entries,entriesByCountry,map,content,locale,addCommunePinContent,"red",options)
}
function getCountry(x) {

View File

@@ -9,6 +9,7 @@
// Export statements
export let callback = null
export let colors = null
export let map = null
// Main code
let mapContainer

View File

@@ -71,17 +71,17 @@
<div bind:this={root} id="root" class="pane-centering">
<div class="pane-container">
<div id="sidebars-left" class="sidebar">
<div bind:this={sidebarLeft} id="sidebar-left" class="pane">
<div bind:this={sidebarLeft} id="sidebar-left">
<slot name="sidebar-left"></slot>
</div>
<div bind:this={sidebarLeft2} id="sidebar-left2" class="pane">
<div bind:this={sidebarLeft2} id="sidebar-left2">
<slot name="sidebar-left2"></slot>
</div>
</div>
<div bind:this={sidebarRight} id="sidebar-right" class="pane sidebar">
<slot name="sidebar-right"></slot>
</div>
<div bind:this={mainPane} id="main-pane" class="pane">
<div bind:this={mainPane} id="main-pane">
<slot name="main" id="main-slot"></slot>
</div>
</div>
@@ -95,7 +95,10 @@
}
#root {
position: relative;
margin-top: auto;
min-height: var(--min-height,auto);
height: 100%;
}
#main-pane {
@@ -105,10 +108,8 @@
padding-top: var(--padding-top,0rem);
padding-bottom: var(--padding-bottom,0rem);
text-align: justify;
background: var(--background,white);
box-shadow: var(--box-shadow,0 0 0.314rem rgb(187, 187, 187));
margin: auto;
height: min-content;
height: 100%;
max-width: var(--width-main,66rem);
width: var(--width-main,66rem);
z-index: 1;
@@ -124,13 +125,14 @@
flex-direction: column;
gap: 1rem;
margin-left: calc(-1*var(--width-left,22.5rem) - 1rem - 4rem);
width: calc(var(--width-left,22.5rem) + 4rem);
width: max-content;
max-width: 30rem;
}
#sidebar-left,#sidebar-left2 {
position: relative;
background-color: white;
padding: 2rem 2rem;
padding: 0rem 0rem;
}
#sidebar-left {
@@ -149,16 +151,16 @@
padding: 2rem 2rem;
}
@media only screen and (max-width: 1880px) {
@media only screen and (max-width: 1340px) {
#main-pane {
max-width: initial;
width: 100%;
max-width: var(--width-main,66rem);
padding-left: var(--padding-left-mobile,1.8rem);
padding-right: var(--padding-right-mobile,1.8rem);
padding-top: var(--padding-top-mobile,1.8rem);
padding-bottom: var(--padding-bottom-mobile,1.8rem);
padding-left: var(--padding-left-mobile,0rem);
padding-right: var(--padding-right-mobile,0rem);
padding-top: var(--padding-top-mobile,0rem);
padding-bottom: var(--padding-bottom-mobile,0rem);
}
#sidebars-left, #sidebar-right {

View File

@@ -43,7 +43,10 @@
function mapCallback(createMap,content,locale) {
let map = createMap([22, 0],2)
addMarkersEntries(entries,entriesByCountry,map,content,locale,addCoopPinContent,"blue")
let options = {
enableCountryGrouping: true,
}
addMarkersEntries(entries,entriesByCountry,map,content,locale,addCoopPinContent,"blue",options)
}
function getCountry(x) {

View File

@@ -32,8 +32,8 @@
<button on:click={() => {location.href='#'}} id="footer-up" aria-label="go up">
<svg xmlns="http://www.w3.org/2000/svg" width="42.545" height="72.601" viewBox="0 0 42.545 72.601">
<g id="Group_268" data-name="Group 268" transform="translate(-6.177 -2.399)">
<rect id="Rectangle_146" data-name="Rectangle 146" width="11" height="51" rx="5.5" transform="translate(22 24)" fill="#cb1816"/>
<path id="Path_1145" data-name="Path 1145" d="M23.814,4.021a5,5,0,0,1,7.372,0l16.134,17.6c2.94,3.207,1.046,10.4-3.686,8.379S28.02,14.081,28.391,13.524,16.544,27.976,11.366,30,4.741,24.828,7.68,21.621Z" fill="#DD1C1A"/>
<rect id="Rectangle_146" data-name="Rectangle 146" width="11" height="51" rx="5.5" transform="translate(22 24)" fill="var(--red)"/>
<path id="Path_1145" data-name="Path 1145" d="M23.814,4.021a5,5,0,0,1,7.372,0l16.134,17.6c2.94,3.207,1.046,10.4-3.686,8.379S28.02,14.081,28.391,13.524,16.544,27.976,11.366,30,4.741,24.828,7.68,21.621Z" fill="var(--red)"/>
</g>
</svg>
</button>
@@ -54,8 +54,8 @@ footer {
bottom: 0;
width: 100%;
height: auto;
background: #5B6970;/*var(--dark-green);*/
border-top: #cb1816 solid 0.5rem;
background: var(--gray);
border-top: var(--red) solid 0.5rem;
}
footer p, footer a {

View File

@@ -2,19 +2,26 @@
<script>
// Import statements
import { onMount } from 'svelte'
import { onMount, getContext } from 'svelte'
import { writable } from 'svelte/store';
import { loadLocaleContent, getData, sendData } from "/js/libraries/serverTools.js"
import { addMarkersEntries, translate } from "/js/libraries/mapTools.js"
// Import components
import "/js/components/map-component.js"
// Export statements
export let map = null
// Main code
let loaded = writable(0)
let content = writable({})
let entries
let entriesByCountry
let userData
let buttonsGroupMember
let buttonsNotGroupMember
let callback = (response) => {
entries = JSON.parse(response)
@@ -41,11 +48,32 @@
let confirmationMsg
let addressInput
let contactInput
let addressVec
let userPinLat = 0
let membersInput
let addressVec = ["","",""]
let userPinData = {
}
let userPinLng = 0
let userPin = createPin(0,0)
userPin.setOpacity(0)
let modeButtons = []
let context = getContext("profile-component")
let closeGroupsAdd = context.closeGroupsAdd
let maps = context.maps
let onLoadedGroups = context.onLoadedGroups
let userGroups = context.userGroups
let user = context.user
let has_group = userGroups.length!=0
let mode = has_group ? 2 : 0
let pendingGroup
if (has_group) {
pendingGroup= userGroups[0].status!=undefined
if (pendingGroup) {
mode = 3
}
}
let locale = loadLocaleContent(content,"groups-component",loaded)
loadLocaleContent(content,"countries",loaded)
@@ -105,23 +133,25 @@
response = JSON.parse(response)
// Extract the address information from the response
let address = response.address
let city = address.city || address.town || address.village || address.hamlet
let state = address.state
let country = address.country
let fullAddress = country
if (state!=undefined) {
fullAddress += ", " + state
if (address!=undefined) {
let city = address.city || address.town || address.village || address.hamlet
let state = address.state
let country = address.country
let fullAddress = country
if (state!=undefined) {
fullAddress += ", " + state
}
else {
state = ""
}
if (city!=undefined) {
fullAddress += ", " + city
}
else {
city = ""
}
addressVec = [country,state,city]
}
else {
state = ""
}
if (city!=undefined) {
fullAddress += ", " + city
}
else {
city = ""
}
addressVec = [country,state,city]
}
getData(url,callback)
}
@@ -153,85 +183,180 @@
return {text,coordinates}
}
function mapCallback(createMap,content,locale) {
let map = createMap([22, 0],2)
addMarkersEntries(entries,entriesByCountry,map,content,locale,addGroupPinContent,"green")
map = createMap([22, 0],2)
maps["groupsAdd"] = map
let options = {
enableCountryGrouping: false,
pinCallback: pinCallback
}
addMarkersEntries(entries,entriesByCountry,map,content,locale,addGroupPinContent,"green",options)
userPin.addTo(map)
map.on('click', function(event) {
let lat = event.latlng.lat;
let lng = event.latlng.lng;
userPinLat = lat
userPinLng = lng
updatePin(userPin,lat,lng)
userPin.setOpacity(1)
reverseGeocodeLocal(lat, lng)
reverseGeocode(lat, lng)
if (mode==0) {
let lat = event.latlng.lat;
let lng = event.latlng.lng;
userPinData["latitude"] = lat
userPinData["longitude"] = lng
userPinData["id"] = null
updatePin(userPin,lat,lng)
userPin.setOpacity(1)
reverseGeocodeLocal(lat, lng)
reverseGeocode(lat, lng)
}
})
}
function updateConfirmationMsg(response) {
if (response!==false) {
confirmationMsg.innerHTML = "You have been added to our database! Now go to our Discord to verify yourself."
if (mode==0 && !user.verified) {
confirmationMsg.innerHTML = "You have been added to our database! Now go to our Discord to verify yourself."
}
else {
confirmationMsg.innerHTML = "Success!"
}
confirmationMsg.style.color = "green"
if (mode==0 || mode==1) {
userGroups[0] = {}
}
userGroups[0].country = addressVec[0]=="" ? null : addressVec[0]
userGroups[0].state = addressVec[1]=="" ? null : addressVec[1]
userGroups[0].town = addressVec[2]=="" ? null : addressVec[2]
userGroups[0].members = userPinData["members"]
onLoadedGroups()
}
else {
confirmationMsg.innerHTML = "Something went wrong."
confirmationMsg.style.color = "red"
}
}
function submitLocation() {
if (addressVec!=undefined) {
let data = {
if (addressVec[0]!="" || mode==3) {
let membersVal, contactVal
if (mode==0) { // Create
membersVal = membersInput.value
contactVal = contactInput.value
}
else if (mode==1) { // Join
contactVal = contactInput.value
}
else if (mode==2 || mode==3) { // Move
membersVal = ""
contactVal = ""
}
else if (mode==3) { // Leave
membersVal = ""
contactVal = ""
addressVec = [null,null,null]
userPinData["latitude"] = null
userPinData["longitude"] = null
}
userData = {
country: addressVec[0],
state: addressVec[1],
town: addressVec[2],
latitude: userPinLat,
longitude: userPinLng,
contact: contactInput.value
latitude: userPinData["latitude"],
longitude: userPinData["longitude"],
contact: contactVal=="" ? null : contactVal,
members: membersVal=="" ? null : parseInt(membersVal),
group_id: userPinData["id"],
mode: mode
}
if (data.state=="") {
data.state = null
if (userData.state=="") {
userData.state = null
}
if (data.town=="") {
data.town = null
}
if (data.contact=="") {
data.contact = null
if (userData.town=="") {
userData.town = null
}
let url = "/" + locale + "/groups-add-post/"
sendData(url,data,updateConfirmationMsg)
sendData(url,userData,updateConfirmationMsg)
}
}
function resizeInput(el) {
el.nextElementSibling.innerHTML = el.value
}
function pinCallback(marker,event) {
if (mode==1) {
let lat = event.latlng.lat;
let lng = event.latlng.lng;
userPinData["latitude"] = lat
userPinData["longitude"] = lng
userPinData["id"] = marker.id
userPinData["members"] = marker.members
updatePin(userPin,lat,lng)
userPin.setOpacity(1)
reverseGeocodeLocal(lat, lng)
reverseGeocode(lat, lng)
}
}
function chooseButton(index) {
for (let b of modeButtons) {
if (b!=undefined) {
b.style.background = "rgba(197, 43, 40, 0.319)"
b.style.color = "black"
}
}
modeButtons[index].style.background = "rgb(197, 43, 40)"
modeButtons[index].style.color = "white"
mode = index
}
function getAddress(g) {
if (g!=undefined) {
let location = [g.country,g.state,g.town].filter(x => x!=null)
return location.map(x => locale=="en" ? x : translate($content,x)).join(", ")
}
else {
return "Create or join group"
}
}
function onLoaded() {
if ($loaded==3) {
chooseButton(mode)
if (mode==2 || mode==3) {
addressInput.value = getAddress(userGroups[0])
}
}
else {
let f = () => onLoaded()
setTimeout(f, 100)
}
}
onMount(() => {
onLoaded()
})
</script>
{#key $loaded}
{#if $loaded==3}
<div id="container">
<button class="close-button" on:click={closeGroupsAdd}></button>
<!--<img src="img/crowd.png" id="crowd" alt="crowd">-->
<div id="text-container">
<h1>Add a Group</h1>
<img id="groups-img" src="/img/common/groups.svg" alt="groups">
<p class="description">If there are no groups in your town with whom you can organize then do the following:</p>
<ol>
<li>Click on the map to show us where you are located;</li>
<li>Add a way to contact you or leave blank for a pin to point to our discord;</li>
<li>Press "Submit" to add yourself to our map;</li>
<li>Verify yourself by having a chat with us at our Discord server to show on the map;</li>
</ol>
{#if !has_group}
<div bind:this={buttonsNotGroupMember} id="button-line">
<button bind:this={modeButtons[0]} on:click={() => chooseButton(0)}>Create</button>
<button bind:this={modeButtons[1]} on:click={() => chooseButton(1)}>Join</button>
</div>
{:else if has_group && !pendingGroup}
<div bind:this={buttonsGroupMember} id="button-line">
<button bind:this={modeButtons[2]} on:click={() => chooseButton(2)} style={"display: " + (pendingGroup ? "none" : "initial")}>Move</button>
<button bind:this={modeButtons[3]} on:click={() => chooseButton(3)}>Leave</button>
</div>
{:else}
<div bind:this={buttonsGroupMember} id="button-line">
<button bind:this={modeButtons[3]} on:click={() => chooseButton(3)}>Leave</button>
</div>
{/if}
<div id="address-input-wrapper" class="input-label-wrapper">
<label for="address-input">Location: </label>
<div class="input-wrapper">
@@ -239,16 +364,30 @@
<div class="ghost-input"></div>
</div>
</div>
<div class="input-label-wrapper">
<label for="contact-input">Contact: </label>
<div class="input-wrapper">
<input bind:this={contactInput} on:input={() => resizeInput(contactInput)} id="contact-input" type="text">
<div class="ghost-input"></div>
</div>
</div>
{#key mode}
{#if mode==0}
<div id="members-input-wrapper" class="input-label-wrapper">
<label for="members-input">Members: </label>
<div class="input-wrapper">
<input bind:this={membersInput} id="members-input" type="text" value={1}>
</div>
</div>
{/if}
{#if mode==0 || mode==1}
<div class="input-label-wrapper">
<label for="contact-input">Contact: </label>
<div class="input-wrapper">
<input bind:this={contactInput} on:input={() => resizeInput(contactInput)} id="contact-input" type="text">
<div class="ghost-input"></div>
</div>
</div>
{/if}
{/key}
<button id="submit-button" on:click={submitLocation}>Submit</button>
<p id="confirmation-msg" bind:this={confirmationMsg}></p>
<map-component id="map" callback={(createMap) => mapCallback(createMap,$content,locale)}></map-component>
{#if !(has_group && pendingGroup)}
<map-component id="map" callback={(createMap) => mapCallback(createMap,$content,locale)}></map-component>
{/if}
</div>
</div>
{/if}
@@ -257,13 +396,75 @@
<style>
@import '/css/common.css';
#button-line {
position: relative;
width: fit-content;
margin: auto;
margin-top: 1.5rem;
}
#button-line button{
font-family: var(--sans-serif,sans-serif);
font-size: 1.15rem;
padding: 1rem 0;
width: 7rem;
}
#button-line :first-child {
border-top-left-radius: 1rem;
border-bottom-left-radius: 1rem;
margin-right: 0.1rem;
}
#button-line :last-child {
margin-left: 0.1rem;
border-top-right-radius: 1rem;
border-bottom-right-radius: 1rem;
}
.close-button {
position: absolute;
top: 2rem;
right: 0rem;
width: 2rem;
height: 2rem;
border: none;
cursor: pointer;
z-index: 2;
}
.close-button:hover {
opacity: 0.7;
}
.close-button::before,
.close-button::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 0.2rem;
height: 2rem;
background-color: #000;
border-radius: 1rem;
}
.close-button::before {
transform: translate(-50%, -50%) rotate(45deg);
}
.close-button::after {
transform: translate(-50%, -50%) rotate(-45deg);
}
#confirmation-msg {
margin-top: 0.5rem;
margin-bottom: 2rem;
}
ol li {
margin-left: 3rem;
margin-left: 1rem;
margin-bottom: 0.5rem;
}
@@ -273,7 +474,7 @@
font-size: 1.15rem;
line-height: 160%;
color: #222222;
width: 5.5rem;
width: 6rem;
}
input, .ghost-input {
@@ -296,6 +497,14 @@
margin-bottom: 1rem;
}
#members-input-wrapper {
margin-bottom: 1rem;
}
#members-input {
width: 5rem;
}
.ghost-input {
display: block;
visibility: hidden;
@@ -339,42 +548,6 @@
color: white;
}
#add-prompt {
margin-bottom: 2rem;
}
#groups-img {
position: absolute;
width: 14rem;
left: 50%;
transform: translate(-50%);
z-index: 0;
opacity: 0.2;
}
#text-container>:nth-child(3) {
margin-top: 8rem;
}
.country-name {
margin-bottom: 0.5rem;
}
.country-block {
margin-bottom: 2rem;
}
.location-info {
margin-bottom: 0.75rem;
}
.location-info p {
margin-bottom: 0;
}
a {
color: #DD1C1A;
}
#map {
--height: 30rem;
@@ -389,16 +562,6 @@
margin: auto;
}
h1 {
margin-bottom: 1rem;
font-size: 2.2rem;
text-align: center;
}
h3 {
margin-bottom: 1rem;
}
#container {
margin: auto;
max-width: 800px;

View File

@@ -62,10 +62,13 @@
function mapCallback(createMap,content,locale) {
let map = createMap([22, 0],2)
let groupsMarkersLayer = addMarkersEntries(entries["groups"],entriesByCountry["groups"],map,content,locale,addGroupPinContent,"green")
let communesMarkersLayer = addMarkersEntries(entries["communes"],entriesByCountry["communes"],map,content,locale,addCommunePinContent,"red")
let coopsMarkersLayer = addMarkersEntries(entries["cooperatives"],entriesByCountry["cooperatives"],map,content,locale,addCoopPinContent,"blue")
let partiesMarkersLayer = addMarkersEntries(entries["parties"],entriesByCountry["parties"],map,content,locale,addPartyPinContent,"gold")
let options = {
enableCountryGrouping: true,
}
let groupsMarkersLayer = addMarkersEntries(entries["groups"],entriesByCountry["groups"],map,content,locale,addGroupPinContent,"green",options)
let communesMarkersLayer = addMarkersEntries(entries["communes"],entriesByCountry["communes"],map,content,locale,addCommunePinContent,"red",options)
let coopsMarkersLayer = addMarkersEntries(entries["cooperatives"],entriesByCountry["cooperatives"],map,content,locale,addCoopPinContent,"blue",options)
let partiesMarkersLayer = addMarkersEntries(entries["parties"],entriesByCountry["parties"],map,content,locale,addPartyPinContent,"gold",options)
let overlayMaps = {}
overlayMaps[content.groups] = groupsMarkersLayer
@@ -163,7 +166,7 @@
font-family: var(--sans-serif,sans-serif);
width: 14rem;
line-height: 4rem;
background: #cb1816;
background: var(--red);
color: white;
text-align: center;
}

View File

@@ -0,0 +1,151 @@
<svelte:options tag="navbar-logged" />
<script>
// Import statements
import { onMount, getContext } from 'svelte'
import { writable } from 'svelte/store'
import { loadLocaleContent, locales } from "/js/libraries/serverTools.js"
// Main code
let hambInput
let navbar
let localesDropdown
let initiativesDropdown
let loaded = writable(0)
let content = writable({})
let logoText
let locale = loadLocaleContent(content,"navbar-component",loaded)
function changeNavbar() {
if (hambInput.checked) {
navbar.style.background = "white"
//navbar.style.boxShadow = "0 0 0.314rem rgb(187, 187, 187)"
}
else {
setTimeout(()=> {
navbar.style.position = "relative"
navbar.style.background = ""
navbar.style.boxShadow = ""
}
,510)
}
}
function showDropdown(dropdown) {
let state = dropdown.style.display
initiativesDropdown.style.display = "none"
localesDropdown.style.display = "none"
if (state=="block") {
dropdown.style.display = "none"
}
else {
dropdown.style.display = "block"
}
}
function changeLocale(lang) {
localStorage.setItem("locale",lang)
let locSplit = location.href.split("/")
let localesSymbols = Object.keys(locales)
locSplit = locSplit.filter(x => !localesSymbols.includes(x))
let loc = locSplit.slice(0,locSplit.length-1).join("/") + "/" + lang + "/" + locSplit[locSplit.length-1]
location.href = loc
}
function fixHeading() {
if (locale=="ru") {
let func = () => {
if (logoText==undefined) {
setTimeout(func,100)
}
else {
if (((window.innerWidth < 1700 && window.innerWidth > 1400) || window.innerWidth < 400) && logoText.style.lineHeight!="100%") {
logoText.style.lineHeight = "120%"
logoText.style.top = "1rem"
logoText.style.width = "16rem"
}
else if (((window.innerWidth > 1700) || (window.innerWidth > 400 && window.innerWidth < 1400)) && logoText.style.lineHeight!="400%") {
logoText.style.lineHeight = "400%"
logoText.style.top = "0rem"
logoText.style.width = "auto"
}
}
}
func()
addEventListener("resize", func)
}
}
function hide(dropdown) {
let callback = () => {
dropdown.style.display = "none"
}
setTimeout(callback,100)
}
onMount(() => {
fixHeading()
})
</script>
<!-- Navigation bar -->
{#key loaded}
{#if Object.keys($content).length!=0}
<header bind:this={navbar} id="navbar">
<!-- Hamburger icon -->
<input bind:this={hambInput} type="checkbox" id="side-menu" on:click={changeNavbar}>
<label id="hamb" for="side-menu"><span id="hamb-line"></span></label>
<!-- Logo -->
<a id=logo-container href={"/" + locale + "/"}>
<img src="/img/common/flag.png" id="navbar-logo" alt="logo">
<span bind:this={logoText} id="navbar-logo-text" >{@html $content.orgName}</span>
</a>
<!-- Menu -->
<nav id="nav">
<ul id="menu">
<li><a href={"/"+locale+"/join-us"}>{$content.joinUs}</a></li>
<li><a href={"/"+locale+"/manifesto"}>{$content.manifesto}</a></li>
<!-- Options dropdown -->
<!-- A list of links pointing to different pages of the website. Implemented as a div opened on :hover-->
<li id="options-container">
<button on:click={() => showDropdown(initiativesDropdown)} on:focusout={() => hide(initiativesDropdown)} class="options-button">{$content.initiatives}</button>
<div bind:this={initiativesDropdown} class="options-dropdown">
<a href={"/"+locale+"/groups"}>{$content.groups}</a>
<a href={"/"+locale+"/communes"}>{$content.communes}</a>
<a href={"/"+locale+"/cooperatives"}>{$content.cooperatives}</a>
<a href={"/"+locale+"/parties"}>{$content.parties}</a>
<a href={"/"+locale+"/partners"}>{$content.partners}</a>
</div>
</li>
<li><a href={"/"+locale+"/profile"}>{$content.profile}</a></li>
<li id="locales">
<button on:click={() => showDropdown(localesDropdown)} on:focusout={() => hide(localesDropdown)}>
<picture>
<source srcset="/img/common/globe.webp">
<source srcset="/img/common/globe.png">
<img id="locales-img" alt="globe">
</picture>
</button>
</li>
<div bind:this={localesDropdown} class="options-dropdown">
{#each Object.entries(locales) as [loc,name]}
<button on:click={() => changeLocale(loc)}>{name}</button>
{/each}
</div>
</ul>
</nav>
</header>
{/if}
{/key}
<style>
@import '/css/common.css';
@import '/css/navbar.css';
</style>

View File

@@ -1,4 +1,4 @@
<svelte:options tag="navbar-component" />
<svelte:options tag="navbar-not-logged" />
<script>
@@ -121,6 +121,7 @@
<a href={"/"+locale+"/partners"}>{$content.partners}</a>
</div>
</li>
<li><a href={"/"+locale+"/auth"}>{$content.login}</a></li>
<li id="locales">
<button on:click={() => showDropdown(localesDropdown)} on:focusout={() => hide(localesDropdown)}>
<picture>
@@ -146,274 +147,5 @@
<style>
@import '/css/common.css';
/* Header */
#navbar{
position: relative;
top: 0;
width: min(100%,116rem);
z-index: 1000000000;
height: 5.26rem;
padding-left: 0rem;
padding-right: 0rem;
}
#navbar * {
font-family: var(--sans-serif, sans-serif);
}
/* Logo */
#logo-container {
display: flex;
position: absolute;
margin-left: 1rem;
height: 100%;
max-height: 5.26rem;
color: black;
z-index: 1;
flex-direction: row;
flex-wrap: nowrap;
align-items: center;
}
#navbar-logo {
height: 3.5rem;
width: 3.5rem;
object-fit: contain;
border-radius: 10rem;
}
#navbar-logo-text {
position: relative;
word-wrap: normal;
height: 100%;
line-height: 400%;
font-size: 1.4rem;
color: #292222;
font-family: var(--sans-serif, sans-serif);
font-weight: 400;
padding-left: 1.2rem;
}
/* Nav menu */
#nav {
position: fixed;
width: 100%;
height: 100%;
background-color: white;
overflow: hidden;
z-index: 0;
}
#menu > li > a, .options-button {
display: block;
padding: 1.2rem;
padding-top: 1rem;
padding-bottom: 1rem;
color: black;
font-size: 1.4rem;
}
#menu > li > a:active{
background-color: #f7aec0;
}
#menu li {
list-style-type: none;
}
#nav{
max-height: 0;
/*transition: max-height .5s ease-out;*/
}
/* Menu Icon */
#hamb{
position: absolute;
cursor: pointer;
right: 0rem;
padding: 2.8rem 2rem;
z-index: 9999;
}/* Style label tag */
#hamb-line {
background: black;
display: block;
height: 2px;
position: relative;
width: 24px;
} /* Style span tag */
#hamb-line::before,
#hamb-line::after{
background: black;
content: '';
display: block;
height: 100%;
position: absolute;
transition: all .2s ease-out;
width: 100%;
}
#hamb-line::before{
top: 5px;
}
#hamb-line::after{
top: -5px;
}
#side-menu {
display: none;
} /* Hide checkbox */
/* Toggle menu icon */
#side-menu:checked ~ nav {
display: block;
max-height: 100%;
padding-top: 5.625rem;
}
#side-menu:checked ~ #logo-container {
position: fixed;
}
#side-menu:checked ~ #hamb {
position: fixed;
}
#side-menu:checked ~ #logo-container {
position: fixed;
}
#side-menu:checked ~ #hamb #hamb-line {
background: transparent;
}
#side-menu:checked ~ #hamb #hamb-line::before {
transform: rotate(-45deg);
top: 0;
}
#side-menu:checked ~ #hamb #hamb-line::after {
transform: rotate(45deg);
top: 0;
}
/* Options */
.options-dropdown {
position: absolute;
display: none;
top: 5.6rem;
right: 1.8rem;
border: #404040 solid 0.1rem;
background-color: white;
z-index: 10;
}
.options-dropdown button, .options-dropdown a {
display: block;
font-family: var(--sans-serif,sans-serif);
font-size: 1.2rem;
width: 100%;
padding: 1rem;
text-align: left;
}
.options-dropdown button:hover, .options-dropdown a:hover {
background-color: rgb(187 53 52 / 96%);
color: white;
}
.options-button {
width: 100%;
text-align: left;
}
/* Localization */
#locales {
position: relative;
}
#locales button {
width: 100%;
text-align: left;
height: 4rem;
}
#locales button:hover {
opacity: 0.5;
}
#locales-img {
position: relative;
top: 0rem;
height: 2rem;
margin-left: 1.2rem;
}
/*
#options-dropdown>:first-child {
padding-bottom: 0.5rem;
}
#options-dropdown>:nth-child(2) {
padding-top: 0.5rem;
}
*/
/* Responsiveness */
@media only screen and (min-width: 1200px) {
#navbar {
position: relative;
width: min(100%,116rem);
left: 50%;
transform: translateX(-50%);
padding-right: 4rem;
padding-left: 4rem;
}
#nav {
max-height: none;
top: 0;
position: relative;
float: right;
width: fit-content;
background-color: transparent;
overflow: visible;
}
#side-menu:checked ~ nav {
padding-top: 0;
}
#menu li {
float: left;
}
#menu > li > a:hover, .options-button:hover, #navbar-logo-text:hover {
color: rgb(127, 127, 127);
}
#menu > li > a, .options-button {
padding: 0.9rem;
padding-top: 1.9rem;
padding-bottom: 1.9rem;
}
#hamb {
display: none;
}
#locales {
position: relative;
margin-right: 1.8rem;
}
#locales-img {
top: 0.9rem;
}
}
@import '/css/navbar.css';
</style>

View File

@@ -0,0 +1,29 @@
<svelte:options tag="profile-communes" />
<script>
// Import statements
import { onMount } from 'svelte'
import * as AuthTools from "/js/libraries/authTools.js"
// Main code
onMount(() => {
})
</script>
<h3>Under development</h3>
<p style=" position: relative; margin-top: 2rem;">Visit <a href="https://discord.gg/Qk8KUk787z" style="color: #c52a28;">https://discord.gg/Qk8KUk787z</a> and ask for your commune to be added.</p>
<style>
@import '/css/common.css';
h3 {
text-align: center;
}
</style>

View File

@@ -0,0 +1,284 @@
<svelte:options tag="profile-component" />
<script>
// Import libraries
import { onMount, afterUpdate, setContext } from 'svelte'
import { writable } from 'svelte/store'
import * as AuthTools from "/js/libraries/authTools.js"
import {svgFromObject} from "/js/libraries/miscTools.js"
//Import components
import "/js/components/pane-aligner.js"
import "/js/components/profile-general.js"
import "/js/components/profile-groups.js"
import "/js/components/profile-communes.js"
import "/js/components/profile-coops.js"
import "/js/components/profile-parties.js"
import "/js/components/groups-add-component.js"
// Main code
AuthTools.redirectNotLogged()
let root
let general
let groups
let communes
let coops
let parties
let panes
let groupsAdd
let generalButton
let groupsButton
let communesButton
let coopsButton
let partiesButton
let buttons
let currentPaneIndex = 0
let locationPopup
let maps = {}
let user = {}
let loaded = writable(0)
let reloadTriggerVal = writable(0)
AuthTools.getUser(user,loaded)
function changePane(pane,button) {
for (let p of panes) {
p.style.display = "none"
}
for (let b of buttons) {
styleField(b,400,"#636363")
}
pane.style.display = "initial"
styleField(button,500,"#c52a28")
}
function styleField(div,weight,color) {
let svgObject = div.querySelector("object")
if (svgObject==null) {
let f = () => styleField(div,weight,color)
setTimeout(f,100)
}
else {
let svgItem = svgFromObject(svgObject)
if (svgItem==null) {
let f = () => styleField(div,weight,color)
setTimeout(f,100)
}
else {
div.style.fontWeight = weight
svgItem.setAttribute("fill", color)
}
}
}
function fillFields() {
if (Object.keys(user).length!=0 && root!=undefined) {
for (let b of buttons) {
styleField(b,400,"#636363")
}
styleField(buttons[currentPaneIndex],500,"#c52a28")
}
else {
setTimeout(fillFields, 100)
}
}
function valid(el) {
return (el!=undefined) && (el!=null)
}
function init() {
panes = [general,groups,communes,coops,parties]
buttons = [generalButton,groupsButton,communesButton,coopsButton,partiesButton]
if ($loaded==1 && panes.every(x => valid(x)) && buttons.every(x => valid(x))) {
panes = [general,groups,communes,coops,parties]
buttons = [generalButton,groupsButton,communesButton,coopsButton,partiesButton]
fillFields()
general.style.display = "initial"
}
else {
let f = () => init()
setTimeout(f,100)
}
}
function reloadTrigger() {
reloadTriggerVal.update((val) => {
return val + 1
})
}
setContext("profile-component",{user,maps,reloadTrigger})
onMount(() => {
init()
})
</script>
<!--
<div bind:this={locationPopup} class="overlay" style="display: none">
<div id="location-overlay-content">
</div>
<button class="overlay-button" on:click={() => locationPopup.style.display = "none"}></button>
</div>
-->
<pane-aligner>
<div id="left-column" class="pane" slot="sidebar-left" bind:this={root}>
<button bind:this={generalButton} on:click={() => changePane(general,generalButton)}>
<object id="general-img" class="icons" type="image/svg+xml" data="/img/profile/icons/general.svg" title="general"></object>
<span>general</span>
</button>
<button bind:this={groupsButton} on:click={() => changePane(groups,groupsButton)}>
<object id="groups-img" class="icons" type="image/svg+xml" data="/img/common/groups.svg" title="groups"></object>
<span>groups</span>
</button>
<button bind:this={communesButton} on:click={() => changePane(communes,communesButton)}>
<object id="communes-img" class="icons" type="image/svg+xml" data="/img/common/communes.svg" title="communes"></object>
<span>communes</span>
</button>
<button bind:this={coopsButton} on:click={() => changePane(coops,coopsButton)}>
<object id="coops-img" class="icons" type="image/svg+xml" data="/img/common/coops.svg" title="coops"></object>
<span>cooperatives</span>
</button>
<button bind:this={partiesButton} on:click={() => changePane(parties,partiesButton)}>
<object id="parties-img" class="icons" type="image/svg+xml" data="/img/common/parties.svg" title="parties"></object>
<span>parties</span>
</button>
<button on:click={AuthTools.logout} id="logout-button">
<object id="logout-img" class="icons" type="image/svg+xml" data="/img/profile/icons/logout.svg" title=""></object>
<span>logout</span>
</button>
</div>
<div id="main-column" slot="main">
{#key $loaded}
{#if $loaded==1}
<profile-general bind:this={general} style="display: none;"></profile-general>
<profile-groups bind:this={groups} style="display: none;"></profile-groups>
<profile-communes bind:this={communes} style="display: none;"></profile-communes>
<profile-coops bind:this={coops} style="display: none;"></profile-coops>
<profile-parties bind:this={parties} style="display: none;"></profile-parties>
{/if}
{/key}
</div>
</pane-aligner>
<style>
@import '/css/common.css';
#general-img {
top: 0rem;
}
#groups-img {
top: 0.3rem;
}
#coops-img {
top: 0rem;
}
#parties-img {
top: 0rem;
}
#logout-img {
width: 1.5rem;
}
#logout-button {
padding-top: 1rem;
padding-left: 0.1rem;
}
#left-column {
position: relative;
display: flex;
flex-direction: column;
width: 15.2rem;
padding: 2rem;
border-radius: 0.64rem 0.64rem 0.64rem 0.64rem;
gap: 1rem;
}
.icons {
position: relative;
width: 1.8rem;
}
#left-column button span {
position: absolute;
padding-left: 3.4rem;
margin-top: 0rem;
font-family: var(--sans-serif,sans-serif);
}
#left-column button {
display: flex;
flex-direction: row;
}
#main-column {
padding: 1rem 2rem 1rem 2rem;
height: 100%;
width: 100%;
border-radius: 0 0.64rem 0.64rem 0;
flex-grow: 1;
flex-shrink: 1;
min-height: 20rem;
}
pane-aligner {
--width-main: 800px;
--width-left: 10.5rem;
}
@media only screen and (max-width: 1340px) {
#left-column {
position: relative;
margin-left: 0rem;
width: 100%;
border-radius: 0.64rem 0.64rem 0rem 0;
}
#main-column {
border-radius: 0.64rem;
padding: 3rem 0.5rem;
padding-bottom: 1.5rem;
border-radius: 0rem 0rem 0.64rem 0.64rem;
width: 100%;
}
#logout-button {
position: relative;
bottom: 0;
}
#left-column button {
margin-left: auto;
margin-right: auto;
width: 10rem;
}
#logout-button {
padding-top: 1rem;
margin-bottom: 0rem;
}
}
</style>

View File

@@ -0,0 +1,29 @@
<svelte:options tag="profile-coops" />
<script>
// Import statements
import { onMount } from 'svelte'
import * as AuthTools from "/js/libraries/authTools.js"
// Main code
onMount(() => {
})
</script>
<h3>Under development</h3>
<p style=" position: relative; margin-top: 2rem;">Visit <a href="https://discord.gg/Qk8KUk787z" style="color: #c52a28;">https://discord.gg/Qk8KUk787z</a> and ask for your cooperative to be added.</p>
<style>
@import '/css/common.css';
h3 {
text-align: center;
}
</style>

View File

@@ -0,0 +1,433 @@
<svelte:options tag="profile-general" />
<script>
// Import statements
import { onMount, getContext } from 'svelte'
import * as AuthTools from "/js/libraries/authTools.js"
//Import components
import "/js/components/select-component.js"
import "/js/components/switch-component.js"
//Export statements
// Main code
let emailInput
let section
let saveEmailButton
let changePasswordInputDiv
let changePasswordMsg
let savePasswordButton
let passwordInput
let changePasswordDiv
let passwordVisibilityButton
let emailMsg
let passwordDiv
let emailDiv
let emailInputDiv
let prevEmail
let context = getContext("profile-component")
let user = context.user
function showSaveButton(button) {
prevEmail = emailInput.value
button.style.display = "initial"
emailMsg.style.display = "inline"
let windowWidth = window.innerWidth
if (windowWidth<1100) {
emailInputDiv.style.marginTop = "1rem"
emailDiv.style.flexDirection = "column"
}
else {
//emailInput.style.width = "19rem"
}
}
function saveEmail() {
let email = emailInput.value
if (AuthTools.checkEmail(email,emailMsg)) {
if (email!=user.email) {
AuthTools.changeUser("email",email,user)
}
resetEmailField()
}
}
function resetEmailField() {
if (prevEmail!=undefined) {
emailInput.value = prevEmail
}
emailInput.style.width = "100%"
emailMsg.style.display = "none"
emailDiv.style.flexDirection = "row"
emailInputDiv.style.marginTop = "0rem"
saveEmailButton.style.display = "none"
emailMsg.innerHTML = ""
}
function launchChangePassword() {
let windowWidth = window.innerWidth
if (windowWidth<1100) {
changePasswordInputDiv.style.display = "flex";
}
else {
changePasswordInputDiv.style.display = "initial";
}
changePasswordDiv.style.display = "none";
passwordInput.focus()
}
function savePassword() {
let password = passwordInput.value
if (AuthTools.checkPassword(password,changePasswordMsg)) {
if (password!=user.password) {
AuthTools.changeUser("password",password,user)
}
changePasswordMsg.innerHTML = ""
resetPasswordField()
}
}
function resetPasswordField() {
changePasswordInputDiv.style.display = "none";
changePasswordDiv.style.display = "initial";
changePasswordMsg.innerHTML = ""
}
function fillFields() {
if (user!=null && Object.keys(user).length!=0 && section!=undefined) {
emailInput.value = user.email
}
else {
setTimeout(fillFields, 10)
}
}
function resizeInput(el) {
el.nextElementSibling.innerHTML = el.value
}
onMount(() => {
fillFields()
document.addEventListener("click", function(event) {
if (passwordDiv.focused) {
resetEmailField()
}
else if (emailDiv.focused) {
resetPasswordField()
}
else {
resetEmailField()
resetPasswordField()
}
})
})
</script>
<section bind:this={section} id="general-section">
<h2 class="title-highlight">General</h2>
<div bind:this={emailDiv} on:mouseenter={emailDiv.focused=true} on:mouseleave={emailDiv.focused=false}>
<div class="title-msg">
<span>Email:</span>
<span bind:this={emailMsg} id="signup-email-msg"></span>
</div>
<div bind:this={emailInputDiv} id="emailInputDiv">
<button bind:this={saveEmailButton} id="save-email" class="save-button" on:click={saveEmail}>save</button>
<div class="input-wrapper">
<input bind:this={emailInput} id="emailInput" class="text-input" type="text" on:click={() => showSaveButton(saveEmailButton)} on:input={() => resizeInput(emailInput)}>
<div class="ghost-input"></div>
</div>
</div>
</div>
<div bind:this={passwordDiv} on:mouseenter={passwordDiv.focused=true} on:mouseleave={passwordDiv.focused=false} id="change-password-line-wrapper">
<div id="change-password-line">
<div class="title-msg">
<span>Password:</span>
<span bind:this={changePasswordMsg} id="signup-password-msg"></span>
</div>
<div bind:this={changePasswordDiv} id="change-password-div">
<button id="change-password" on:click={launchChangePassword}>change
<object type="image/svg+xml" data="/img/profile/icons/pencil.svg" title="pencil-icon"></object>
</button>
</div>
</div>
<div bind:this={changePasswordInputDiv} id="change-password-input-div">
<button bind:this={savePasswordButton} id="save-password" class="save-button" on:click={savePassword}>save</button>
<input bind:this={passwordInput} id="passwordInput" class="text-input" type="password">
<button bind:this={passwordVisibilityButton} class="eye-icon" on:click="{() => AuthTools.changePasswordVisibility(passwordVisibilityButton)}">
<object type="image/svg+xml" data="/img/auth/eye_icon.svg" title="eye icon"></object>
</button>
</div>
</div>
<div>
<div id="verifiedDiv">
<span>Verified:</span>
<span style="color: {user.verified ? "green" : "red"}">{user.verified}</span>
</div>
</div>
</section>
<style>
@import '/css/common.css';
#verifiedDiv {
display: inline;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
height: 2rem;
width: 100%;
}
/*---General section-----------------------------------------------------------*/
.ghost-input {
display: block;
visibility: hidden;
height: 0;
padding-left: 0.5rem;
padding-right: 0.5rem;
}
.input-wrapper {
display: inline-block;
max-width: calc(100% - 10rem);
min-width: 0rem;
height: 2.5rem;
position: relative;
right: 0
}
span {
font-family: var(--sans-serif,sans-serif);
font-size: 1.15rem;
}
#general-section {
display: flex;
flex-direction: column;
}
#general-section h2 {
margin: auto;
margin-top: 0;
margin-bottom: 0;
}
#general-section >div {
height: 3.5rem;
padding-bottom: 0.75rem;
padding-top: 0.75rem;
border-bottom: 0.14rem solid;
border-color: #cdcdcd;
}
#general-section >div >:first-child {
font-family: var(--sans-serif,sans-serif);
}
/* add padding to every line to center the diving line*/
#general-section >div:last-child {
padding-bottom: 0.75rem;
padding-top: 0.75rem;
border-bottom: 0;
}
#general-section >div div,
#general-section >div input,
#general-section >div :not(:first-child) input {
font-weight: 500;
font-size: 1.15rem;
font-family: var(--sans-serif,sans-serif);
color: #292222;
border: 0;
}
#general-section >div>:last-child {
padding-right: 1.35rem;
}
.text-input {
position: relative;
width: 20.475rem;
direction: rtl;
border: 0;
outline: none;
bottom: 0.341rem;
}
/*---Email-------------------------------------------------------------------*/
#emailInput {
position: relative;
right: 0;
top: 0.1rem;
width: 100%;
}
#save-email {
display: none;
margin-top: 0.5rem;
}
#signup-email-msg,
#signup-password-msg {
position: relative;
display: inline-block;
color:red;
font-weight: 400;
white-space: nowrap;
}
#signup-email-msg {
display: none;
}
#general-section >div:nth-child(2) {
display: flex;
flex-direction: row;
}
#emailInputDiv {
display: flex;
flex-direction: row;
justify-content: right;
align-items: center;
height: 2rem;
width: 100%;
}
.title-msg {
display: flex;
gap: 0.5rem;
}
.title-msg * {
text-align: left;
}
/*---Change password-------------------------------------------------------------------*/
#change-password-line {
display: flex;
justify-content: space-between;
}
#change-password-div {
width: 9.3rem;
left: 0;
}
#change-password {
position: absolute;
cursor: pointer;
width: 8rem;
height: 2.73rem;
font-size: 1.15rem;
font-family: var(--sans-serif,sans-serif);
font-weight: 500;
text-align: right;
padding-right: 2rem;
margin-top: -0.55rem;
background-color: transparent;
}
#change-password > object {
pointer-events: none;
position: absolute;
width: 1.5rem;
right: 0.0rem;
}
#change-password-input-div {
display: none;
float: right;
position: relative;
margin-top: -1.7rem;
}
#passwordInput {
width: 15rem;
right: 0.65rem;
margin-left: 1.5rem;
}
.save-button {
position: relative;
bottom: 0.34rem;
margin-right: 0.6rem;
height: 2.73rem;
width: 4.778rem;
font-family: var(--sans-serif,sans-serif);
font-size: 1.15rem;
color: white;
background-color: var(--red);
border-color: var(--red);
border-radius: 0.512rem;
}
#save-password {
bottom: 0.6rem
}
.eye-icon {
display: inline-block;
position: relative;
cursor: pointer;
opacity: 0.25;
height: 2.2rem;
width: 1.7rem;
}
.eye-icon * {
pointer-events: none;
}
@media only screen and (max-width: 1100px) {
#change-password-line-wrapper {
display: flex;
flex-direction: column;
height: auto;
min-height: 4rem;
}
#change-password-input-div {
margin-top: 1rem;
justify-content: flex-end;
}
#general-section >div:nth-child(2) {
height: auto;
min-height: 4rem;
position: relative;
}
#passwordInput {
width: 100%;
bottom: 0;
}
#emailInput {
width: 100%;
}
#save-password {
bottom: 0rem
}
}
</style>

View File

@@ -0,0 +1,455 @@
<svelte:options tag="profile-groups" />
<script>
// Import statements
import { onMount, getContext } from 'svelte'
import { writable } from 'svelte/store'
import { getData, sendData } from "/js/libraries/serverTools.js"
//Import components
import "/js/components/select-component.js"
import "/js/components/switch-component.js"
//Export statements
// Main code
let section
let userGroups = []
let groupsRequests = []
let content = writable({})
let loaded = writable(0)
let keyRequests = 0
let numLoaded = 2
let mainPane
let groupsAdd
let membersInput
let saveMembersButton
let membersInputDiv
let contactInput
let saveContactButton
let contactInputDiv
let locale = "en"
let inputLocation
let inputContact
let inputMembers
let pencilMembers
let pencilContact
let pencilButtonMembers
let pencilButtonContact
let myGroupLocation
let myGroupStatus
let context = getContext("profile-component")
let maps = context.maps
function groups_callback(response) {
userGroups = JSON.parse(response)
context["userGroups"] = userGroups
loaded.update((val) => {
return val + 1
})
}
getData("/xx/get-user-groups",groups_callback)
function requests_callback(response) {
let parsed = JSON.parse(response)
groupsRequests.push(...parsed)
loaded.update((val) => {
return val + 1
})
}
getData("/xx/get-group-requests",requests_callback)
function getAddress(g) {
if (g!=undefined) {
let location = [g.country,g.state,g.town].filter(x => x!=null)
return location.map(x => locale=="en" ? x : translate($content,x)).join(", ")
}
else {
return "Create or join group"
}
}
function getContact(c) {
if (c==null) {
return "https://discord.gg/Qk8KUk787z"
}
else {
return c
}
}
function launchChangeLocation() {
showLocationOverlay()
}
function launchChangeMembers() {
}
function showSaveButton(button,input) {
if (!input.readOnly) {
button.style.display = "initial"
}
}
function resetMembersField() {
saveMembersButton.style.display = "none"
}
function resetContactField() {
saveContactButton.style.display = "none"
}
function saveMembers() {
let email = emailInput.value
if (AuthTools.checkEmail(email,emailMsg)) {
if (email!=user.email) {
AuthTools.changeUser("email",email,user)
}
resetMembersField()
}
}
function saveContact() {}
function updateUserGroup(newInfo) {
if (newInfo!=undefined) {
myGroupLocation.innerHTML = getAddress(newInfo)
}
}
function onLoadedGroups() {
let els = [saveMembersButton,saveContactButton,membersInputDiv,contactInputDiv]
if ($loaded==numLoaded && els.every(x => x!=undefined && x!=null)) {
document.addEventListener("click", function(event) {
let activeEl
let shadowRoot = this.activeElement.shadowRoot
if (shadowRoot!=null) {
activeEl = shadowRoot.activeElement
shadowRoot = activeEl.shadowRoot
if (shadowRoot!=null) {
activeEl = shadowRoot.activeElement
}
}
if (activeEl == membersInput || activeEl == saveMembersButton) {
resetContactField()
}
else if (activeEl == contactInput || activeEl == saveContactButton) {
resetMembersField()
}
else {
resetMembersField()
resetContactField()
}
})
context["updateUserGroup"] = updateUserGroup
inputLocation = getAddress(userGroups[0])
if (userGroups.length==0) {
inputContact = ""
inputMembers = ""
}
else {
let group = userGroups[0]
inputContact = getContact(group.contact)
inputMembers = group.members
let status = group.status
if (status!=undefined) {
if (status==0) {
myGroupStatus.innerHTML = "(pending)"
myGroupStatus.style.color = "#FFC90E"
}
else if (status==2) {
myGroupStatus.innerHTML = "(rejected)"
myGroupStatus.style.color = "#c52a28"
}
pencilMembers.style.display = "none"
pencilContact.style.display = "none"
pencilButtonContact.style.cursor = "default"
pencilButtonMembers.style.cursor = "default"
membersInput.readOnly = true
contactInput.readOnly = true
}
}
}
else {
let f = () => onLoadedGroups()
setTimeout(f, 100)
}
}
function focus(el) {
el.focus()
el.click()
}
function approveRequest(ind,user_id) {
sendData("/xx/group-approve-request",{user_id: user_id})
groupsRequests.splice(ind,1)
keyRequests = keyRequests + 1
}
function rejectRequest(ind,user_id) {
sendData("/xx/group-reject-request",{user_id: user_id})
groupsRequests.splice(ind,1)
keyRequests = keyRequests + 1
}
function launchGroupsAdd() {
groupsAdd.style.display = "block"
mainPane.style.display = "none"
if (maps["groupsAdd"]!=undefined) {
maps["groupsAdd"].invalidateSize()
}
}
function closeGroupsAdd() {
groupsAdd.style.display = "none"
mainPane.style.display = "block"
}
context["onLoadedGroups"] = onLoadedGroups
context["launchGroupsAdd"] = launchGroupsAdd
context["closeGroupsAdd"] = closeGroupsAdd
onMount(() => {
onLoadedGroups()
})
</script>
{#key $loaded}
{#if $loaded==numLoaded}
<div bind:this={mainPane}>
<h2>Groups</h2>
<div>
<h3 class="group-heading">My group</h3>
<span bind:this={myGroupStatus} class="status"></span>
</div>
<section bind:this={section} class="entries-section">
<div>
<div class="change-field-line">
<span>Location:</span>
<div class="change-field-div">
<button class="change-field-button" bind:this={myGroupLocation} on:click={launchGroupsAdd}>{inputLocation}
<object type="image/svg+xml" data="/img/profile/icons/pencil.svg" title="pencil-icon" class="pencil"></object>
</button>
</div>
</div>
</div>
<div>
<div class="change-field-line">
<span>Members:</span>
<div bind:this={membersInputDiv} class="change-field-div input-pencil">
<div class="save-button-wrapper">
<button bind:this={saveMembersButton} on:click={saveMembers} class="save-button" style="display: none">save</button>
</div>
<input bind:this={membersInput} id="membersInput" class="text-input" type="text" bind:value={inputMembers} on:click={() => showSaveButton(saveMembersButton,membersInput)}>
<button bind:this={pencilButtonMembers} class="text-input-pencil-button" on:click={() => {focus(membersInput)}}>
<object bind:this={pencilMembers} type="image/svg+xml" data="/img/profile/icons/pencil.svg" title="pencil-icon" class="pencil"></object>
</button>
</div>
</div>
</div>
<div>
<div class="change-field-line">
<span>Contact:</span>
<div bind:this={contactInputDiv} class="change-field-div input-pencil">
<div class="save-button-wrapper">
<button bind:this={saveContactButton} on:click={saveContact} class="save-button" style="display: none">save</button>
</div>
<input bind:this={contactInput} id="contactInput" class="text-input" type="text" bind:value={inputContact} on:click={() => showSaveButton(saveContactButton,contactInput)}>
<button bind:this={pencilButtonContact} class="text-input-pencil-button" on:click={focus(contactInput)}>
<object bind:this={pencilContact} type="image/svg+xml" data="/img/profile/icons/pencil.svg" title="pencil-icon" class="pencil"></object>
</button>
</div>
</div>
</div>
</section>
<h3>Requests</h3>
<section bind:this={section} class="entries-section">
{#key keyRequests}
{#each groupsRequests as req,ind}
<div>
<div class="change-field-line">
<span>{req.email}</span>
<div class="request-button-wrapper">
<button on:click={() => approveRequest(ind,req.user_id)} class="approve-button">approve</button>
<button on:click={() => rejectRequest(ind,req.user_id)} class="approve-button" style="display:visible">reject</button>
</div>
</div>
</div>
{/each}
{/key}
</section>
</div>
<!--Helper panes-->
<groups-add-component bind:this={groupsAdd} style="display: none;"></groups-add-component>
{/if}
{/key}
<style>
@import '/css/common.css';
.request-button-wrapper {
display: flex;
gap: 1rem;
}
.approve-button {
height: 2.7rem;
padding: 0rem 1rem;
font-family: var(--sans-serif,sans-serif);
font-size: 1.15rem;
color: white;
background-color: var(--red);
border-color: var(--red);
border-radius: 0.5rem;
margin-top: -0.5rem;
}
.group-heading {
display: inline-block;
}
.status {
display: inline-block;
font-size: 1.15rem;
font-family: var(--sans-serif,sans-serif);
margin-left: 0.5rem;
}
input {
font-family: var(--sans-serif,sans-serif)
}
.text-input-pencil-button {
display: inline-block;
position: relative;
height: 2.7rem;
width: 2rem;
}
.text-input-pencil-button object {
top: 0rem;
}
.pencil {
pointer-events: none;
position: absolute;
width: 1.5rem;
right: 0.0rem;
}
.change-field-div input.text-input {
position: relative;
width: 20.475rem;
direction: rtl;
border: 0;
outline: none;
height: 2.7rem;
font-style: var(--sans-serif,sans-serif);
background: transparent;
margin-top: -0.5rem;
}
#membersInput {
width: 5rem;
}
#contactInput {
max-width: 18rem;
}
.save-button {
position: absolute;
right: 0;
top: -0.4rem;
margin-right: 0.6rem;
height: 2.7rem;
width: 5rem;
font-family: var(--sans-serif,sans-serif);
font-size: 1.15rem;
color: white;
background-color: var(--red);
border-color: var(--red);
border-radius: 0.5rem;
}
.save-button-wrapper {
display: inline-block;
position: relative;
height: 2rem;
}
h2 {
text-align: center;
margin-bottom: 0.5rem;
}
.entries-section {
margin-bottom: 1rem;
}
.entries-section >div {
height: 3.5rem;
padding-bottom: 0.75rem;
padding-top: 0.75rem;
border-bottom: 0.14rem solid;
border-color: #cdcdcd;
}
/* add padding to every line to center the diving line*/
.entries-section >div:last-child {
padding-bottom: 0.75rem;
padding-top: 0.75rem;
border-bottom: 0;
}
/*---Change field-------------------------------------------------------------------*/
.change-field-line {
display: flex;
justify-content: space-between;
}
.change-field-div {
width: max-content;
position: relative;
display: flex;
}
.change-field-button {
position: relative;
cursor: pointer;
height: 2.7rem;
font-size: 1.15rem;
font-family: var(--sans-serif,sans-serif);
font-weight: 500;
text-align: right;
padding-right: 1.9rem;
margin-top: -0.55rem;
background-color: transparent;
width: 100%;
}
/*---General section-----------------------------------------------------------*/
h3 {
margin-bottom: 0.5rem;
}
span {
font-family: var(--sans-serif,sans-serif);
font-size: 1.15rem;
}
</style>

View File

@@ -0,0 +1,29 @@
<svelte:options tag="profile-parties" />
<script>
// Import statements
import { onMount } from 'svelte'
import * as AuthTools from "/js/libraries/authTools.js"
// Main code
onMount(() => {
})
</script>
<h3>Under development</h3>
<p style=" position: relative; margin-top: 2rem;">Visit <a href="https://discord.gg/Qk8KUk787z" style="color: #c52a28;">https://discord.gg/Qk8KUk787z</a> and ask for your party to be added.</p>
<style>
@import '/css/common.css';
h3 {
text-align: center;
}
</style>