Version 3 #40
|
@ -58,16 +58,16 @@ module private CacheHelpers =
|
||||||
|
|
||||||
open DataConnection
|
open DataConnection
|
||||||
|
|
||||||
/// A distributed cache implementation in PostgreSQL used to handle sessions for myWebLog
|
/// A distributed cache implementation in PostgreSQL used to handle sessions for Jobs, Jobs, Jobs
|
||||||
type DistributedCache () =
|
type DistributedCache () =
|
||||||
|
|
||||||
// ~~~ INITIALIZATION ~~~
|
// ~~~ INITIALIZATION ~~~
|
||||||
|
|
||||||
do
|
do
|
||||||
task {
|
task {
|
||||||
let conn = connection ()
|
let dataSource = dataSource ()
|
||||||
let! exists =
|
let! exists =
|
||||||
conn
|
dataSource
|
||||||
|> Sql.query $"
|
|> Sql.query $"
|
||||||
SELECT EXISTS
|
SELECT EXISTS
|
||||||
(SELECT 1 FROM pg_tables WHERE schemaname = 'public' AND tablename = 'session')
|
(SELECT 1 FROM pg_tables WHERE schemaname = 'public' AND tablename = 'session')
|
||||||
|
@ -75,7 +75,7 @@ type DistributedCache () =
|
||||||
|> Sql.executeRowAsync (fun row -> row.bool "does_exist")
|
|> Sql.executeRowAsync (fun row -> row.bool "does_exist")
|
||||||
if not exists then
|
if not exists then
|
||||||
let! _ =
|
let! _ =
|
||||||
conn
|
dataSource
|
||||||
|> Sql.query
|
|> Sql.query
|
||||||
"CREATE TABLE session (
|
"CREATE TABLE session (
|
||||||
id TEXT NOT NULL PRIMARY KEY,
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
@ -92,10 +92,10 @@ type DistributedCache () =
|
||||||
|
|
||||||
/// Get an entry, updating it for sliding expiration
|
/// Get an entry, updating it for sliding expiration
|
||||||
let getEntry key = backgroundTask {
|
let getEntry key = backgroundTask {
|
||||||
let conn = connection ()
|
let dataSource = dataSource ()
|
||||||
let idParam = "@id", Sql.string key
|
let idParam = "@id", Sql.string key
|
||||||
let! tryEntry =
|
let! tryEntry =
|
||||||
conn
|
dataSource
|
||||||
|> Sql.query "SELECT * FROM session WHERE id = @id"
|
|> Sql.query "SELECT * FROM session WHERE id = @id"
|
||||||
|> Sql.parameters [ idParam ]
|
|> Sql.parameters [ idParam ]
|
||||||
|> Sql.executeAsync (fun row ->
|
|> Sql.executeAsync (fun row ->
|
||||||
|
@ -118,7 +118,7 @@ type DistributedCache () =
|
||||||
else true, { entry with ExpireAt = now.Plus slideExp }
|
else true, { entry with ExpireAt = now.Plus slideExp }
|
||||||
if needsRefresh then
|
if needsRefresh then
|
||||||
let! _ =
|
let! _ =
|
||||||
conn
|
dataSource
|
||||||
|> Sql.query "UPDATE session SET expire_at = @expireAt WHERE id = @id"
|
|> Sql.query "UPDATE session SET expire_at = @expireAt WHERE id = @id"
|
||||||
|> Sql.parameters [ expireParam item.ExpireAt; idParam ]
|
|> Sql.parameters [ expireParam item.ExpireAt; idParam ]
|
||||||
|> Sql.executeNonQueryAsync
|
|> Sql.executeNonQueryAsync
|
||||||
|
@ -135,7 +135,7 @@ type DistributedCache () =
|
||||||
let now = getNow ()
|
let now = getNow ()
|
||||||
if lastPurge.Plus (Duration.FromMinutes 30L) < now then
|
if lastPurge.Plus (Duration.FromMinutes 30L) < now then
|
||||||
let! _ =
|
let! _ =
|
||||||
connection ()
|
dataSource ()
|
||||||
|> Sql.query "DELETE FROM session WHERE expire_at < @expireAt"
|
|> Sql.query "DELETE FROM session WHERE expire_at < @expireAt"
|
||||||
|> Sql.parameters [ expireParam now ]
|
|> Sql.parameters [ expireParam now ]
|
||||||
|> Sql.executeNonQueryAsync
|
|> Sql.executeNonQueryAsync
|
||||||
|
@ -145,7 +145,7 @@ type DistributedCache () =
|
||||||
/// Remove a cache entry
|
/// Remove a cache entry
|
||||||
let removeEntry key = backgroundTask {
|
let removeEntry key = backgroundTask {
|
||||||
let! _ =
|
let! _ =
|
||||||
connection ()
|
dataSource ()
|
||||||
|> Sql.query "DELETE FROM session WHERE id = @id"
|
|> Sql.query "DELETE FROM session WHERE id = @id"
|
||||||
|> Sql.parameters [ "@id", Sql.string key ]
|
|> Sql.parameters [ "@id", Sql.string key ]
|
||||||
|> Sql.executeNonQueryAsync
|
|> Sql.executeNonQueryAsync
|
||||||
|
@ -170,7 +170,7 @@ type DistributedCache () =
|
||||||
let slide = Duration.FromHours 1
|
let slide = Duration.FromHours 1
|
||||||
now.Plus slide, Some slide, None
|
now.Plus slide, Some slide, None
|
||||||
let! _ =
|
let! _ =
|
||||||
connection ()
|
dataSource ()
|
||||||
|> Sql.query
|
|> Sql.query
|
||||||
"INSERT INTO session (
|
"INSERT INTO session (
|
||||||
id, payload, expire_at, sliding_expiration, absolute_expiration
|
id, payload, expire_at, sliding_expiration, absolute_expiration
|
||||||
|
|
|
@ -37,12 +37,12 @@ module DataConnection =
|
||||||
open Npgsql
|
open Npgsql
|
||||||
|
|
||||||
/// The data source for the document store
|
/// The data source for the document store
|
||||||
let mutable private dataSource : NpgsqlDataSource option = None
|
let mutable private theDataSource : NpgsqlDataSource option = None
|
||||||
|
|
||||||
/// Get a connection
|
/// Get the data source as the start of a SQL statement
|
||||||
let connection () =
|
let dataSource () =
|
||||||
match dataSource with
|
match theDataSource with
|
||||||
| Some ds -> ds.OpenConnection () |> Sql.existingConnection
|
| Some ds -> Sql.fromDataSource ds
|
||||||
| None -> invalidOp "Connection.setUp() must be called before accessing the database"
|
| None -> invalidOp "Connection.setUp() must be called before accessing the database"
|
||||||
|
|
||||||
/// Create tables
|
/// Create tables
|
||||||
|
@ -66,7 +66,7 @@ module DataConnection =
|
||||||
$"CREATE INDEX IF NOT EXISTS idx_success_citizen ON {Table.Success} ((data -> 'citizenId'))"
|
$"CREATE INDEX IF NOT EXISTS idx_success_citizen ON {Table.Success} ((data -> 'citizenId'))"
|
||||||
]
|
]
|
||||||
let! _ =
|
let! _ =
|
||||||
connection ()
|
dataSource ()
|
||||||
|> Sql.executeTransactionAsync (sql |> List.map (fun sql -> sql, [ [] ]))
|
|> Sql.executeTransactionAsync (sql |> List.map (fun sql -> sql, [ [] ]))
|
||||||
()
|
()
|
||||||
}
|
}
|
||||||
|
@ -75,7 +75,7 @@ module DataConnection =
|
||||||
let setUp (cfg : IConfiguration) = backgroundTask {
|
let setUp (cfg : IConfiguration) = backgroundTask {
|
||||||
let builder = NpgsqlDataSourceBuilder (cfg.GetConnectionString "PostgreSQL")
|
let builder = NpgsqlDataSourceBuilder (cfg.GetConnectionString "PostgreSQL")
|
||||||
let _ = builder.UseNodaTime ()
|
let _ = builder.UseNodaTime ()
|
||||||
dataSource <- Some (builder.Build ())
|
theDataSource <- Some (builder.Build ())
|
||||||
do! createTables ()
|
do! createTables ()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -166,7 +166,7 @@ module Citizens =
|
||||||
|
|
||||||
/// Delete a citizen by their ID
|
/// Delete a citizen by their ID
|
||||||
let deleteById citizenId =
|
let deleteById citizenId =
|
||||||
doDeleteById citizenId (connection ())
|
doDeleteById citizenId (dataSource ())
|
||||||
|
|
||||||
/// Save a citizen
|
/// Save a citizen
|
||||||
let private saveCitizen (citizen : Citizen) connProps =
|
let private saveCitizen (citizen : Citizen) connProps =
|
||||||
|
@ -178,7 +178,7 @@ module Citizens =
|
||||||
|
|
||||||
/// Purge expired tokens
|
/// Purge expired tokens
|
||||||
let private purgeExpiredTokens now = backgroundTask {
|
let private purgeExpiredTokens now = backgroundTask {
|
||||||
let connProps = connection ()
|
let connProps = dataSource ()
|
||||||
let! info =
|
let! info =
|
||||||
Sql.query $"SELECT * FROM {Table.SecurityInfo} WHERE data ->> 'tokenExpires' IS NOT NULL" connProps
|
Sql.query $"SELECT * FROM {Table.SecurityInfo} WHERE data ->> 'tokenExpires' IS NOT NULL" connProps
|
||||||
|> Sql.executeAsync toDocument<SecurityInfo>
|
|> Sql.executeAsync toDocument<SecurityInfo>
|
||||||
|
@ -202,7 +202,7 @@ module Citizens =
|
||||||
|
|
||||||
/// Find a citizen by their ID
|
/// Find a citizen by their ID
|
||||||
let findById citizenId = backgroundTask {
|
let findById citizenId = backgroundTask {
|
||||||
match! connection () |> getDocument<Citizen> Table.Citizen (CitizenId.toString citizenId) with
|
match! dataSource () |> getDocument<Citizen> Table.Citizen (CitizenId.toString citizenId) with
|
||||||
| Some c when not c.IsLegacy -> return Some c
|
| Some c when not c.IsLegacy -> return Some c
|
||||||
| Some _
|
| Some _
|
||||||
| None -> return None
|
| None -> return None
|
||||||
|
@ -210,11 +210,11 @@ module Citizens =
|
||||||
|
|
||||||
/// Save a citizen
|
/// Save a citizen
|
||||||
let save citizen =
|
let save citizen =
|
||||||
saveCitizen citizen (connection ())
|
saveCitizen citizen (dataSource ())
|
||||||
|
|
||||||
/// Register a citizen (saves citizen and security settings); returns false if the e-mail is already taken
|
/// Register a citizen (saves citizen and security settings); returns false if the e-mail is already taken
|
||||||
let register citizen (security : SecurityInfo) = backgroundTask {
|
let register citizen (security : SecurityInfo) = backgroundTask {
|
||||||
let connProps = connection ()
|
let connProps = dataSource ()
|
||||||
use conn = Sql.createConnection connProps
|
use conn = Sql.createConnection connProps
|
||||||
use! txn = conn.BeginTransactionAsync ()
|
use! txn = conn.BeginTransactionAsync ()
|
||||||
try
|
try
|
||||||
|
@ -245,7 +245,7 @@ module Citizens =
|
||||||
/// Confirm a citizen's account
|
/// Confirm a citizen's account
|
||||||
let confirmAccount token = backgroundTask {
|
let confirmAccount token = backgroundTask {
|
||||||
do! checkForPurge true
|
do! checkForPurge true
|
||||||
let connProps = connection ()
|
let connProps = dataSource ()
|
||||||
match! tryConfirmToken token connProps with
|
match! tryConfirmToken token connProps with
|
||||||
| Some info ->
|
| Some info ->
|
||||||
do! saveSecurity { info with AccountLocked = false; Token = None; TokenUsage = None; TokenExpires = None }
|
do! saveSecurity { info with AccountLocked = false; Token = None; TokenUsage = None; TokenExpires = None }
|
||||||
|
@ -257,7 +257,7 @@ module Citizens =
|
||||||
/// Deny a citizen's account (user-initiated; used if someone used their e-mail address without their consent)
|
/// Deny a citizen's account (user-initiated; used if someone used their e-mail address without their consent)
|
||||||
let denyAccount token = backgroundTask {
|
let denyAccount token = backgroundTask {
|
||||||
do! checkForPurge true
|
do! checkForPurge true
|
||||||
let connProps = connection ()
|
let connProps = dataSource ()
|
||||||
match! tryConfirmToken token connProps with
|
match! tryConfirmToken token connProps with
|
||||||
| Some info ->
|
| Some info ->
|
||||||
do! doDeleteById info.Id connProps
|
do! doDeleteById info.Id connProps
|
||||||
|
@ -269,7 +269,7 @@ module Citizens =
|
||||||
let tryLogOn email password (pwVerify : Citizen -> string -> bool option) (pwHash : Citizen -> string -> string)
|
let tryLogOn email password (pwVerify : Citizen -> string -> bool option) (pwHash : Citizen -> string -> string)
|
||||||
now = backgroundTask {
|
now = backgroundTask {
|
||||||
do! checkForPurge false
|
do! checkForPurge false
|
||||||
let connProps = connection ()
|
let connProps = dataSource ()
|
||||||
let! tryCitizen =
|
let! tryCitizen =
|
||||||
connProps
|
connProps
|
||||||
|> Sql.query $"
|
|> Sql.query $"
|
||||||
|
@ -307,6 +307,37 @@ module Citizens =
|
||||||
| None -> return Error "Log on unsuccessful"
|
| None -> return Error "Log on unsuccessful"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Try to retrieve a citizen and their security information by their e-mail address
|
||||||
|
let tryByEmailWithSecurity email = backgroundTask {
|
||||||
|
let toCitizenSecurityPair row = (toDocument<Citizen> row, toDocumentFrom<SecurityInfo> "sec_data" row)
|
||||||
|
let! results =
|
||||||
|
dataSource ()
|
||||||
|
|> Sql.query $"
|
||||||
|
SELECT c.*, s.data AS sec_data
|
||||||
|
FROM {Table.Citizen} c
|
||||||
|
INNER JOIN {Table.SecurityInfo} s ON s.id = c.id
|
||||||
|
WHERE c.data ->> 'email' = @email"
|
||||||
|
|> Sql.parameters [ "@email", Sql.string email ]
|
||||||
|
|> Sql.executeAsync toCitizenSecurityPair
|
||||||
|
return List.tryHead results
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save an updated security information document
|
||||||
|
let saveSecurityInfo security = backgroundTask {
|
||||||
|
do! saveSecurity security (dataSource ())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try to retrieve security information by the given token
|
||||||
|
let trySecurityByToken token = backgroundTask {
|
||||||
|
do! checkForPurge false
|
||||||
|
let! results =
|
||||||
|
dataSource ()
|
||||||
|
|> Sql.query $"SELECT * FROM {Table.SecurityInfo} WHERE data ->> 'token' = @token"
|
||||||
|
|> Sql.parameters [ "@token", Sql.string token ]
|
||||||
|
|> Sql.executeAsync toDocument<SecurityInfo>
|
||||||
|
return List.tryHead results
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Continent data access functions
|
/// Continent data access functions
|
||||||
[<RequireQualifiedAccess>]
|
[<RequireQualifiedAccess>]
|
||||||
|
@ -314,13 +345,13 @@ module Continents =
|
||||||
|
|
||||||
/// Retrieve all continents
|
/// Retrieve all continents
|
||||||
let all () =
|
let all () =
|
||||||
connection ()
|
dataSource ()
|
||||||
|> Sql.query $"SELECT * FROM {Table.Continent} ORDER BY data ->> 'name'"
|
|> Sql.query $"SELECT * FROM {Table.Continent} ORDER BY data ->> 'name'"
|
||||||
|> Sql.executeAsync toDocument<Continent>
|
|> Sql.executeAsync toDocument<Continent>
|
||||||
|
|
||||||
/// Retrieve a continent by its ID
|
/// Retrieve a continent by its ID
|
||||||
let findById continentId =
|
let findById continentId =
|
||||||
connection () |> getDocument<Continent> Table.Continent (ContinentId.toString continentId)
|
dataSource () |> getDocument<Continent> Table.Continent (ContinentId.toString continentId)
|
||||||
|
|
||||||
|
|
||||||
open JobsJobsJobs.Domain.SharedTypes
|
open JobsJobsJobs.Domain.SharedTypes
|
||||||
|
@ -345,14 +376,14 @@ module Listings =
|
||||||
|
|
||||||
/// Find all job listings posted by the given citizen
|
/// Find all job listings posted by the given citizen
|
||||||
let findByCitizen citizenId =
|
let findByCitizen citizenId =
|
||||||
connection ()
|
dataSource ()
|
||||||
|> Sql.query $"{viewSql} WHERE l.data ->> 'citizenId' = @citizenId AND l.data ->> 'isLegacy' = 'false'"
|
|> Sql.query $"{viewSql} WHERE l.data ->> 'citizenId' = @citizenId AND l.data ->> 'isLegacy' = 'false'"
|
||||||
|> Sql.parameters [ "@citizenId", Sql.string (CitizenId.toString citizenId) ]
|
|> Sql.parameters [ "@citizenId", Sql.string (CitizenId.toString citizenId) ]
|
||||||
|> Sql.executeAsync toListingForView
|
|> Sql.executeAsync toListingForView
|
||||||
|
|
||||||
/// Find a listing by its ID
|
/// Find a listing by its ID
|
||||||
let findById listingId = backgroundTask {
|
let findById listingId = backgroundTask {
|
||||||
match! connection () |> getDocument<Listing> Table.Listing (ListingId.toString listingId) with
|
match! dataSource () |> getDocument<Listing> Table.Listing (ListingId.toString listingId) with
|
||||||
| Some listing when not listing.IsLegacy -> return Some listing
|
| Some listing when not listing.IsLegacy -> return Some listing
|
||||||
| Some _
|
| Some _
|
||||||
| None -> return None
|
| None -> return None
|
||||||
|
@ -361,7 +392,7 @@ module Listings =
|
||||||
/// Find a listing by its ID for viewing (includes continent information)
|
/// Find a listing by its ID for viewing (includes continent information)
|
||||||
let findByIdForView listingId = backgroundTask {
|
let findByIdForView listingId = backgroundTask {
|
||||||
let! tryListing =
|
let! tryListing =
|
||||||
connection ()
|
dataSource ()
|
||||||
|> Sql.query $"{viewSql} WHERE l.id = @id AND l.data ->> 'isLegacy' = 'false'"
|
|> Sql.query $"{viewSql} WHERE l.id = @id AND l.data ->> 'isLegacy' = 'false'"
|
||||||
|> Sql.parameters [ "@id", Sql.string (ListingId.toString listingId) ]
|
|> Sql.parameters [ "@id", Sql.string (ListingId.toString listingId) ]
|
||||||
|> Sql.executeAsync toListingForView
|
|> Sql.executeAsync toListingForView
|
||||||
|
@ -370,7 +401,7 @@ module Listings =
|
||||||
|
|
||||||
/// Save a listing
|
/// Save a listing
|
||||||
let save (listing : Listing) =
|
let save (listing : Listing) =
|
||||||
connection () |> saveDocument Table.Listing (ListingId.toString listing.Id) <| mkDoc listing
|
dataSource () |> saveDocument Table.Listing (ListingId.toString listing.Id) <| mkDoc listing
|
||||||
|
|
||||||
/// Search job listings
|
/// Search job listings
|
||||||
let search (search : ListingSearchForm) =
|
let search (search : ListingSearchForm) =
|
||||||
|
@ -384,7 +415,7 @@ module Listings =
|
||||||
if search.Text <> "" then
|
if search.Text <> "" then
|
||||||
"l.data ->> 'text' ILIKE @text", [ "@text", like search.Text ]
|
"l.data ->> 'text' ILIKE @text", [ "@text", like search.Text ]
|
||||||
]
|
]
|
||||||
connection ()
|
dataSource ()
|
||||||
|> Sql.query $"
|
|> Sql.query $"
|
||||||
{viewSql}
|
{viewSql}
|
||||||
WHERE l.data ->> 'isExpired' = 'false' AND l.data ->> 'isLegacy' = 'false'
|
WHERE l.data ->> 'isExpired' = 'false' AND l.data ->> 'isLegacy' = 'false'
|
||||||
|
@ -399,14 +430,14 @@ module Profiles =
|
||||||
|
|
||||||
/// Count the current profiles
|
/// Count the current profiles
|
||||||
let count () =
|
let count () =
|
||||||
connection ()
|
dataSource ()
|
||||||
|> Sql.query $"SELECT COUNT(id) AS the_count FROM {Table.Profile} WHERE data ->> 'isLegacy' = 'false'"
|
|> Sql.query $"SELECT COUNT(id) AS the_count FROM {Table.Profile} WHERE data ->> 'isLegacy' = 'false'"
|
||||||
|> Sql.executeRowAsync (fun row -> row.int64 "the_count")
|
|> Sql.executeRowAsync (fun row -> row.int64 "the_count")
|
||||||
|
|
||||||
/// Delete a profile by its ID
|
/// Delete a profile by its ID
|
||||||
let deleteById citizenId = backgroundTask {
|
let deleteById citizenId = backgroundTask {
|
||||||
let! _ =
|
let! _ =
|
||||||
connection ()
|
dataSource ()
|
||||||
|> Sql.query $"DELETE FROM {Table.Profile} WHERE id = @id"
|
|> Sql.query $"DELETE FROM {Table.Profile} WHERE id = @id"
|
||||||
|> Sql.parameters [ "@id", Sql.string (CitizenId.toString citizenId) ]
|
|> Sql.parameters [ "@id", Sql.string (CitizenId.toString citizenId) ]
|
||||||
|> Sql.executeNonQueryAsync
|
|> Sql.executeNonQueryAsync
|
||||||
|
@ -415,7 +446,7 @@ module Profiles =
|
||||||
|
|
||||||
/// Find a profile by citizen ID
|
/// Find a profile by citizen ID
|
||||||
let findById citizenId = backgroundTask {
|
let findById citizenId = backgroundTask {
|
||||||
match! connection () |> getDocument<Profile> Table.Profile (CitizenId.toString citizenId) with
|
match! dataSource () |> getDocument<Profile> Table.Profile (CitizenId.toString citizenId) with
|
||||||
| Some profile when not profile.IsLegacy -> return Some profile
|
| Some profile when not profile.IsLegacy -> return Some profile
|
||||||
| Some _
|
| Some _
|
||||||
| None -> return None
|
| None -> return None
|
||||||
|
@ -424,7 +455,7 @@ module Profiles =
|
||||||
/// Find a profile by citizen ID for viewing (includes citizen and continent information)
|
/// Find a profile by citizen ID for viewing (includes citizen and continent information)
|
||||||
let findByIdForView citizenId = backgroundTask {
|
let findByIdForView citizenId = backgroundTask {
|
||||||
let! tryCitizen =
|
let! tryCitizen =
|
||||||
connection ()
|
dataSource ()
|
||||||
|> Sql.query $"
|
|> Sql.query $"
|
||||||
SELECT p.*, c.data AS cit_data, o.data AS cont_data
|
SELECT p.*, c.data AS cit_data, o.data AS cont_data
|
||||||
FROM {Table.Profile} p
|
FROM {Table.Profile} p
|
||||||
|
@ -443,7 +474,7 @@ module Profiles =
|
||||||
|
|
||||||
/// Save a profile
|
/// Save a profile
|
||||||
let save (profile : Profile) =
|
let save (profile : Profile) =
|
||||||
connection () |> saveDocument Table.Profile (CitizenId.toString profile.Id) <| mkDoc profile
|
dataSource () |> saveDocument Table.Profile (CitizenId.toString profile.Id) <| mkDoc profile
|
||||||
|
|
||||||
/// Search profiles (logged-on users)
|
/// Search profiles (logged-on users)
|
||||||
let search (search : ProfileSearchForm) = backgroundTask {
|
let search (search : ProfileSearchForm) = backgroundTask {
|
||||||
|
@ -462,7 +493,7 @@ module Profiles =
|
||||||
[ "@text", like search.BioExperience ]
|
[ "@text", like search.BioExperience ]
|
||||||
]
|
]
|
||||||
let! results =
|
let! results =
|
||||||
connection ()
|
dataSource ()
|
||||||
|> Sql.query $"
|
|> Sql.query $"
|
||||||
SELECT p.*, c.data AS cit_data
|
SELECT p.*, c.data AS cit_data
|
||||||
FROM {Table.Profile} p
|
FROM {Table.Profile} p
|
||||||
|
@ -498,7 +529,7 @@ module Profiles =
|
||||||
WHERE x ->> 'description' ILIKE @description)",
|
WHERE x ->> 'description' ILIKE @description)",
|
||||||
[ "@description", like search.Skill ]
|
[ "@description", like search.Skill ]
|
||||||
]
|
]
|
||||||
connection ()
|
dataSource ()
|
||||||
|> Sql.query $"
|
|> Sql.query $"
|
||||||
SELECT p.*, c.data AS cont_data
|
SELECT p.*, c.data AS cont_data
|
||||||
FROM {Table.Profile} p
|
FROM {Table.Profile} p
|
||||||
|
@ -525,7 +556,7 @@ module Successes =
|
||||||
|
|
||||||
// Retrieve all success stories
|
// Retrieve all success stories
|
||||||
let all () =
|
let all () =
|
||||||
connection ()
|
dataSource ()
|
||||||
|> Sql.query $"
|
|> Sql.query $"
|
||||||
SELECT s.*, c.data AS cit_data
|
SELECT s.*, c.data AS cit_data
|
||||||
FROM {Table.Success} s
|
FROM {Table.Success} s
|
||||||
|
@ -544,9 +575,9 @@ module Successes =
|
||||||
|
|
||||||
/// Find a success story by its ID
|
/// Find a success story by its ID
|
||||||
let findById successId =
|
let findById successId =
|
||||||
connection () |> getDocument<Success> Table.Success (SuccessId.toString successId)
|
dataSource () |> getDocument<Success> Table.Success (SuccessId.toString successId)
|
||||||
|
|
||||||
/// Save a success story
|
/// Save a success story
|
||||||
let save (success : Success) =
|
let save (success : Success) =
|
||||||
connection () |> saveDocument Table.Success (SuccessId.toString success.Id) <| mkDoc success
|
dataSource () |> saveDocument Table.Success (SuccessId.toString success.Id) <| mkDoc success
|
||||||
|
|
|
@ -17,8 +17,7 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="FSharp.SystemTextJson" Version="0.19.13" />
|
<PackageReference Include="FSharp.SystemTextJson" Version="0.19.13" />
|
||||||
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.0.0" />
|
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.0.0" />
|
||||||
<PackageReference Include="Npgsql" Version="7.0.0" />
|
<PackageReference Include="Npgsql.FSharp" Version="5.6.0" />
|
||||||
<PackageReference Include="Npgsql.FSharp" Version="5.5.0" />
|
|
||||||
<PackageReference Include="Npgsql.NodaTime" Version="7.0.0" />
|
<PackageReference Include="Npgsql.NodaTime" Version="7.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
|
@ -6,20 +6,34 @@ open MailKit.Net.Smtp
|
||||||
open MailKit.Security
|
open MailKit.Security
|
||||||
open MimeKit
|
open MimeKit
|
||||||
|
|
||||||
|
/// Private functions for sending e-mail
|
||||||
|
[<AutoOpen>]
|
||||||
|
module private Helpers =
|
||||||
|
|
||||||
|
/// Create an SMTP client
|
||||||
|
let smtpClient () = backgroundTask {
|
||||||
|
let client = new SmtpClient ()
|
||||||
|
do! client.ConnectAsync ("localhost", 25, SecureSocketOptions.None)
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a message with to, from, and subject completed
|
||||||
|
let createMessage citizen subject =
|
||||||
|
let msg = new MimeMessage ()
|
||||||
|
msg.From.Add (MailboxAddress ("Jobs, Jobs, Jobs", (* "daniel@bitbadger.solutions" *) "summersd@localhost"))
|
||||||
|
msg.To.Add (MailboxAddress (Citizen.name citizen, (* citizen.Email *) "summersd@localhost"))
|
||||||
|
msg.Subject <- subject
|
||||||
|
msg
|
||||||
|
|
||||||
|
|
||||||
/// Send an account confirmation e-mail
|
/// Send an account confirmation e-mail
|
||||||
let sendAccountConfirmation citizen security = backgroundTask {
|
let sendAccountConfirmation citizen security = backgroundTask {
|
||||||
let name = Citizen.name citizen
|
let token = WebUtility.UrlEncode security.Token.Value
|
||||||
let token = WebUtility.UrlEncode security.Token.Value
|
use! client = smtpClient ()
|
||||||
use client = new SmtpClient ()
|
use msg = createMessage citizen "Account Confirmation Request"
|
||||||
do! client.ConnectAsync ("localhost", 25, SecureSocketOptions.None)
|
|
||||||
|
|
||||||
use msg = new MimeMessage ()
|
|
||||||
msg.From.Add (MailboxAddress ("Jobs, Jobs, Jobs", "daniel@bitbadger.solutions" (* "summersd@localhost" *) ))
|
|
||||||
msg.To.Add (MailboxAddress (name, citizen.Email (* "summersd@localhost" *) ))
|
|
||||||
msg.Subject <- "Account Confirmation Request"
|
|
||||||
|
|
||||||
let text =
|
let text =
|
||||||
[ $"ITM, {name}!"
|
[ $"ITM, {Citizen.name citizen}!"
|
||||||
""
|
""
|
||||||
"This e-mail address was recently used to establish an account on"
|
"This e-mail address was recently used to establish an account on"
|
||||||
"Jobs, Jobs, Jobs (noagendacareers.com). Before this account can be"
|
"Jobs, Jobs, Jobs (noagendacareers.com). Before this account can be"
|
||||||
|
@ -45,3 +59,36 @@ let sendAccountConfirmation citizen security = backgroundTask {
|
||||||
|
|
||||||
return! client.SendAsync msg
|
return! client.SendAsync msg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Send a password reset link
|
||||||
|
let sendPasswordReset citizen security = backgroundTask {
|
||||||
|
let token = WebUtility.UrlEncode security.Token.Value
|
||||||
|
use! client = smtpClient ()
|
||||||
|
use msg = createMessage citizen "Reset Password for Jobs, Jobs, Jobs"
|
||||||
|
|
||||||
|
let text =
|
||||||
|
[ $"ITM, {Citizen.name citizen}!"
|
||||||
|
""
|
||||||
|
"We recently receive a request to reset the password for your account"
|
||||||
|
"on Jobs, Jobs, Jobs (noagendacareers.com). Use the link below to"
|
||||||
|
"do so; it will work for the next 72 hours (3 days)."
|
||||||
|
""
|
||||||
|
$"https://noagendacareers.com/citizen/reset-password/{token}"
|
||||||
|
""
|
||||||
|
"If you did not take this action, you can do nothing, and the link"
|
||||||
|
"will expire normally. If you wish to expire the token immediately,"
|
||||||
|
"use the link below (also valid for 72 hours)."
|
||||||
|
""
|
||||||
|
$"https://noagendacareers.com/citizen/cancel-reset/{token}"
|
||||||
|
""
|
||||||
|
"TYFYC!"
|
||||||
|
""
|
||||||
|
"--"
|
||||||
|
"Jobs, Jobs, Jobs"
|
||||||
|
"https://noagendacareers.com"
|
||||||
|
] |> String.concat "\n"
|
||||||
|
use msgText = new TextPart (Text = text)
|
||||||
|
msg.Body <- msgText
|
||||||
|
|
||||||
|
return! client.SendAsync msg
|
||||||
|
}
|
||||||
|
|
|
@ -261,14 +261,26 @@ module Citizen =
|
||||||
| None -> return! Error.notFound next ctx
|
| None -> return! Error.notFound next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GET: /citizen/cancel-reset/[token]
|
||||||
|
let cancelReset token : HttpHandler = fun next ctx -> task {
|
||||||
|
let! wasCanceled = task {
|
||||||
|
match! Citizens.trySecurityByToken token with
|
||||||
|
| Some security ->
|
||||||
|
do! Citizens.saveSecurityInfo { security with Token = None; TokenUsage = None; TokenExpires = None }
|
||||||
|
return true
|
||||||
|
| None -> return false
|
||||||
|
}
|
||||||
|
return! Citizen.resetCanceled wasCanceled |> render "Password Reset Cancellation" next ctx
|
||||||
|
}
|
||||||
|
|
||||||
// GET: /citizen/confirm/[token]
|
// GET: /citizen/confirm/[token]
|
||||||
let confirm token next ctx = task {
|
let confirm token : HttpHandler = fun next ctx -> task {
|
||||||
let! isConfirmed = Citizens.confirmAccount token
|
let! isConfirmed = Citizens.confirmAccount token
|
||||||
return! Citizen.confirmAccount isConfirmed |> render "Account Confirmation" next ctx
|
return! Citizen.confirmAccount isConfirmed |> render "Account Confirmation" next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET: /citizen/dashboard
|
// GET: /citizen/dashboard
|
||||||
let dashboard = requireUser >=> fun next ctx -> task {
|
let dashboard : HttpHandler = requireUser >=> fun next ctx -> task {
|
||||||
let citizenId = currentCitizenId ctx
|
let citizenId = currentCitizenId ctx
|
||||||
let! citizen = Citizens.findById citizenId
|
let! citizen = Citizens.findById citizenId
|
||||||
let! profile = Profiles.findById citizenId
|
let! profile = Profiles.findById citizenId
|
||||||
|
@ -284,13 +296,38 @@ module Citizen =
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET: /citizen/deny/[token]
|
// GET: /citizen/deny/[token]
|
||||||
let deny token next ctx = task {
|
let deny token : HttpHandler = fun next ctx -> task {
|
||||||
let! wasDeleted = Citizens.denyAccount token
|
let! wasDeleted = Citizens.denyAccount token
|
||||||
return! Citizen.denyAccount wasDeleted |> render "Account Deletion" next ctx
|
return! Citizen.denyAccount wasDeleted |> render "Account Deletion" next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GET: /citizen/forgot-password
|
||||||
|
let forgotPassword : HttpHandler = fun next ctx ->
|
||||||
|
Citizen.forgotPassword (csrf ctx) |> render "Forgot Password" next ctx
|
||||||
|
|
||||||
|
// POST: /citizen/forgot-password
|
||||||
|
let doForgotPassword : HttpHandler = validateCsrf >=> fun next ctx -> task {
|
||||||
|
let! form = ctx.BindFormAsync<ForgotPasswordForm> ()
|
||||||
|
match! Citizens.tryByEmailWithSecurity form.Email with
|
||||||
|
| Some (citizen, security) ->
|
||||||
|
let withToken =
|
||||||
|
{ security with
|
||||||
|
Token = Some (Auth.createToken citizen)
|
||||||
|
TokenUsage = Some "reset"
|
||||||
|
TokenExpires = Some (now ctx + (Duration.FromDays 3))
|
||||||
|
}
|
||||||
|
do! Citizens.saveSecurityInfo withToken
|
||||||
|
let! emailResponse = Email.sendPasswordReset citizen withToken
|
||||||
|
let logFac = logger ctx
|
||||||
|
let log = logFac.CreateLogger "JobsJobsJobs.Handlers.Citizen"
|
||||||
|
log.LogInformation $"Password reset e-mail for {citizen.Email} received {emailResponse}"
|
||||||
|
| None -> ()
|
||||||
|
// TODO: send link if it matches an account
|
||||||
|
return! Citizen.forgotPasswordSent form |> render "Reset Request Processed" next ctx
|
||||||
|
}
|
||||||
|
|
||||||
// GET: /citizen/log-off
|
// GET: /citizen/log-off
|
||||||
let logOff = requireUser >=> fun next ctx -> task {
|
let logOff : HttpHandler = requireUser >=> fun next ctx -> task {
|
||||||
do! ctx.SignOutAsync CookieAuthenticationDefaults.AuthenticationScheme
|
do! ctx.SignOutAsync CookieAuthenticationDefaults.AuthenticationScheme
|
||||||
do! addSuccess "Log off successful" ctx
|
do! addSuccess "Log off successful" ctx
|
||||||
return! redirectToGet "/" next ctx
|
return! redirectToGet "/" next ctx
|
||||||
|
@ -396,6 +433,15 @@ module Citizen =
|
||||||
return! refreshPage () next ctx
|
return! refreshPage () next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GET: /citizen/reset-password/[token]
|
||||||
|
let resetPassword token : HttpHandler = fun next ctx -> task {
|
||||||
|
match! Citizens.trySecurityByToken token with
|
||||||
|
| Some security ->
|
||||||
|
// TODO: create form and page
|
||||||
|
return! Home.home |> render "TODO" next ctx
|
||||||
|
| None -> return! Error.notFound next ctx
|
||||||
|
}
|
||||||
|
|
||||||
// POST: /citizen/save-account
|
// POST: /citizen/save-account
|
||||||
let saveAccount : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task {
|
let saveAccount : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task {
|
||||||
let! theForm = ctx.BindFormAsync<AccountProfileForm> ()
|
let! theForm = ctx.BindFormAsync<AccountProfileForm> ()
|
||||||
|
@ -807,20 +853,24 @@ let allEndpoints = [
|
||||||
]
|
]
|
||||||
subRoute "/citizen" [
|
subRoute "/citizen" [
|
||||||
GET_HEAD [
|
GET_HEAD [
|
||||||
route "/account" Citizen.account
|
route "/account" Citizen.account
|
||||||
routef "/confirm/%s" Citizen.confirm
|
routef "/cancel-reset/%s" Citizen.cancelReset
|
||||||
route "/dashboard" Citizen.dashboard
|
routef "/confirm/%s" Citizen.confirm
|
||||||
routef "/deny/%s" Citizen.deny
|
route "/dashboard" Citizen.dashboard
|
||||||
route "/log-off" Citizen.logOff
|
routef "/deny/%s" Citizen.deny
|
||||||
route "/log-on" Citizen.logOn
|
route "/forgot-password" Citizen.forgotPassword
|
||||||
route "/register" Citizen.register
|
route "/log-off" Citizen.logOff
|
||||||
route "/so-long" Citizen.soLong
|
route "/log-on" Citizen.logOn
|
||||||
|
route "/register" Citizen.register
|
||||||
|
routef "/reset-password/%s" Citizen.resetPassword
|
||||||
|
route "/so-long" Citizen.soLong
|
||||||
]
|
]
|
||||||
POST [
|
POST [
|
||||||
route "/delete" Citizen.delete
|
route "/delete" Citizen.delete
|
||||||
route "/log-on" Citizen.doLogOn
|
route "/forgot-password" Citizen.doForgotPassword
|
||||||
route "/register" Citizen.doRegistration
|
route "/log-on" Citizen.doLogOn
|
||||||
route "/save-account" Citizen.saveAccount
|
route "/register" Citizen.doRegistration
|
||||||
|
route "/save-account" Citizen.saveAccount
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
subRoute "/listing" [
|
subRoute "/listing" [
|
||||||
|
|
|
@ -33,9 +33,9 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Giraffe" Version="6.0.0" />
|
<PackageReference Include="Giraffe" Version="6.0.0" />
|
||||||
<PackageReference Include="Giraffe.Htmx" Version="1.8.4" />
|
<PackageReference Include="Giraffe.Htmx" Version="1.8.5" />
|
||||||
<PackageReference Include="Giraffe.ViewEngine" Version="1.4.0" />
|
<PackageReference Include="Giraffe.ViewEngine" Version="1.4.0" />
|
||||||
<PackageReference Include="Giraffe.ViewEngine.Htmx" Version="1.8.4" />
|
<PackageReference Include="Giraffe.ViewEngine.Htmx" Version="1.8.5" />
|
||||||
<PackageReference Include="MailKit" Version="3.3.0" />
|
<PackageReference Include="MailKit" Version="3.3.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.0" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.0" />
|
||||||
<PackageReference Include="Microsoft.FSharpLu.Json" Version="0.11.7" />
|
<PackageReference Include="Microsoft.FSharpLu.Json" Version="0.11.7" />
|
||||||
|
|
|
@ -238,6 +238,14 @@ type ExpireListingForm =
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Form for the forgot / reset password page
|
||||||
|
[<CLIMutable; NoComparison; NoEquality>]
|
||||||
|
type ForgotPasswordForm =
|
||||||
|
{ /// The e-mail address for the account wishing to reset their password
|
||||||
|
Email : string
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// View model for the log on page
|
/// View model for the log on page
|
||||||
[<CLIMutable; NoComparison; NoEquality>]
|
[<CLIMutable; NoComparison; NoEquality>]
|
||||||
type LogOnViewModel =
|
type LogOnViewModel =
|
||||||
|
|
|
@ -13,9 +13,7 @@ let contactEdit (contacts : OtherContactForm array) =
|
||||||
div [ _id $"contactRow{idx}"; _class "row pb-3" ] [
|
div [ _id $"contactRow{idx}"; _class "row pb-3" ] [
|
||||||
div [ _class "col-2 col-md-1" ] [
|
div [ _class "col-2 col-md-1" ] [
|
||||||
button [ _type "button"; _class "btn btn-sm btn-outline-danger rounded-pill mt-3"; _title "Delete"
|
button [ _type "button"; _class "btn btn-sm btn-outline-danger rounded-pill mt-3"; _title "Delete"
|
||||||
_onclick $"jjj.citizen.removeContact({idx})" ] [
|
_onclick $"jjj.citizen.removeContact({idx})" ] [ txt " − " ]
|
||||||
rawText " − "
|
|
||||||
]
|
|
||||||
]
|
]
|
||||||
div [ _class "col-10 col-md-4 col-xl-3" ] [
|
div [ _class "col-10 col-md-4 col-xl-3" ] [
|
||||||
div [ _class "form-floating" ] [
|
div [ _class "form-floating" ] [
|
||||||
|
@ -23,37 +21,37 @@ let contactEdit (contacts : OtherContactForm array) =
|
||||||
_value contact.ContactType; _placeholder "Type"; _required ] [
|
_value contact.ContactType; _placeholder "Type"; _required ] [
|
||||||
let optionFor value label =
|
let optionFor value label =
|
||||||
let typ = ContactType.toString value
|
let typ = ContactType.toString value
|
||||||
option [ _value typ; if contact.ContactType = typ then _selected ] [ rawText label ]
|
option [ _value typ; if contact.ContactType = typ then _selected ] [ txt label ]
|
||||||
optionFor Website "Website"
|
optionFor Website "Website"
|
||||||
optionFor Email "E-mail Address"
|
optionFor Email "E-mail Address"
|
||||||
optionFor Phone "Phone Number"
|
optionFor Phone "Phone Number"
|
||||||
]
|
]
|
||||||
label [ _class "jjj-required"; _for $"contactType{idx}" ] [ rawText "Type" ]
|
label [ _class "jjj-required"; _for $"contactType{idx}" ] [ txt "Type" ]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
div [ _class "col-12 col-md-4 col-xl-3" ] [
|
div [ _class "col-12 col-md-4 col-xl-3" ] [
|
||||||
div [ _class "form-floating" ] [
|
div [ _class "form-floating" ] [
|
||||||
input [ _type "text"; _id $"contactName{idx}"; _name $"Contacts[{idx}].Name"; _class "form-control"
|
input [ _type "text"; _id $"contactName{idx}"; _name $"Contacts[{idx}].Name"; _class "form-control"
|
||||||
_maxlength "1000"; _value contact.Name; _placeholder "Name" ]
|
_maxlength "1000"; _value contact.Name; _placeholder "Name" ]
|
||||||
label [ _class "jjj-label"; _for $"contactName{idx}" ] [ rawText "Name" ]
|
label [ _class "jjj-label"; _for $"contactName{idx}" ] [ txt "Name" ]
|
||||||
]
|
]
|
||||||
if idx < 1 then
|
if idx < 1 then
|
||||||
div [ _class "form-text" ] [ rawText "Optional; will link sites and e-mail, qualify phone numbers" ]
|
div [ _class "form-text" ] [ txt "Optional; will link sites and e-mail, qualify phone numbers" ]
|
||||||
]
|
]
|
||||||
div [ _class "col-12 col-md-7 offset-md-1 col-xl-4 offset-xl-0" ] [
|
div [ _class "col-12 col-md-7 offset-md-1 col-xl-4 offset-xl-0" ] [
|
||||||
div [ _class "form-floating" ] [
|
div [ _class "form-floating" ] [
|
||||||
input [ _type "text"; _id $"contactValue{idx}"; _name $"Contacts[{idx}].Value"
|
input [ _type "text"; _id $"contactValue{idx}"; _name $"Contacts[{idx}].Value"
|
||||||
_class "form-control"; _maxlength "1000"; _value contact.Value; _placeholder "Contact"
|
_class "form-control"; _maxlength "1000"; _value contact.Value; _placeholder "Contact"
|
||||||
_required ]
|
_required ]
|
||||||
label [ _class "jjj-required"; _for "contactValue{idx}" ] [ rawText "Contact" ]
|
label [ _class "jjj-required"; _for "contactValue{idx}" ] [ txt "Contact" ]
|
||||||
]
|
]
|
||||||
if idx < 1 then div [ _class "form-text"] [ rawText "The URL, e-mail address, or phone number" ]
|
if idx < 1 then div [ _class "form-text"] [ txt "The URL, e-mail address, or phone number" ]
|
||||||
]
|
]
|
||||||
div [ _class "col-12 col-md-3 offset-md-1 col-xl-1 offset-xl-0" ] [
|
div [ _class "col-12 col-md-3 offset-md-1 col-xl-1 offset-xl-0" ] [
|
||||||
div [ _class "form-check mt-3" ] [
|
div [ _class "form-check mt-3" ] [
|
||||||
input [ _type "checkbox"; _id $"contactIsPublic{idx}"; _name $"Contacts[{idx}].IsPublic";
|
input [ _type "checkbox"; _id $"contactIsPublic{idx}"; _name $"Contacts[{idx}].IsPublic";
|
||||||
_class "form-check-input"; _value "true"; if contact.IsPublic then _checked ]
|
_class "form-check-input"; _value "true"; if contact.IsPublic then _checked ]
|
||||||
label [ _class "form-check-label"; _for $"contactIsPublic{idx}" ] [ rawText "Public" ]
|
label [ _class "form-check-label"; _for $"contactIsPublic{idx}" ] [ txt "Public" ]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
@ -64,12 +62,11 @@ let contactEdit (contacts : OtherContactForm array) =
|
||||||
|
|
||||||
/// The account edit page
|
/// The account edit page
|
||||||
let account (m : AccountProfileForm) csrf =
|
let account (m : AccountProfileForm) csrf =
|
||||||
article [] [
|
pageWithTitle "Account Profile" [
|
||||||
h3 [ _class "pb-3" ] [ rawText "Account Profile" ]
|
|
||||||
p [] [
|
p [] [
|
||||||
rawText "This information is visible to all fellow logged-on citizens. For publicly-visible employment "
|
txt "This information is visible to all fellow logged-on citizens. For publicly-visible employment "
|
||||||
rawText "profiles and job listings, the “Display Name” fields and any public contacts will be "
|
txt "profiles and job listings, the “Display Name” fields and any public contacts will be "
|
||||||
rawText "displayed."
|
txt "displayed."
|
||||||
]
|
]
|
||||||
form [ _class "row g-3"; _method "POST"; _action "/citizen/save-account" ] [
|
form [ _class "row g-3"; _method "POST"; _action "/citizen/save-account" ] [
|
||||||
antiForgery csrf
|
antiForgery csrf
|
||||||
|
@ -81,38 +78,32 @@ let account (m : AccountProfileForm) csrf =
|
||||||
]
|
]
|
||||||
div [ _class "col-6 col-xl-4" ] [
|
div [ _class "col-6 col-xl-4" ] [
|
||||||
textBox [ _type "text" ] (nameof m.DisplayName) m.DisplayName "Display Name" false
|
textBox [ _type "text" ] (nameof m.DisplayName) m.DisplayName "Display Name" false
|
||||||
div [ _class "form-text" ] [ em [] [ rawText "Optional; overrides first/last for display" ] ]
|
div [ _class "form-text" ] [ em [] [ txt "Optional; overrides first/last for display" ] ]
|
||||||
]
|
]
|
||||||
div [ _class "col-6 col-xl-4" ] [
|
div [ _class "col-6 col-xl-4" ] [
|
||||||
textBox [ _type "password"; _minlength "8" ] (nameof m.NewPassword) "" "New Password" false
|
textBox [ _type "password"; _minlength "8" ] (nameof m.NewPassword) "" "New Password" false
|
||||||
div [ _class "form-text" ] [ rawText "Leave blank to keep your current password" ]
|
div [ _class "form-text" ] [ txt "Leave blank to keep your current password" ]
|
||||||
]
|
]
|
||||||
div [ _class "col-6 col-xl-4" ] [
|
div [ _class "col-6 col-xl-4" ] [
|
||||||
textBox [ _type "password"; _minlength "8" ] (nameof m.NewPasswordConfirm) "" "Confirm New Password"
|
textBox [ _type "password"; _minlength "8" ] (nameof m.NewPasswordConfirm) "" "Confirm New Password"
|
||||||
false
|
false
|
||||||
div [ _class "form-text" ] [ rawText "Leave blank to keep your current password" ]
|
div [ _class "form-text" ] [ txt "Leave blank to keep your current password" ]
|
||||||
]
|
]
|
||||||
div [ _class "col-12" ] [
|
div [ _class "col-12" ] [
|
||||||
hr []
|
hr []
|
||||||
h4 [ _class "pb-2" ] [
|
h4 [ _class "pb-2" ] [
|
||||||
rawText "Ways to Be Contacted "
|
txt "Ways to Be Contacted "
|
||||||
button [ _type "button"; _class "btn btn-sm btn-outline-primary rounded-pill"
|
button [ _type "button"; _class "btn btn-sm btn-outline-primary rounded-pill"
|
||||||
_onclick "jjj.citizen.addContact()" ] [
|
_onclick "jjj.citizen.addContact()" ] [ txt "Add a Contact Method" ]
|
||||||
rawText "Add a Contact Method"
|
|
||||||
]
|
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
yield! contactEdit m.Contacts
|
yield! contactEdit m.Contacts
|
||||||
div [ _class "col-12" ] [
|
div [ _class "col-12" ] [ submitButton "content-save-outline" "Save" ]
|
||||||
button [ _type "submit"; _class "btn btn-primary" ] [
|
|
||||||
i [ _class "mdi mdi-content-save-outline" ] [ rawText " Save" ]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
]
|
||||||
hr []
|
hr []
|
||||||
p [ _class "text-muted fst-italic" ] [
|
p [ _class "text-muted fst-italic" ] [
|
||||||
rawText "(If you want to delete your profile, or your entire account, "
|
txt "(If you want to delete your profile, or your entire account, "
|
||||||
a [ _href "/citizen/so-long" ] [ rawText "see your deletion options here" ]; rawText ".)"
|
a [ _href "/citizen/so-long" ] [ rawText "see your deletion options here" ]; txt ".)"
|
||||||
]
|
]
|
||||||
jsOnLoad $"
|
jsOnLoad $"
|
||||||
jjj.citizen.nextIndex = {m.Contacts.Length}
|
jjj.citizen.nextIndex = {m.Contacts.Length}
|
||||||
|
@ -122,157 +113,171 @@ let account (m : AccountProfileForm) csrf =
|
||||||
|
|
||||||
/// The account confirmation page
|
/// The account confirmation page
|
||||||
let confirmAccount isConfirmed =
|
let confirmAccount isConfirmed =
|
||||||
article [] [
|
pageWithTitle "Account Confirmation" [
|
||||||
h3 [ _class "pb-3" ] [ rawText "Account Confirmation" ]
|
|
||||||
p [] [
|
p [] [
|
||||||
if isConfirmed then
|
if isConfirmed then
|
||||||
rawText "Your account was confirmed successfully! You may "
|
txt "Your account was confirmed successfully! You may "
|
||||||
a [ _href "/citizen/log-on" ] [ rawText "log on here" ]; rawText "."
|
a [ _href "/citizen/log-on" ] [ rawText "log on here" ]; txt "."
|
||||||
else
|
else
|
||||||
rawText "The confirmation token did not match any pending accounts. Confirmation tokens are only valid "
|
txt "The confirmation token did not match any pending accounts. Confirmation tokens are only valid for "
|
||||||
rawText "for 3 days; if the token expired, you will need to re-register, which "
|
txt "3 days; if the token expired, you will need to re-register, which "
|
||||||
a [ _href "/citizen/register" ] [ rawText "you can do here" ]; rawText "."
|
a [ _href "/citizen/register" ] [ txt "you can do here" ]; txt "."
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
/// The citizen's dashboard page
|
/// The citizen's dashboard page
|
||||||
let dashboard (citizen : Citizen) (profile : Profile option) profileCount tz =
|
let dashboard (citizen : Citizen) (profile : Profile option) profileCount tz =
|
||||||
article [ _class "container" ] [
|
article [ _class "container" ] [
|
||||||
h3 [ _class "pb-4" ] [ rawText "ITM, "; str citizen.FirstName; rawText "!" ]
|
h3 [ _class "pb-4" ] [ str $"ITM, {citizen.FirstName}!" ]
|
||||||
div [ _class "row row-cols-1 row-cols-md-2" ] [
|
div [ _class "row row-cols-1 row-cols-md-2" ] [
|
||||||
div [ _class "col" ] [
|
div [ _class "col" ] [
|
||||||
div [ _class "card h-100" ] [
|
div [ _class "card h-100" ] [
|
||||||
h5 [ _class "card-header" ] [ rawText "Your Profile" ]
|
h5 [ _class "card-header" ] [ txt "Your Profile" ]
|
||||||
div [ _class "card-body" ] [
|
div [ _class "card-body" ] [
|
||||||
match profile with
|
match profile with
|
||||||
| Some prfl ->
|
| Some prfl ->
|
||||||
h6 [ _class "card-subtitle mb-3 text-muted fst-italic" ] [
|
h6 [ _class "card-subtitle mb-3 text-muted fst-italic" ] [
|
||||||
rawText "Last updated "; str (fullDateTime prfl.LastUpdatedOn tz)
|
str $"Last updated {fullDateTime prfl.LastUpdatedOn tz}"
|
||||||
]
|
]
|
||||||
p [ _class "card-text" ] [
|
p [ _class "card-text" ] [
|
||||||
rawText "Your profile currently lists "; str $"{List.length prfl.Skills}"
|
txt $"Your profile currently lists {List.length prfl.Skills} skill"
|
||||||
rawText " skill"; rawText (if List.length prfl.Skills <> 1 then "s" else "")
|
txt (if List.length prfl.Skills <> 1 then "s" else ""); txt "."
|
||||||
rawText "."
|
|
||||||
if prfl.IsSeekingEmployment then
|
if prfl.IsSeekingEmployment then
|
||||||
br []; br []
|
br []; br []
|
||||||
rawText "Your profile indicates that you are seeking employment. Once you find it, "
|
txt "Your profile indicates that you are seeking employment. Once you find it, "
|
||||||
a [ _href "/success-story/add" ] [ rawText "tell your fellow citizens about it!" ]
|
a [ _href "/success-story/add" ] [ txt "tell your fellow citizens about it!" ]
|
||||||
]
|
]
|
||||||
| None ->
|
| None ->
|
||||||
p [ _class "card-text" ] [
|
p [ _class "card-text" ] [
|
||||||
rawText "You do not have an employment profile established; click below (or "
|
txt "You do not have an employment profile established; click below (or “Edit "
|
||||||
rawText "“Edit Profile” in the menu) to get started!"
|
txt "Profile” in the menu) to get started!"
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
div [ _class "card-footer" ] [
|
div [ _class "card-footer" ] [
|
||||||
match profile with
|
match profile with
|
||||||
| Some p ->
|
| Some _ ->
|
||||||
a [ _href $"/profile/{CitizenId.toString citizen.Id}/view"
|
a [ _href $"/profile/{CitizenId.toString citizen.Id}/view"
|
||||||
_class "btn btn-outline-secondary" ] [
|
_class "btn btn-outline-secondary" ] [ txt "View Profile" ]; txt " "
|
||||||
rawText "View Profile"
|
a [ _href "/profile/edit"; _class "btn btn-outline-secondary" ] [ txt "Edit Profile" ]
|
||||||
]; rawText " "
|
|
||||||
a [ _href "/profile/edit"; _class "btn btn-outline-secondary" ] [ rawText "Edit Profile" ]
|
|
||||||
| None ->
|
| None ->
|
||||||
a [ _href "/profile/edit"; _class "btn btn-primary" ] [ rawText "Create Profile" ]
|
a [ _href "/profile/edit"; _class "btn btn-primary" ] [ txt "Create Profile" ]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
div [ _class "col" ] [
|
div [ _class "col" ] [
|
||||||
div [ _class "card h-100" ] [
|
div [ _class "card h-100" ] [
|
||||||
h5 [ _class "card-header" ] [ rawText "Other Citizens" ]
|
h5 [ _class "card-header" ] [ txt "Other Citizens" ]
|
||||||
div [ _class "card-body" ] [
|
div [ _class "card-body" ] [
|
||||||
h6 [ _class "card-subtitle mb-3 text-muted fst-italic" ] [
|
h6 [ _class "card-subtitle mb-3 text-muted fst-italic" ] [
|
||||||
rawText (if profileCount = 0L then "No" else $"{profileCount} Total")
|
txt (if profileCount = 0L then "No" else $"{profileCount} Total")
|
||||||
rawText " Employment Profile"; rawText (if profileCount <> 1 then "s" else "")
|
txt " Employment Profile"; txt (if profileCount <> 1 then "s" else "")
|
||||||
]
|
]
|
||||||
p [ _class "card-text" ] [
|
p [ _class "card-text" ] [
|
||||||
if profileCount = 1 && Option.isSome profile then
|
if profileCount = 1 && Option.isSome profile then
|
||||||
"It looks like, for now, it’s just you…"
|
"It looks like, for now, it’s just you…"
|
||||||
else if profileCount > 0 then "Take a look around and see if you can help them find work!"
|
else if profileCount > 0 then "Take a look around and see if you can help them find work!"
|
||||||
else "You can click below, but you will not find anything…"
|
else "You can click below, but you will not find anything…"
|
||||||
|> rawText
|
|> txt
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
div [ _class "card-footer" ] [
|
div [ _class "card-footer" ] [
|
||||||
a [ _href "/profile/search"; _class "btn btn-outline-secondary" ] [ rawText "Search Profiles" ]
|
a [ _href "/profile/search"; _class "btn btn-outline-secondary" ] [ txt "Search Profiles" ]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
p [] [ rawText " " ]
|
emptyP
|
||||||
p [] [
|
p [] [
|
||||||
rawText "To see how this application works, check out “How It Works” in the sidebar (last "
|
txt "To see how this application works, check out “How It Works” in the sidebar (last updated "
|
||||||
rawText "updated August 29<sup>th</sup>, 2021)."
|
txt "August 29<sup>th</sup>, 2021)."
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
/// The account deletion success page
|
/// The account deletion success page
|
||||||
let deleted =
|
let deleted =
|
||||||
article [] [
|
pageWithTitle "Account Deletion Success" [
|
||||||
h3 [ _class "pb-3" ] [ rawText "Account Deletion Success" ]
|
emptyP; p [] [ txt "Your account has been successfully deleted." ]
|
||||||
p [] [ rawText " " ]
|
emptyP; p [] [ txt "Thank you for participating, and thank you for your courage. #GitmoNation" ]
|
||||||
p [] [ rawText "Your account has been successfully deleted." ]
|
|
||||||
p [] [ rawText " " ]
|
|
||||||
p [] [ rawText "Thank you for participating, and thank you for your courage. #GitmoNation" ]
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
/// The profile or account deletion page
|
/// The profile or account deletion page
|
||||||
let deletionOptions csrf =
|
let deletionOptions csrf =
|
||||||
article [] [
|
pageWithTitle "Account Deletion Options" [
|
||||||
h3 [ _class "pb-3" ] [ rawText "Account Deletion Options" ]
|
h4 [ _class "pb-3" ] [ txt "Option 1 – Delete Your Profile" ]
|
||||||
h4 [ _class "pb-3" ] [ rawText "Option 1 – Delete Your Profile" ]
|
|
||||||
p [] [
|
p [] [
|
||||||
rawText "Utilizing this option will remove your current employment profile and skills. This will preserve "
|
txt "Utilizing this option will remove your current employment profile and skills. This will preserve any "
|
||||||
rawText "any job listings you may have posted, or any success stories you may have written, and preserves "
|
txt "job listings you may have posted, or any success stories you may have written, and preserves this "
|
||||||
rawText "this application’s knowledge of you. This is what you want to use if you want to clear out "
|
txt "this application’s knowledge of you. This is what you want to use if you want to clear out your "
|
||||||
rawText "your profile and start again (and remove the current one from others’ view)."
|
txt "profile and start again (and remove the current one from others’ view)."
|
||||||
]
|
]
|
||||||
form [ _class "text-center"; _method "POST"; _action "/profile/delete" ] [
|
form [ _class "text-center"; _method "POST"; _action "/profile/delete" ] [
|
||||||
antiForgery csrf
|
antiForgery csrf
|
||||||
button [ _type "submit"; _class "btn btn-danger" ] [ rawText "Delete Your Profile" ]
|
button [ _type "submit"; _class "btn btn-danger" ] [ txt "Delete Your Profile" ]
|
||||||
]
|
]
|
||||||
hr []
|
hr []
|
||||||
h4 [ _class "pb-3" ] [ rawText "Option 2 – Delete Your Account" ]
|
h4 [ _class "pb-3" ] [ txt "Option 2 – Delete Your Account" ]
|
||||||
p [] [
|
p [] [
|
||||||
rawText "This option will make it like you never visited this site. It will delete your profile, skills, "
|
txt "This option will make it like you never visited this site. It will delete your profile, skills, job "
|
||||||
rawText "job listings, success stories, and account. This is what you want to use if you want to disappear "
|
txt "listings, success stories, and account. This is what you want to use if you want to disappear from "
|
||||||
rawText "from this application."
|
txt "this application."
|
||||||
]
|
]
|
||||||
form [ _class "text-center"; _method "POST"; _action "/citizen/delete" ] [
|
form [ _class "text-center"; _method "POST"; _action "/citizen/delete" ] [
|
||||||
antiForgery csrf
|
antiForgery csrf
|
||||||
button [ _type "submit"; _class "btn btn-danger" ] [ rawText "Delete Your Entire Account" ]
|
button [ _type "submit"; _class "btn btn-danger" ] [ txt "Delete Your Entire Account" ]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
/// The account denial page
|
/// The account denial page
|
||||||
let denyAccount wasDeleted =
|
let denyAccount wasDeleted =
|
||||||
article [] [
|
pageWithTitle "Account Deletion" [
|
||||||
h3 [ _class "pb-3" ] [ rawText "Account Deletion" ]
|
|
||||||
p [] [
|
p [] [
|
||||||
if wasDeleted then
|
if wasDeleted then txt "The account was deleted successfully; sorry for the trouble."
|
||||||
rawText "The account was deleted successfully; sorry for the trouble."
|
|
||||||
else
|
else
|
||||||
rawText "The confirmation token did not match any pending accounts; if this was an inadvertently "
|
txt "The confirmation token did not match any pending accounts; if this was an inadvertently created "
|
||||||
rawText "created account, it has likely already been deleted."
|
txt "account, it has likely already been deleted."
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
/// The forgot / reset password page
|
||||||
|
let forgotPassword csrf =
|
||||||
|
let m = { Email = "" }
|
||||||
|
pageWithTitle "Forgot Password" [
|
||||||
|
p [] [
|
||||||
|
txt "Enter your e-mail address below; if it matches the e-mail address of an account, we will send a "
|
||||||
|
txt "password reset link."
|
||||||
|
]
|
||||||
|
form [ _class "row g-3 pb-3"; _method "POST"; _action "/citizen/forgot-password" ] [
|
||||||
|
antiForgery csrf
|
||||||
|
div [ _class "col-12 col-md-6 offset-md-3" ] [
|
||||||
|
textBox [ _type "email"; _autofocus ] (nameof m.Email) m.Email "E-mail Address" true
|
||||||
|
]
|
||||||
|
div [ _class "col-12" ] [ submitButton "login" "Send Reset Link" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
/// The page displayed after a forgotten / reset request has been processed
|
||||||
|
let forgotPasswordSent (m : ForgotPasswordForm) =
|
||||||
|
pageWithTitle "Reset Request Processed" [
|
||||||
|
p [] [ txt "The reset link request has been processed; check your e-mail for further instructions." ]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
/// The log on page
|
/// The log on page
|
||||||
let logOn (m : LogOnViewModel) csrf =
|
let logOn (m : LogOnViewModel) csrf =
|
||||||
article [] [
|
pageWithTitle "Log On" [
|
||||||
h3 [ _class "pb-3" ] [ rawText "Log On" ]
|
|
||||||
match m.ErrorMessage with
|
match m.ErrorMessage with
|
||||||
| Some msg ->
|
| Some msg ->
|
||||||
p [ _class "pb-3 text-center" ] [
|
p [ _class "pb-3 text-center" ] [
|
||||||
span [ _class "text-danger" ] [ str msg ]; br []
|
span [ _class "text-danger" ] [ txt msg ]; br []
|
||||||
if msg.IndexOf("ocked") > -1 then
|
if msg.IndexOf("ocked") > -1 then
|
||||||
rawText "If this is a new account, it must be confirmed before it can be used; otherwise, you need "
|
txt "If this is a new account, it must be confirmed before it can be used; otherwise, you need to "
|
||||||
rawText "to "
|
a [ _href "/citizen/forgot-password" ] [ txt "request an unlock code" ]
|
||||||
a [ _href "/citizen/forgot-password" ] [ rawText "request an unlock code" ]
|
txt " before you may log on."
|
||||||
rawText " before you may log on."
|
|
||||||
]
|
]
|
||||||
| None -> ()
|
| None -> ()
|
||||||
form [ _class "row g-3 pb-3"; _hxPost "/citizen/log-on" ] [
|
form [ _class "row g-3 pb-3"; _hxPost "/citizen/log-on" ] [
|
||||||
|
@ -286,24 +291,19 @@ let logOn (m : LogOnViewModel) csrf =
|
||||||
div [ _class "col-12 col-md-6" ] [
|
div [ _class "col-12 col-md-6" ] [
|
||||||
textBox [ _type "password" ] (nameof m.Password) "" "Password" true
|
textBox [ _type "password" ] (nameof m.Password) "" "Password" true
|
||||||
]
|
]
|
||||||
div [ _class "col-12" ] [
|
div [ _class "col-12" ] [ submitButton "login" "Log On" ]
|
||||||
button [ _class "btn btn-primary"; _type "submit" ] [
|
|
||||||
i [ _class "mdi mdi-login" ] []; rawText " Log On"
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
]
|
||||||
p [ _class "text-center" ] [
|
p [ _class "text-center" ] [
|
||||||
rawText "Need an account? "; a [ _href "/citizen/register" ] [ rawText "Register for one!" ]
|
txt "Need an account? "; a [ _href "/citizen/register" ] [ txt "Register for one!" ]
|
||||||
]
|
]
|
||||||
p [ _class "text-center" ] [
|
p [ _class "text-center" ] [
|
||||||
rawText "Forgot your password? "; a [ _href "/citizen/forgot-password" ] [ rawText "Request a reset." ]
|
txt "Forgot your password? "; a [ _href "/citizen/forgot-password" ] [ txt "Request a reset." ]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
/// The registration page
|
/// The registration page
|
||||||
let register q1 q2 (m : RegisterViewModel) csrf =
|
let register q1 q2 (m : RegisterViewModel) csrf =
|
||||||
article [] [
|
pageWithTitle "Register" [
|
||||||
h3 [ _class "pb-3" ] [ rawText "Register" ]
|
|
||||||
form [ _class "row g-3"; _hxPost "/citizen/register" ] [
|
form [ _class "row g-3"; _hxPost "/citizen/register" ] [
|
||||||
antiForgery csrf
|
antiForgery csrf
|
||||||
div [ _class "col-6 col-xl-4" ] [
|
div [ _class "col-6 col-xl-4" ] [
|
||||||
|
@ -314,7 +314,7 @@ let register q1 q2 (m : RegisterViewModel) csrf =
|
||||||
]
|
]
|
||||||
div [ _class "col-6 col-xl-4" ] [
|
div [ _class "col-6 col-xl-4" ] [
|
||||||
textBox [ _type "text" ] (nameof m.DisplayName) (defaultArg m.DisplayName "") "Display Name" false
|
textBox [ _type "text" ] (nameof m.DisplayName) (defaultArg m.DisplayName "") "Display Name" false
|
||||||
div [ _class "form-text" ] [ em [] [ rawText "Optional; overrides first/last for display" ] ]
|
div [ _class "form-text fst-italic" ] [ txt "Optional; overrides first/last for display" ]
|
||||||
]
|
]
|
||||||
div [ _class "col-6 col-xl-4" ] [
|
div [ _class "col-6 col-xl-4" ] [
|
||||||
textBox [ _type "text" ] (nameof m.Email) m.Email "E-mail Address" true
|
textBox [ _type "text" ] (nameof m.Email) m.Email "E-mail Address" true
|
||||||
|
@ -328,7 +328,7 @@ let register q1 q2 (m : RegisterViewModel) csrf =
|
||||||
div [ _class "col-12" ] [
|
div [ _class "col-12" ] [
|
||||||
hr []
|
hr []
|
||||||
p [ _class "mb-0 text-muted fst-italic" ] [
|
p [ _class "mb-0 text-muted fst-italic" ] [
|
||||||
rawText "Before your account request is through, you must answer these questions two…"
|
txt "Before your account request is through, you must answer these questions two…"
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
div [ _class "col-12 col-xl-6" ] [
|
div [ _class "col-12 col-xl-6" ] [
|
||||||
|
@ -339,30 +339,34 @@ let register q1 q2 (m : RegisterViewModel) csrf =
|
||||||
textBox [ _type "text"; _maxlength "30" ] (nameof m.Question2Answer) m.Question2Answer q2 true
|
textBox [ _type "text"; _maxlength "30" ] (nameof m.Question2Answer) m.Question2Answer q2 true
|
||||||
input [ _type "hidden"; _name (nameof m.Question2Index); _value (string m.Question2Index ) ]
|
input [ _type "hidden"; _name (nameof m.Question2Index); _value (string m.Question2Index ) ]
|
||||||
]
|
]
|
||||||
div [ _class "col-12" ] [
|
div [ _class "col-12" ] [ submitButton "content-save-outline" "Save" ]
|
||||||
button [ _type "submit"; _class "btn btn-primary" ] [
|
|
||||||
i [ _class "mdi mdi-content-save-outline" ] []; rawText " Save"
|
|
||||||
]
|
|
||||||
]
|
|
||||||
jsOnLoad $"jjj.citizen.validatePasswords('{nameof m.Password}', 'ConfirmPassword', true)"
|
jsOnLoad $"jjj.citizen.validatePasswords('{nameof m.Password}', 'ConfirmPassword', true)"
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
/// The confirmation page for user registration
|
/// The confirmation page for user registration
|
||||||
let registered =
|
let registered =
|
||||||
article [] [
|
pageWithTitle "Registration Successful" [
|
||||||
h3 [ _class "pb-3" ] [ rawText "Registration Successful" ]
|
|
||||||
p [] [
|
p [] [
|
||||||
rawText "You have been successfully registered with Jobs, Jobs, Jobs. Check your e-mail for a confirmation "
|
txt "You have been successfully registered with Jobs, Jobs, Jobs. Check your e-mail for a confirmation "
|
||||||
rawText "link; it will be valid for the next 72 hours (3 days). Once you confirm your account, you will be "
|
txt "link; it will be valid for the next 72 hours (3 days). Once you confirm your account, you will be "
|
||||||
rawText "able to log on using the e-mail address and password you provided."
|
txt "able to log on using the e-mail address and password you provided."
|
||||||
]
|
]
|
||||||
p [] [
|
p [] [
|
||||||
rawText "If the account is not confirmed within the 72-hour window, it will be deleted, and you will need "
|
txt "If the account is not confirmed within the 72-hour window, it will be deleted, and you will need to "
|
||||||
rawText "to register again."
|
txt "register again."
|
||||||
]
|
]
|
||||||
p [] [
|
p [] [
|
||||||
rawText "If you encounter issues, feel free to reach out to @danieljsummers on No Agenda Social for "
|
txt "If you encounter issues, feel free to reach out to @danieljsummers on No Agenda Social for assistance."
|
||||||
rawText "assistance."
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
/// The confirmation page for canceling a reset request
|
||||||
|
let resetCanceled wasCanceled =
|
||||||
|
let pgTitle = if wasCanceled then "Password Reset Request Canceled" else "Reset Request Not Found"
|
||||||
|
pageWithTitle pgTitle [
|
||||||
|
p [] [
|
||||||
|
if wasCanceled then txt "Your password reset request has been canceled."
|
||||||
|
else txt "There was no active password reset request found; it may have already expired."
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
|
@ -16,6 +16,16 @@ let audioClip clip text =
|
||||||
let antiForgery (csrf : AntiforgeryTokenSet) =
|
let antiForgery (csrf : AntiforgeryTokenSet) =
|
||||||
input [ _type "hidden"; _name csrf.FormFieldName; _value csrf.RequestToken ]
|
input [ _type "hidden"; _name csrf.FormFieldName; _value csrf.RequestToken ]
|
||||||
|
|
||||||
|
/// Alias for rawText
|
||||||
|
let txt = rawText
|
||||||
|
|
||||||
|
/// Create a page with a title displayed on the page
|
||||||
|
let pageWithTitle title content =
|
||||||
|
article [] [
|
||||||
|
h3 [ _class "pb-3" ] [ txt title ]
|
||||||
|
yield! content
|
||||||
|
]
|
||||||
|
|
||||||
/// Create a floating-label text input box
|
/// Create a floating-label text input box
|
||||||
let textBox attrs name value fieldLabel isRequired =
|
let textBox attrs name value fieldLabel isRequired =
|
||||||
div [ _class "form-floating" ] [
|
div [ _class "form-floating" ] [
|
||||||
|
@ -23,7 +33,7 @@ let textBox attrs name value fieldLabel isRequired =
|
||||||
_id name; _name name; _class "form-control"; _placeholder fieldLabel; _value value
|
_id name; _name name; _class "form-control"; _placeholder fieldLabel; _value value
|
||||||
if isRequired then _required
|
if isRequired then _required
|
||||||
] |> input
|
] |> input
|
||||||
label [ _class (if isRequired then "jjj-required" else "jjj-label"); _for name ] [ rawText fieldLabel ]
|
label [ _class (if isRequired then "jjj-required" else "jjj-label"); _for name ] [ txt fieldLabel ]
|
||||||
]
|
]
|
||||||
|
|
||||||
/// Create a checkbox that will post "true" if checked
|
/// Create a checkbox that will post "true" if checked
|
||||||
|
@ -33,7 +43,7 @@ let checkBox attrs name isChecked checkLabel =
|
||||||
[ _type "checkbox"; _id name; _name name; _class "form-check-input"; _value "true"
|
[ _type "checkbox"; _id name; _name name; _class "form-check-input"; _value "true"
|
||||||
if isChecked then _checked ]
|
if isChecked then _checked ]
|
||||||
|> input
|
|> input
|
||||||
label [ _class "form-check-label"; _for name ] [ str checkLabel ]
|
label [ _class "form-check-label"; _for name ] [ txt checkLabel ]
|
||||||
]
|
]
|
||||||
|
|
||||||
/// Create a select list of continents
|
/// Create a select list of continents
|
||||||
|
@ -46,26 +56,32 @@ let continentList attrs name (continents : Continent list) emptyLabel selectedVa
|
||||||
|> List.map (fun c ->
|
|> List.map (fun c ->
|
||||||
let theId = ContinentId.toString c.Id
|
let theId = ContinentId.toString c.Id
|
||||||
option [ _value theId; if theId = selectedValue then _selected ] [ str c.Name ])))
|
option [ _value theId; if theId = selectedValue then _selected ] [ str c.Name ])))
|
||||||
label [ _class (if isRequired then "jjj-required" else "jjj-label"); _for name ] [ rawText "Continent" ]
|
label [ _class (if isRequired then "jjj-required" else "jjj-label"); _for name ] [ txt "Continent" ]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
/// Create a submit button with the given icon and text
|
||||||
|
let submitButton icon text =
|
||||||
|
button [ _type "submit"; _class "btn btn-primary" ] [ i [ _class $"mdi mdi-%s{icon}" ] []; txt $" %s{text}" ]
|
||||||
|
|
||||||
|
/// An empty paragraph
|
||||||
|
let emptyP =
|
||||||
|
p [] [ txt " " ]
|
||||||
|
|
||||||
/// Register JavaScript code to run in the DOMContentLoaded event on the page
|
/// Register JavaScript code to run in the DOMContentLoaded event on the page
|
||||||
let jsOnLoad js =
|
let jsOnLoad js =
|
||||||
script [] [
|
script [] [ txt """document.addEventListener("DOMContentLoaded", function () { """; txt js; txt " })" ]
|
||||||
rawText """document.addEventListener("DOMContentLoaded", function () { """; rawText js; rawText " })"
|
|
||||||
]
|
|
||||||
|
|
||||||
/// Create a Markdown editor
|
/// Create a Markdown editor
|
||||||
let markdownEditor attrs name value editorLabel =
|
let markdownEditor attrs name value editorLabel =
|
||||||
div [ _class "col-12"; _id $"{name}EditRow" ] [
|
div [ _class "col-12"; _id $"{name}EditRow" ] [
|
||||||
nav [ _class "nav nav-pills pb-1" ] [
|
nav [ _class "nav nav-pills pb-1" ] [
|
||||||
button [ _type "button"; _id $"{name}EditButton"; _class "btn btn-primary btn-sm rounded-pill" ] [
|
button [ _type "button"; _id $"{name}EditButton"; _class "btn btn-primary btn-sm rounded-pill" ] [
|
||||||
rawText "Markdown"
|
txt "Markdown"
|
||||||
]
|
]
|
||||||
rawText " "
|
rawText " "
|
||||||
button [ _type "button"; _id $"{name}PreviewButton"
|
button [ _type "button"; _id $"{name}PreviewButton"
|
||||||
_class "btn btn-outline-secondary btn-sm rounded-pill" ] [
|
_class "btn btn-outline-secondary btn-sm rounded-pill" ] [
|
||||||
rawText "Preview"
|
txt "Preview"
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
section [ _id $"{name}Preview"; _class "jjj-not-shown jjj-markdown-preview px-2 pt-2"
|
section [ _id $"{name}Preview"; _class "jjj-not-shown jjj-markdown-preview px-2 pt-2"
|
||||||
|
@ -73,9 +89,9 @@ let markdownEditor attrs name value editorLabel =
|
||||||
div [ _id $"{name}Edit"; _class "form-floating jjj-shown" ] [
|
div [ _id $"{name}Edit"; _class "form-floating jjj-shown" ] [
|
||||||
textarea (List.append attrs
|
textarea (List.append attrs
|
||||||
[ _id name; _name name; _class "form-control jjj-markdown-editor"; _rows "10" ]) [
|
[ _id name; _name name; _class "form-control jjj-markdown-editor"; _rows "10" ]) [
|
||||||
rawText value
|
txt value
|
||||||
]
|
]
|
||||||
label [ _for name ] [ rawText editorLabel ]
|
label [ _for name ] [ txt editorLabel ]
|
||||||
]
|
]
|
||||||
jsOnLoad $"jjj.markdownOnLoad('{name}')"
|
jsOnLoad $"jjj.markdownOnLoad('{name}')"
|
||||||
]
|
]
|
||||||
|
@ -87,7 +103,7 @@ let collapsePanel header content =
|
||||||
h6 [ _class "card-title" ] [
|
h6 [ _class "card-title" ] [
|
||||||
// TODO: toggle collapse
|
// TODO: toggle collapse
|
||||||
//a [ _href "#"; _class "{ 'cp-c': collapsed, 'cp-o': !collapsed }"; @click.prevent="toggle">{{headerText}} ]
|
//a [ _href "#"; _class "{ 'cp-c': collapsed, 'cp-o': !collapsed }"; @click.prevent="toggle">{{headerText}} ]
|
||||||
rawText header
|
txt header
|
||||||
]
|
]
|
||||||
yield! content
|
yield! content
|
||||||
]
|
]
|
||||||
|
@ -99,7 +115,7 @@ let yesOrNo value =
|
||||||
|
|
||||||
/// Markdown as a raw HTML text node
|
/// Markdown as a raw HTML text node
|
||||||
let md2html value =
|
let md2html value =
|
||||||
rawText (MarkdownString.toHtml value)
|
(MarkdownString.toHtml >> txt) value
|
||||||
|
|
||||||
/// Display a citizen's contact information
|
/// Display a citizen's contact information
|
||||||
let contactInfo citizen isPublic =
|
let contactInfo citizen isPublic =
|
||||||
|
|
|
@ -5,558 +5,539 @@ open Giraffe.ViewEngine
|
||||||
/// The home page
|
/// The home page
|
||||||
let home =
|
let home =
|
||||||
article [] [
|
article [] [
|
||||||
p [] [ rawText " " ]
|
emptyP
|
||||||
p [] [
|
p [] [
|
||||||
rawText "Welcome to Jobs, Jobs, Jobs (AKA No Agenda Careers), where citizens of Gitmo Nation can assist "
|
txt "Welcome to Jobs, Jobs, Jobs (AKA No Agenda Careers), where citizens of Gitmo Nation can assist one "
|
||||||
rawText "one another in finding employment. This will enable them to continue providing value-for-value to "
|
txt "another in finding employment. This will enable them to continue providing value-for-value to Adam "
|
||||||
rawText "Adam and John, as they continue their work deconstructing the misinformation that passes for news "
|
txt "and John, as they continue their work deconstructing the misinformation that passes for news on a "
|
||||||
rawText "on a day-to-day basis."
|
txt "day-to-day basis."
|
||||||
]
|
]
|
||||||
p [] [
|
p [] [
|
||||||
rawText "Do you not understand the terms in the paragraph above? No worries; just head over to "
|
txt "Do you not understand the terms in the paragraph above? No worries; just head over to "
|
||||||
a [ _href "https://noagendashow.net"; _target "_blank"; _rel "noopener" ] [
|
a [ _href "https://noagendashow.net"; _target "_blank"; _rel "noopener" ] [
|
||||||
rawText "The Best Podcast in the Universe"
|
txt "The Best Podcast in the Universe"
|
||||||
]
|
]
|
||||||
rawText " "; em [] [ audioClip "thats-true" (rawText "(that’s true!)") ]
|
txt " "; em [] [ audioClip "thats-true" (txt "(that’s true!)") ]
|
||||||
rawText " and find out what you’re missing."
|
txt " and find out what you’re missing."
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
/// Online help / documentation
|
/// Online help / documentation
|
||||||
let howItWorks =
|
let howItWorks =
|
||||||
article [] [
|
pageWithTitle "How It Works" [
|
||||||
h3 [] [ rawText "How It Works" ]
|
p [] [ txt "TODO: convert and update for v3"]
|
||||||
p [] [ rawText "TODO: convert and update for v3"]
|
|
||||||
]
|
]
|
||||||
|
|
||||||
/// The privacy policy
|
/// The privacy policy
|
||||||
let privacyPolicy =
|
let privacyPolicy =
|
||||||
let appName = rawText "Jobs, Jobs, Jobs"
|
let appName = txt "Jobs, Jobs, Jobs"
|
||||||
article [] [
|
article [] [
|
||||||
h3 [] [ rawText "Privacy Policy" ]
|
h3 [] [ txt "Privacy Policy" ]
|
||||||
p [ _class "fst-italic" ] [ rawText "(as of December 27<sup>th</sup>, 2022)" ]
|
p [ _class "fst-italic" ] [ txt "(as of December 27<sup>th</sup>, 2022)" ]
|
||||||
|
|
||||||
p [] [
|
p [] [
|
||||||
appName; rawText " (“we,” “our,” or “us”) is committed to protecting "
|
appName; txt " (“we,” “our,” or “us”) is committed to protecting your "
|
||||||
rawText "your privacy. This Privacy Policy explains how your personal information is collected, used, and "
|
txt "privacy. This Privacy Policy explains how your personal information is collected, used, and disclosed "
|
||||||
rawText "disclosed by "; appName; rawText "."
|
txt "disclosed by "; appName; txt "."
|
||||||
]
|
]
|
||||||
p [] [
|
p [] [
|
||||||
rawText "This Privacy Policy applies to our website, and its associated subdomains (collectively, our "
|
txt "This Privacy Policy applies to our website, and its associated subdomains (collectively, our "
|
||||||
rawText "“Service”) alongside our application, "; appName; rawText ". By accessing or using "
|
txt "“Service”) alongside our application, "; appName; rawText ". By accessing or using our "
|
||||||
rawText "our Service, you signify that you have read, understood, and agree to our collection, storage, "
|
txt "Service, you signify that you have read, understood, and agree to our collection, storage, use, and "
|
||||||
rawText "use, and disclosure of your personal information as described in this Privacy Policy and our "
|
txt "disclosure of your personal information as described in this Privacy Policy and our Terms of Service."
|
||||||
rawText "Terms of Service."
|
|
||||||
]
|
]
|
||||||
|
|
||||||
h4 [] [ rawText "Definitions and key terms" ]
|
h4 [] [ txt "Definitions and key terms" ]
|
||||||
p [] [
|
p [] [
|
||||||
rawText "To help explain things as clearly as possible in this Privacy Policy, every time any of these "
|
txt "To help explain things as clearly as possible in this Privacy Policy, every time any of these terms "
|
||||||
rawText "terms are referenced, are strictly defined as:"
|
txt "are referenced, are strictly defined as:"
|
||||||
]
|
]
|
||||||
ul [] [
|
ul [] [
|
||||||
li [] [
|
li [] [
|
||||||
rawText "Cookie: small amount of data generated by a website and saved by your web browser. It is used "
|
txt "Cookie: small amount of data generated by a website and saved by your web browser. It is used to "
|
||||||
rawText "to identify your browser, provide analytics, remember information about you such as your "
|
txt "identify your browser, provide analytics, remember information about you such as your language "
|
||||||
rawText "language preference or login information."
|
txt "preference or login information."
|
||||||
]
|
]
|
||||||
li [] [
|
li [] [
|
||||||
rawText "Company: when this policy mentions “Company,” “we,” “us,” "
|
txt "Company: when this policy mentions “Company,” “we,” “us,” or "
|
||||||
rawText "or “our,” it refers to "; appName; rawText ", that is responsible for your "
|
txt "“our,” it refers to "; appName; txt ", that is responsible for your information under "
|
||||||
rawText "information under this Privacy Policy."
|
txt "this Privacy Policy."
|
||||||
]
|
]
|
||||||
li [] [
|
li [] [
|
||||||
rawText "Country: where "; appName; rawText " or the owners/founders of "; appName
|
txt "Country: where "; appName; txt " or the owners/founders of "; appName
|
||||||
rawText " are based, in this case is US."
|
txt " are based, in this case is US."
|
||||||
]
|
]
|
||||||
li [] [
|
li [] [
|
||||||
rawText "Customer: refers to the company, organization or person that signs up to use the "; appName
|
txt "Customer: refers to the company, organization or person that signs up to use the "; appName
|
||||||
rawText " Service to manage the relationships with your consumers or service users."
|
txt " Service to manage the relationships with your consumers or service users."
|
||||||
]
|
]
|
||||||
li [] [
|
li [] [
|
||||||
rawText "Device: any internet connected device such as a phone, tablet, computer or any other device "
|
txt "Device: any internet connected device such as a phone, tablet, computer or any other device that "
|
||||||
rawText "that can be used to visit "; appName; rawText " and use the services."
|
txt "can be used to visit "; appName; txt " and use the services."
|
||||||
]
|
]
|
||||||
li [] [
|
li [] [
|
||||||
rawText "IP address: Every device connected to the Internet is assigned a number known as an Internet "
|
txt "IP address: Every device connected to the Internet is assigned a number known as an Internet "
|
||||||
rawText "protocol (IP) address. These numbers are usually assigned in geographic blocks. An IP address "
|
txt "protocol (IP) address. These numbers are usually assigned in geographic blocks. An IP address can "
|
||||||
rawText "can often be used to identify the location from which a device is connecting to the Internet."
|
txt "often be used to identify the location from which a device is connecting to the Internet."
|
||||||
]
|
]
|
||||||
li [] [
|
li [] [
|
||||||
rawText "Personnel: refers to those individuals who are employed by "; appName; rawText " or are under "
|
txt "Personnel: refers to those individuals who are employed by "; appName; txt " or are under "
|
||||||
rawText "contract to perform a service on behalf of one of the parties."
|
txt "contract to perform a service on behalf of one of the parties."
|
||||||
]
|
]
|
||||||
li [] [
|
li [] [
|
||||||
rawText "Personal Data: any information that directly, indirectly, or in connection with other "
|
txt "Personal Data: any information that directly, indirectly, or in connection with other information "
|
||||||
rawText "information — including a personal identification number — allows for the identification or "
|
txt "— including a personal identification number — allows for the identification or identifiability "
|
||||||
rawText "identifiability of a natural person."
|
txt "of a natural person."
|
||||||
]
|
]
|
||||||
li [] [
|
li [] [
|
||||||
rawText "Service: refers to the service provided by "; appName; rawText " as described in the relative "
|
txt "Service: refers to the service provided by "; appName; txt " as described in the relative terms "
|
||||||
rawText "terms (if available) and on this platform."
|
txt "(if available) and on this platform."
|
||||||
]
|
]
|
||||||
li [] [
|
li [] [
|
||||||
rawText "Third-party service: refers to advertisers, contest sponsors, promotional and marketing "
|
txt "Third-party service: refers to advertisers, contest sponsors, promotional and marketing partners, "
|
||||||
rawText "partners, and others who provide our content or whose products or services we think may "
|
txt "and others who provide our content or whose products or services we think may interest you."
|
||||||
rawText "interest you."
|
|
||||||
]
|
]
|
||||||
li [] [
|
li [] [
|
||||||
rawText "Website: "; appName; rawText "’s site, which can be accessed via this URL: "
|
txt "Website: "; appName; txt "’s site, which can be accessed via this URL: "
|
||||||
a [ _href "/" ] [ rawText "https://noagendacareers.com/" ]
|
a [ _href "/" ] [ txt "https://noagendacareers.com/" ]
|
||||||
]
|
]
|
||||||
li [] [
|
li [] [
|
||||||
rawText "You: a person or entity that is registered with "; appName; rawText " to use the Services."
|
txt "You: a person or entity that is registered with "; appName; txt " to use the Services."
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
h4 [] [ rawText "What Information Do We Collect?" ]
|
h4 [] [ txt "What Information Do We Collect?" ]
|
||||||
p [] [
|
p [] [
|
||||||
rawText "We collect information from you when you visit our website, register on our site, or fill out a "
|
txt "We collect information from you when you visit our website, register on our site, or fill out a form."
|
||||||
rawText "form."
|
|
||||||
]
|
]
|
||||||
ul [] [
|
ul [] [
|
||||||
li [] [ rawText "Name / Username" ]
|
li [] [ txt "Name / Username" ]
|
||||||
li [] [ rawText "Coarse Geographic Location" ]
|
li [] [ txt "Coarse Geographic Location" ]
|
||||||
li [] [ rawText "Employment History" ]
|
li [] [ txt "Employment History" ]
|
||||||
li [] [ rawText "Job Listing Information" ]
|
li [] [ txt "Job Listing Information" ]
|
||||||
]
|
]
|
||||||
|
|
||||||
h4 [] [ rawText "How Do We Use The Information We Collect?" ]
|
h4 [] [ txt "How Do We Use The Information We Collect?" ]
|
||||||
p [] [ rawText "Any of the information we collect from you may be used in one of the following ways:" ]
|
p [] [ txt "Any of the information we collect from you may be used in one of the following ways:" ]
|
||||||
ul[] [
|
|
||||||
li [] [
|
|
||||||
rawText "To personalize your experience (your information helps us to better respond to your "
|
|
||||||
rawText "individual needs)"
|
|
||||||
]
|
|
||||||
li [] [
|
|
||||||
rawText "To improve our website (we continually strive to improve our website offerings based on the "
|
|
||||||
rawText "information and feedback we receive from you)"
|
|
||||||
]
|
|
||||||
li [] [
|
|
||||||
rawText "To improve customer service (your information helps us to more effectively respond to your "
|
|
||||||
rawText "customer service requests and support needs)"
|
|
||||||
]
|
|
||||||
]
|
|
||||||
|
|
||||||
h4 [] [ rawText "When does "; appName; rawText " use end user information from third parties?" ]
|
|
||||||
p [] [
|
|
||||||
appName; rawText " will collect End User Data necessary to provide the "; appName
|
|
||||||
rawText " services to our customers."
|
|
||||||
]
|
|
||||||
p [] [
|
|
||||||
rawText "End users may voluntarily provide us with information they have made available on social media "
|
|
||||||
rawText "websites. If you provide us with any such information, we may collect publicly available "
|
|
||||||
rawText "information from the social media websites you have indicated. You can control how much of your "
|
|
||||||
rawText "information social media websites make public by visiting these websites and changing your "
|
|
||||||
rawText "privacy settings."
|
|
||||||
]
|
|
||||||
|
|
||||||
h4 [] [ rawText "When does "; appName; rawText " use customer information from third parties?" ]
|
|
||||||
p [] [ rawText "We do not utilize third party information apart from the end-user data described above." ]
|
|
||||||
|
|
||||||
h4 [] [ rawText "Do we share the information we collect with third parties?" ]
|
|
||||||
p [] [
|
|
||||||
rawText "We may disclose personal and non-personal information about you to government or law enforcement "
|
|
||||||
rawText "officials or private parties as we, in our sole discretion, believe necessary or appropriate in "
|
|
||||||
rawText "order to respond to claims, legal process (including subpoenas), to protect our rights and "
|
|
||||||
rawText "interests or those of a third party, the safety of the public or any person, to prevent or stop "
|
|
||||||
rawText "any illegal, unethical, or legally actionable activity, or to otherwise comply with applicable "
|
|
||||||
rawText "court orders, laws, rules and regulations."
|
|
||||||
]
|
|
||||||
|
|
||||||
h4 [] [ rawText "Where and when is information collected from customers and end users?" ]
|
|
||||||
p [] [
|
|
||||||
appName; rawText " will collect personal information that you submit to us. We may also receive personal "
|
|
||||||
rawText "information about you from third parties as described above."
|
|
||||||
]
|
|
||||||
|
|
||||||
h4 [] [ rawText "How Do We Use Your E-mail Address?" ]
|
|
||||||
p [] [
|
|
||||||
appName; rawText " uses your e-mail address to identify you, along with your password, as an authorized "
|
|
||||||
rawText "user of this site. E-mail addresses are verified via a time-sensitive link, and may also be used "
|
|
||||||
rawText "to send password reset authorization codes. We do not display this e-mail address to users. If "
|
|
||||||
rawText "you choose to add an e-mail address as a contact type, that e-mail address will be visible to "
|
|
||||||
rawText "other authorized users."
|
|
||||||
]
|
|
||||||
|
|
||||||
h4 [] [ rawText "How Long Do We Keep Your Information?" ]
|
|
||||||
p [] [
|
|
||||||
rawText "We keep your information only so long as we need it to provide "; appName; rawText " to you and "
|
|
||||||
rawText "fulfill the purposes described in this policy. When we no longer need to use your information and "
|
|
||||||
rawText "there is no need for us to keep it to comply with our legal or regulatory obligations, we’ll "
|
|
||||||
rawText "either remove it from our systems or depersonalize it so that we can’t identify you."
|
|
||||||
]
|
|
||||||
|
|
||||||
h4 [] [ rawText "How Do We Protect Your Information?" ]
|
|
||||||
p [] [
|
|
||||||
rawText "We implement a variety of security measures to maintain the safety of your personal information "
|
|
||||||
rawText "when you enter, submit, or access your personal information. We mandate the use of a secure "
|
|
||||||
rawText "server. We cannot, however, ensure or warrant the absolute security of any information you "
|
|
||||||
rawText "transmit to "; appName; rawText " or guarantee that your information on the Service may not be "
|
|
||||||
rawText "accessed, disclosed, altered, or destroyed by a breach of any of our physical, technical, or "
|
|
||||||
rawText "managerial safeguards."
|
|
||||||
]
|
|
||||||
|
|
||||||
h4 [] [ rawText "Could my information be transferred to other countries?" ]
|
|
||||||
p [] [
|
|
||||||
appName; rawText " is hosted in the US. Information collected via our website may be viewed and hosted "
|
|
||||||
rawText "anywhere in the world, including countries that may not have laws of general applicability "
|
|
||||||
rawText "regulating the use and transfer of such data. To the fullest extent allowed by applicable law, by "
|
|
||||||
rawText "using any of the above, you voluntarily consent to the trans-border transfer and hosting of such "
|
|
||||||
rawText "information."
|
|
||||||
]
|
|
||||||
|
|
||||||
h4 [] [ rawText "Is the information collected through the "; appName; rawText " Service secure?" ]
|
|
||||||
p [] [
|
|
||||||
rawText "We take precautions to protect the security of your information. We have physical, electronic, "
|
|
||||||
rawText "and managerial procedures to help safeguard, prevent unauthorized access, maintain data security, "
|
|
||||||
rawText "and correctly use your information. However, neither people nor security systems are foolproof, "
|
|
||||||
rawText "including encryption systems. In addition, people can commit intentional crimes, make mistakes, "
|
|
||||||
rawText "or fail to follow policies. Therefore, while we use reasonable efforts to protect your personal "
|
|
||||||
rawText "information, we cannot guarantee its absolute security. If applicable law imposes any "
|
|
||||||
rawText "non-disclaimable duty to protect your personal information, you agree that intentional misconduct "
|
|
||||||
rawText "will be the standards used to measure our compliance with that duty."
|
|
||||||
]
|
|
||||||
|
|
||||||
h4 [] [ rawText "Can I update or correct my information?" ]
|
|
||||||
p [] [
|
|
||||||
rawText "The rights you have to request updates or corrections to the information "; appName
|
|
||||||
rawText " collects depend on your relationship with "; appName; rawText "."
|
|
||||||
]
|
|
||||||
p [] [
|
|
||||||
rawText "Customers have the right to request the restriction of certain uses and disclosures of personally "
|
|
||||||
rawText "identifiable information as follows. You can contact us in order to (1) update or correct your "
|
|
||||||
rawText "personally identifiable information, or (3) delete the personally identifiable information "
|
|
||||||
rawText "maintained about you on our systems (subject to the following paragraph), by cancelling your "
|
|
||||||
rawText "account. Such updates, corrections, changes and deletions will have no effect on other "
|
|
||||||
rawText "information that we maintain in accordance with this Privacy Policy prior to such update, "
|
|
||||||
rawText "correction, change, or deletion. You are responsible for maintaining the secrecy of your unique "
|
|
||||||
rawText "password and account information at all times."
|
|
||||||
]
|
|
||||||
p [] [
|
|
||||||
appName; rawText " also provides ways for users to modify or remove the information we have collected from "
|
|
||||||
rawText "them from the application; these actions will have the same effect as contacting us to modify or "
|
|
||||||
rawText "remove data."
|
|
||||||
]
|
|
||||||
p [] [
|
|
||||||
rawText "You should be aware that it is not technologically possible to remove each and every record of "
|
|
||||||
rawText "the information you have provided to us from our system. The need to back up our systems to "
|
|
||||||
rawText "protect information from inadvertent loss means that a copy of your information may exist in a "
|
|
||||||
rawText "non-erasable form that will be difficult or impossible for us to locate. Promptly after receiving "
|
|
||||||
rawText "your request, all personal information stored in databases we actively use, and other readily "
|
|
||||||
rawText "searchable media will be updated, corrected, changed, or deleted, as appropriate, as soon as and "
|
|
||||||
rawText "to the extent reasonably and technically practicable."
|
|
||||||
]
|
|
||||||
p [] [
|
|
||||||
rawText "If you are an end user and wish to update, delete, or receive any information we have about you, "
|
|
||||||
rawText "you may do so by contacting the organization of which you are a customer."
|
|
||||||
]
|
|
||||||
|
|
||||||
h4 [] [ rawText "Governing Law" ]
|
|
||||||
p [] [
|
|
||||||
rawText "This Privacy Policy is governed by the laws of US without regard to its conflict of laws "
|
|
||||||
rawText "provision. You consent to the exclusive jurisdiction of the courts in connection with any action "
|
|
||||||
rawText "or dispute arising between the parties under or in connection with this Privacy Policy except for "
|
|
||||||
rawText "those individuals who may have rights to make claims under Privacy Shield, or the Swiss-US "
|
|
||||||
rawText "framework."
|
|
||||||
]
|
|
||||||
p [] [
|
|
||||||
rawText "The laws of US, excluding its conflicts of law rules, shall govern this Agreement and your use of "
|
|
||||||
rawText "the website. Your use of the website may also be subject to other local, state, national, or "
|
|
||||||
rawText "international laws."
|
|
||||||
]
|
|
||||||
p [] [
|
|
||||||
rawText "By using "; appName; rawText " or contacting us directly, you signify your acceptance of this "
|
|
||||||
rawText "Privacy Policy. If you do not agree to this Privacy Policy, you should not engage with our "
|
|
||||||
rawText "website, or use our services. Continued use of the website, direct engagement with us, or "
|
|
||||||
rawText "following the posting of changes to this Privacy Policy that do not significantly affect the use "
|
|
||||||
rawText "or disclosure of your personal information will mean that you accept those changes."
|
|
||||||
]
|
|
||||||
|
|
||||||
h4 [] [ rawText "Your Consent" ]
|
|
||||||
p [] [
|
|
||||||
rawText "We’ve updated our Privacy Policy to provide you with complete transparency into what is "
|
|
||||||
rawText "being set when you visit our site and how it’s being used. By using our website, "
|
|
||||||
rawText "registering an account, or making a purchase, you hereby consent to our Privacy Policy and agree "
|
|
||||||
rawText "to its terms."
|
|
||||||
]
|
|
||||||
|
|
||||||
h4 [] [ rawText "Links to Other Websites" ]
|
|
||||||
p [] [
|
|
||||||
rawText "This Privacy Policy applies only to the Services. The Services may contain links to other "
|
|
||||||
rawText "websites not operated or controlled by "; appName; rawText ". We are not responsible for the "
|
|
||||||
rawText "content, accuracy or opinions expressed in such websites, and such websites are not investigated, "
|
|
||||||
rawText "monitored or checked for accuracy or completeness by us. Please remember that when you use a link "
|
|
||||||
rawText "to go from the Services to another website, our Privacy Policy is no longer in effect. Your "
|
|
||||||
rawText "browsing and interaction on any other website, including those that have a link on our platform, "
|
|
||||||
rawText "is subject to that website’s own rules and policies. Such third parties may use their own "
|
|
||||||
rawText "cookies or other methods to collect information about you."
|
|
||||||
]
|
|
||||||
|
|
||||||
h4 [] [ rawText "Cookies" ]
|
|
||||||
p [] [
|
|
||||||
appName; rawText " uses a session Cookie to identify an active, logged-on session. This Cookie is removed "
|
|
||||||
rawText "when You explicitly log off; is not accessible via script; and must be transferred over a "
|
|
||||||
rawText "secured, encrypted connection."
|
|
||||||
]
|
|
||||||
p [] [
|
|
||||||
appName; rawText " uses no persistent or Third-Party Cookies."
|
|
||||||
]
|
|
||||||
|
|
||||||
h4 [] [ rawText "Kids’ Privacy" ]
|
|
||||||
p [] [
|
|
||||||
rawText "We do not address anyone under the age of 13. We do not knowingly collect personally identifiable "
|
|
||||||
rawText "information from anyone under the age of 13. If You are a parent or guardian and You are aware "
|
|
||||||
rawText "that Your child has provided Us with Personal Data, please contact Us. If We become aware that We "
|
|
||||||
rawText "have collected Personal Data from anyone under the age of 13 without verification of parental "
|
|
||||||
rawText "consent, We take steps to remove that information from Our servers."
|
|
||||||
]
|
|
||||||
|
|
||||||
h4 [] [ rawText "Changes To Our Privacy Policy" ]
|
|
||||||
p [] [
|
|
||||||
rawText "We may change our Service and policies, and we may need to make changes to this Privacy Policy so "
|
|
||||||
rawText "that they accurately reflect our Service and policies. Unless otherwise required by law, we will "
|
|
||||||
rawText "notify you (for example, through our Service) before we make changes to this Privacy Policy and "
|
|
||||||
rawText "give you an opportunity to review them before they go into effect. Then, if you continue to use "
|
|
||||||
rawText "the Service, you will be bound by the updated Privacy Policy. If you do not want to agree to this "
|
|
||||||
rawText "or any updated Privacy Policy, you can delete your account."
|
|
||||||
]
|
|
||||||
|
|
||||||
h4 [] [ rawText "Third-Party Services" ]
|
|
||||||
p [] [
|
|
||||||
rawText "We may display, include or make available third-party content (including data, information, "
|
|
||||||
rawText "applications and other products services) or provide links to third-party websites or services "
|
|
||||||
rawText "(“Third-Party Services”)."
|
|
||||||
]
|
|
||||||
p [] [
|
|
||||||
rawText "You acknowledge and agree that "; appName; rawText " shall not be responsible for any Third-Party "
|
|
||||||
rawText "Services, including their accuracy, completeness, timeliness, validity, copyright compliance, "
|
|
||||||
rawText "legality, decency, quality or any other aspect thereof. "; appName; rawText " does not assume and "
|
|
||||||
rawText "shall not have any liability or responsibility to you or any other person or entity for any "
|
|
||||||
rawText "Third-Party Services."
|
|
||||||
]
|
|
||||||
p [] [
|
|
||||||
rawText "Third-Party Services and links thereto are provided solely as a convenience to you and you access "
|
|
||||||
rawText "and use them entirely at your own risk and subject to such third parties’ terms and "
|
|
||||||
rawText "conditions."
|
|
||||||
]
|
|
||||||
|
|
||||||
h4 [] [ rawText "Tracking Technologies" ]
|
|
||||||
p [] [ appName; rawText " does not use any tracking technologies." ]
|
|
||||||
|
|
||||||
h4 [] [ rawText "Information about General Data Protection Regulation (GDPR)" ]
|
|
||||||
p [] [
|
|
||||||
rawText "We may be collecting and using information from you if you are from the European Economic Area "
|
|
||||||
rawText "(EEA), and in this section of our Privacy Policy we are going to explain exactly how and why is "
|
|
||||||
rawText "this data collected, and how we maintain this data under protection from being replicated or used "
|
|
||||||
rawText "in the wrong way."
|
|
||||||
]
|
|
||||||
|
|
||||||
h5 [] [ rawText "What is GDPR?" ]
|
|
||||||
p [] [
|
|
||||||
rawText "GDPR is an EU-wide privacy and data protection law that regulates how EU residents’ data is "
|
|
||||||
rawText "protected by companies and enhances the control the EU residents have, over their personal data."
|
|
||||||
]
|
|
||||||
p [] [
|
|
||||||
rawText "The GDPR is relevant to any globally operating company and not just the EU-based businesses and "
|
|
||||||
rawText "EU residents. Our customers’ data is important irrespective of where they are located, which is "
|
|
||||||
rawText "why we have implemented GDPR controls as our baseline standard for all our operations worldwide."
|
|
||||||
]
|
|
||||||
|
|
||||||
h5 [] [ rawText "What is personal data?" ]
|
|
||||||
p [] [
|
|
||||||
rawText "Any data that relates to an identifiable or identified individual. GDPR covers a broad spectrum "
|
|
||||||
rawText "of information that could be used on its own, or in combination with other pieces of information, "
|
|
||||||
rawText "to identify a person. Personal data extends beyond a person’s name or email address. Some "
|
|
||||||
rawText "examples include financial information, political opinions, genetic data, biometric data, IP "
|
|
||||||
rawText "addresses, physical address, sexual orientation, and ethnicity."
|
|
||||||
]
|
|
||||||
p [] [ rawText "The Data Protection Principles include requirements such as:" ]
|
|
||||||
ul [] [
|
ul [] [
|
||||||
li [] [
|
li [] [
|
||||||
rawText "Personal data collected must be processed in a fair, legal, and transparent way and should "
|
txt "To personalize your experience (your information helps us to better respond to your individual "
|
||||||
rawText "only be used in a way that a person would reasonably expect."
|
txt "needs)"
|
||||||
]
|
]
|
||||||
li [] [
|
li [] [
|
||||||
rawText "Personal data should only be collected to fulfil a specific purpose and it should only be "
|
txt "To improve our website (we continually strive to improve our website offerings based on the "
|
||||||
rawText "used for that purpose. Organizations must specify why they need the personal data when they "
|
txt "information and feedback we receive from you)"
|
||||||
rawText "collect it."
|
|
||||||
]
|
]
|
||||||
li [] [ rawText "Personal data should be held no longer than necessary to fulfil its purpose." ]
|
|
||||||
li [] [
|
li [] [
|
||||||
rawText "People covered by the GDPR have the right to access their own personal data. They can also "
|
txt "To improve customer service (your information helps us to more effectively respond to your "
|
||||||
rawText "request a copy of their data, and that their data be updated, deleted, restricted, or moved "
|
txt "customer service requests and support needs)"
|
||||||
rawText "to another organization."
|
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
h5 [] [ rawText "Why is GDPR important?" ]
|
h4 [] [ txt "When does "; appName; txt " use end user information from third parties?" ]
|
||||||
p [] [
|
p [] [
|
||||||
rawText "GDPR adds some new requirements regarding how companies should protect individuals’ "
|
appName; txt " will collect End User Data necessary to provide the "; appName
|
||||||
rawText "personal data that they collect and process. It also raises the stakes for compliance by "
|
txt " services to our customers."
|
||||||
rawText "increasing enforcement and imposing greater fines for breach. Beyond these facts, it’s "
|
]
|
||||||
rawText "simply the right thing to do. At "; appName; rawText " we strongly believe that your data privacy "
|
p [] [
|
||||||
rawText "is very important and we already have solid security and privacy practices in place that go "
|
txt "End users may voluntarily provide us with information they have made available on social media "
|
||||||
rawText "beyond the requirements of this regulation."
|
txt "websites. If you provide us with any such information, we may collect publicly available information "
|
||||||
|
txt "from the social media websites you have indicated. You can control how much of your information "
|
||||||
|
txt "social media websites make public by visiting these websites and changing your privacy settings."
|
||||||
]
|
]
|
||||||
|
|
||||||
h5 [] [ rawText "Individual Data Subject’s Rights - Data Access, Portability, and Deletion" ]
|
h4 [] [ txt "When does "; appName; txt " use customer information from third parties?" ]
|
||||||
|
p [] [ txt "We do not utilize third party information apart from the end-user data described above." ]
|
||||||
|
|
||||||
|
h4 [] [ txt "Do we share the information we collect with third parties?" ]
|
||||||
p [] [
|
p [] [
|
||||||
rawText "We are committed to helping our customers meet the data subject rights requirements of GDPR. "
|
txt "We may disclose personal and non-personal information about you to government or law enforcement "
|
||||||
appName; rawText " processes or stores all personal data in fully vetted, DPA compliant vendors. We do "
|
txt "officials or private parties as we, in our sole discretion, believe necessary or appropriate in order "
|
||||||
rawText "store all conversation and personal data for up to 6 years unless your account is deleted. In "
|
txt "to respond to claims, legal process (including subpoenas), to protect our rights and interests or "
|
||||||
rawText "which case, we dispose of all data in accordance with our Terms of Service and Privacy Policy, "
|
txt "those of a third party, the safety of the public or any person, to prevent or stop any illegal, "
|
||||||
rawText "but we will not hold it longer than 60 days."
|
txt "unethical, or legally actionable activity, or to otherwise comply with applicable court orders, laws, "
|
||||||
]
|
txt "rules and regulations."
|
||||||
p [] [
|
|
||||||
rawText "We are aware that if you are working with EU customers, you need to be able to provide them with "
|
|
||||||
rawText "the ability to access, update, retrieve and remove personal data. We got you! We’ve been "
|
|
||||||
rawText "set up as self service from the start and have always given you access to your data. Our customer "
|
|
||||||
rawText "support team is here for you to answer any questions you might have about working with the API."
|
|
||||||
]
|
]
|
||||||
|
|
||||||
h4 [] [ rawText "California Residents" ]
|
h4 [] [ txt "Where and when is information collected from customers and end users?" ]
|
||||||
p [] [
|
p [] [
|
||||||
rawText "The California Consumer Privacy Act (CCPA) requires us to disclose categories of Personal "
|
appName; txt " will collect personal information that you submit to us. We may also receive personal "
|
||||||
rawText "Information we collect and how we use it, the categories of sources from whom we collect Personal "
|
txt "information about you from third parties as described above."
|
||||||
rawText "Information, and the third parties with whom we share it, which we have explained above."
|
]
|
||||||
|
|
||||||
|
h4 [] [ txt "How Do We Use Your E-mail Address?" ]
|
||||||
|
p [] [
|
||||||
|
appName; txt " uses your e-mail address to identify you, along with your password, as an authorized user "
|
||||||
|
txt "of this site. E-mail addresses are verified via a time-sensitive link, and may also be used to send "
|
||||||
|
txt "password reset authorization codes. We do not display this e-mail address to users. If you choose to "
|
||||||
|
txt "add an e-mail address as a contact type, that e-mail address will be visible to other authorized "
|
||||||
|
txt "users."
|
||||||
|
]
|
||||||
|
|
||||||
|
h4 [] [ txt "How Long Do We Keep Your Information?" ]
|
||||||
|
p [] [
|
||||||
|
txt "We keep your information only so long as we need it to provide "; appName; txt " to you and fulfill "
|
||||||
|
txt "the purposes described in this policy. When we no longer need to use your information and there is no "
|
||||||
|
txt "need for us to keep it to comply with our legal or regulatory obligations, we’ll either remove it "
|
||||||
|
txt "from our systems or depersonalize it so that we can’t identify you."
|
||||||
|
]
|
||||||
|
|
||||||
|
h4 [] [ txt "How Do We Protect Your Information?" ]
|
||||||
|
p [] [
|
||||||
|
txt "We implement a variety of security measures to maintain the safety of your personal information when "
|
||||||
|
txt "you enter, submit, or access your personal information. We mandate the use of a secure server. We "
|
||||||
|
txt "cannot, however, ensure or warrant the absolute security of any information you transmit to "; appName
|
||||||
|
txt " or guarantee that your information on the Service may not be accessed, disclosed, altered, or "
|
||||||
|
txt "destroyed by a breach of any of our physical, technical, or managerial safeguards."
|
||||||
|
]
|
||||||
|
|
||||||
|
h4 [] [ txt "Could my information be transferred to other countries?" ]
|
||||||
|
p [] [
|
||||||
|
appName; txt " is hosted in the US. Information collected via our website may be viewed and hosted "
|
||||||
|
txt "anywhere in the world, including countries that may not have laws of general applicability regulating "
|
||||||
|
txt "the use and transfer of such data. To the fullest extent allowed by applicable law, by using any of "
|
||||||
|
txt "the above, you voluntarily consent to the trans-border transfer and hosting of such information."
|
||||||
|
]
|
||||||
|
|
||||||
|
h4 [] [ txt "Is the information collected through the "; appName; txt " Service secure?" ]
|
||||||
|
p [] [
|
||||||
|
txt "We take precautions to protect the security of your information. We have physical, electronic, and "
|
||||||
|
txt "managerial procedures to help safeguard, prevent unauthorized access, maintain data security, and "
|
||||||
|
txt "correctly use your information. However, neither people nor security systems are foolproof, including "
|
||||||
|
txt "encryption systems. In addition, people can commit intentional crimes, make mistakes, or fail to "
|
||||||
|
txt "follow policies. Therefore, while we use reasonable efforts to protect your personal information, we "
|
||||||
|
txt "cannot guarantee its absolute security. If applicable law imposes any non-disclaimable duty to "
|
||||||
|
txt "protect your personal information, you agree that intentional misconduct will be the standards used "
|
||||||
|
txt "to measure our compliance with that duty."
|
||||||
|
]
|
||||||
|
|
||||||
|
h4 [] [ txt "Can I update or correct my information?" ]
|
||||||
|
p [] [
|
||||||
|
txt "The rights you have to request updates or corrections to the information "; appName
|
||||||
|
txt " collects depend on your relationship with "; appName; txt "."
|
||||||
]
|
]
|
||||||
p [] [
|
p [] [
|
||||||
rawText "We are also required to communicate information about rights California residents have under "
|
txt "Customers have the right to request the restriction of certain uses and disclosures of personally "
|
||||||
rawText "California law. You may exercise the following rights:"
|
txt "identifiable information as follows. You can contact us in order to (1) update or correct your "
|
||||||
|
txt "personally identifiable information, or (3) delete the personally identifiable information maintained "
|
||||||
|
txt "about you on our systems (subject to the following paragraph), by cancelling your account. Such "
|
||||||
|
txt "updates, corrections, changes and deletions will have no effect on other information that we maintain "
|
||||||
|
txt "in accordance with this Privacy Policy prior to such update, correction, change, or deletion. You are "
|
||||||
|
txt "responsible for maintaining the secrecy of your unique password and account information at all times."
|
||||||
|
]
|
||||||
|
p [] [
|
||||||
|
appName; txt " also provides ways for users to modify or remove the information we have collected from "
|
||||||
|
txt "them from the application; these actions will have the same effect as contacting us to modify or "
|
||||||
|
txt "remove data."
|
||||||
|
]
|
||||||
|
p [] [
|
||||||
|
txt "You should be aware that it is not technologically possible to remove each and every record of the "
|
||||||
|
txt "information you have provided to us from our system. The need to back up our systems to protect "
|
||||||
|
txt "information from inadvertent loss means that a copy of your information may exist in a non-erasable "
|
||||||
|
txt "form that will be difficult or impossible for us to locate. Promptly after receiving your request, "
|
||||||
|
txt "all personal information stored in databases we actively use, and other readily searchable media will "
|
||||||
|
txt "be updated, corrected, changed, or deleted, as appropriate, as soon as and to the extent reasonably "
|
||||||
|
txt "and technically practicable."
|
||||||
|
]
|
||||||
|
p [] [
|
||||||
|
txt "If you are an end user and wish to update, delete, or receive any information we have about you, you "
|
||||||
|
txt "may do so by contacting the organization of which you are a customer."
|
||||||
|
]
|
||||||
|
|
||||||
|
h4 [] [ txt "Governing Law" ]
|
||||||
|
p [] [
|
||||||
|
txt "This Privacy Policy is governed by the laws of US without regard to its conflict of laws provision. "
|
||||||
|
txt "You consent to the exclusive jurisdiction of the courts in connection with any action or dispute "
|
||||||
|
txt "arising between the parties under or in connection with this Privacy Policy except for those "
|
||||||
|
txt "individuals who may have rights to make claims under Privacy Shield, or the Swiss-US framework."
|
||||||
|
]
|
||||||
|
p [] [
|
||||||
|
txt "The laws of US, excluding its conflicts of law rules, shall govern this Agreement and your use of the "
|
||||||
|
txt "website. Your use of the website may also be subject to other local, state, national, or "
|
||||||
|
txt "international laws."
|
||||||
|
]
|
||||||
|
p [] [
|
||||||
|
txt "By using "; appName; txt " or contacting us directly, you signify your acceptance of this Privacy "
|
||||||
|
txt "Policy. If you do not agree to this Privacy Policy, you should not engage with our website, or use "
|
||||||
|
txt "our services. Continued use of the website, direct engagement with us, or following the posting of "
|
||||||
|
txt "changes to this Privacy Policy that do not significantly affect the use or disclosure of your "
|
||||||
|
txt "personal information will mean that you accept those changes."
|
||||||
|
]
|
||||||
|
|
||||||
|
h4 [] [ txt "Your Consent" ]
|
||||||
|
p [] [
|
||||||
|
txt "We’ve updated our Privacy Policy to provide you with complete transparency into what is being "
|
||||||
|
txt "set when you visit our site and how it’s being used. By using our website, registering an "
|
||||||
|
txt "account, or making a purchase, you hereby consent to our Privacy Policy and agree to its terms."
|
||||||
|
]
|
||||||
|
|
||||||
|
h4 [] [ txt "Links to Other Websites" ]
|
||||||
|
p [] [
|
||||||
|
txt "This Privacy Policy applies only to the Services. The Services may contain links to other websites "
|
||||||
|
txt "not operated or controlled by "; appName; txt ". We are not responsible for the content, accuracy, or "
|
||||||
|
txt "opinions expressed in such websites, and such websites are not investigated, monitored, or checked "
|
||||||
|
txt "for accuracy or completeness by us. Please remember that when you use a link to from the Services to "
|
||||||
|
txt "another website, our Privacy Policy is no longer in effect. Your browsing and interaction on any "
|
||||||
|
txt "other website, including those that have a link on our platform, is subject to that website’s own "
|
||||||
|
txt "rules and policies. Such third parties may use their own cookies or other methods to collect "
|
||||||
|
txt "information about you."
|
||||||
|
]
|
||||||
|
|
||||||
|
h4 [] [ txt "Cookies" ]
|
||||||
|
p [] [
|
||||||
|
appName; txt " uses a session Cookie to identify an active, logged-on session. This Cookie is removed when "
|
||||||
|
txt "when You explicitly log off; is not accessible via script; and must be transferred over a secured, "
|
||||||
|
txt "encrypted connection."
|
||||||
|
]
|
||||||
|
p [] [ appName; txt " uses no persistent or Third-Party Cookies." ]
|
||||||
|
|
||||||
|
h4 [] [ txt "Kids’ Privacy" ]
|
||||||
|
p [] [
|
||||||
|
txt "We do not address anyone under the age of 13. We do not knowingly collect personally identifiable "
|
||||||
|
txt "information from anyone under the age of 13. If You are a parent or guardian and You are aware that "
|
||||||
|
txt "Your child has provided Us with Personal Data, please contact Us. If We become aware that We have "
|
||||||
|
txt "collected Personal Data from anyone under the age of 13 without verification of parental consent, We "
|
||||||
|
txt "take steps to remove that information from Our servers."
|
||||||
|
]
|
||||||
|
|
||||||
|
h4 [] [ txt "Changes To Our Privacy Policy" ]
|
||||||
|
p [] [
|
||||||
|
txt "We may change our Service and policies, and we may need to make changes to this Privacy Policy so "
|
||||||
|
txt "that they accurately reflect our Service and policies. Unless otherwise required by law, we will "
|
||||||
|
txt "notify you (for example, through our Service) before we make changes to this Privacy Policy and give "
|
||||||
|
txt "you an opportunity to review them before they go into effect. Then, if you continue to use the "
|
||||||
|
txt "Service, you will be bound by the updated Privacy Policy. If you do not want to agree to this or any "
|
||||||
|
txt "updated Privacy Policy, you can delete your account."
|
||||||
|
]
|
||||||
|
|
||||||
|
h4 [] [ txt "Third-Party Services" ]
|
||||||
|
p [] [
|
||||||
|
txt "We may display, include or make available third-party content (including data, information, "
|
||||||
|
txt "applications and other products services) or provide links to third-party websites or services "
|
||||||
|
txt "(“Third-Party Services”)."
|
||||||
|
]
|
||||||
|
p [] [
|
||||||
|
txt "You acknowledge and agree that "; appName; txt " shall not be responsible for any Third-Party "
|
||||||
|
txt "Services, including their accuracy, completeness, timeliness, validity, copyright compliance, "
|
||||||
|
txt "legality, decency, quality or any other aspect thereof. "; appName; txt " does not assume and shall "
|
||||||
|
txt "not have any liability or responsibility to you or any other person or entity for any Third-Party "
|
||||||
|
txt "Services."
|
||||||
|
]
|
||||||
|
p [] [
|
||||||
|
txt "Third-Party Services and links thereto are provided solely as a convenience to you and you access and "
|
||||||
|
txt "use them entirely at your own risk and subject to such third parties’ terms and conditions."
|
||||||
|
]
|
||||||
|
|
||||||
|
h4 [] [ txt "Tracking Technologies" ]
|
||||||
|
p [] [ appName; txt " does not use any tracking technologies." ]
|
||||||
|
|
||||||
|
h4 [] [ txt "Information about General Data Protection Regulation (GDPR)" ]
|
||||||
|
p [] [
|
||||||
|
txt "We may be collecting and using information from you if you are from the European Economic Area (EEA), "
|
||||||
|
txt "and in this section of our Privacy Policy we are going to explain exactly how and why is this data "
|
||||||
|
txt "collected, and how we maintain this data under protection from being replicated or used in the wrong "
|
||||||
|
txt "way."
|
||||||
|
]
|
||||||
|
|
||||||
|
h5 [] [ txt "What is GDPR?" ]
|
||||||
|
p [] [
|
||||||
|
txt "GDPR is an EU-wide privacy and data protection law that regulates how EU residents’ data is "
|
||||||
|
txt "protected by companies and enhances the control the EU residents have, over their personal data."
|
||||||
|
]
|
||||||
|
p [] [
|
||||||
|
txt "The GDPR is relevant to any globally operating company and not just the EU-based businesses and EU "
|
||||||
|
txt "residents. Our customers’ data is important irrespective of where they are located, which is why we "
|
||||||
|
txt "have implemented GDPR controls as our baseline standard for all our operations worldwide."
|
||||||
|
]
|
||||||
|
|
||||||
|
h5 [] [ txt "What is personal data?" ]
|
||||||
|
p [] [
|
||||||
|
txt "Any data that relates to an identifiable or identified individual. GDPR covers a broad spectrum of "
|
||||||
|
txt "information that could be used on its own, or in combination with other pieces of information, to "
|
||||||
|
txt "identify a person. Personal data extends beyond a person’s name or email address. Some examples "
|
||||||
|
txt "include financial information, political opinions, genetic data, biometric data, IP addresses, "
|
||||||
|
txt "physical address, sexual orientation, and ethnicity."
|
||||||
|
]
|
||||||
|
p [] [ txt "The Data Protection Principles include requirements such as:" ]
|
||||||
|
ul [] [
|
||||||
|
li [] [
|
||||||
|
txt "Personal data collected must be processed in a fair, legal, and transparent way and should only "
|
||||||
|
txt "be used in a way that a person would reasonably expect."
|
||||||
|
]
|
||||||
|
li [] [
|
||||||
|
txt "Personal data should only be collected to fulfil a specific purpose and it should only be used "
|
||||||
|
txt "for that purpose. Organizations must specify why they need the personal data when they collect it."
|
||||||
|
]
|
||||||
|
li [] [ txt "Personal data should be held no longer than necessary to fulfil its purpose." ]
|
||||||
|
li [] [
|
||||||
|
txt "People covered by the GDPR have the right to access their own personal data. They can also "
|
||||||
|
txt "request a copy of their data, and that their data be updated, deleted, restricted, or moved to "
|
||||||
|
txt "another organization."
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
h5 [] [ txt "Why is GDPR important?" ]
|
||||||
|
p [] [
|
||||||
|
txt "GDPR adds some new requirements regarding how companies should protect individuals’ personal "
|
||||||
|
txt "data that they collect and process. It also raises the stakes for compliance by increasing "
|
||||||
|
txt "enforcement and imposing greater fines for breach. Beyond these facts, it’s simply the right "
|
||||||
|
txt "thing to do. At "; appName; txt " we strongly believe that your data privacy is very important and we "
|
||||||
|
txt "already have solid security and privacy practices in place that go beyond the requirements of this "
|
||||||
|
txt "regulation."
|
||||||
|
]
|
||||||
|
|
||||||
|
h5 [] [ txt "Individual Data Subject’s Rights - Data Access, Portability, and Deletion" ]
|
||||||
|
p [] [
|
||||||
|
txt "We are committed to helping our customers meet the data subject rights requirements of GDPR. "
|
||||||
|
appName; txt " processes or stores all personal data in fully vetted, DPA compliant vendors. We do store "
|
||||||
|
txt "all conversation and personal data for up to 6 years unless your account is deleted. In which case, "
|
||||||
|
txt "we dispose of all data in accordance with our Terms of Service and Privacy Policy, but we will not "
|
||||||
|
txt "hold it longer than 60 days."
|
||||||
|
]
|
||||||
|
p [] [
|
||||||
|
txt "We are aware that if you are working with EU customers, you need to be able to provide them with the "
|
||||||
|
txt "ability to access, update, retrieve and remove personal data. We got you! We’ve been set up as "
|
||||||
|
txt "self service from the start and have always given you access to your data. Our customer support team "
|
||||||
|
txt "is here for you to answer any questions you might have about working with the API."
|
||||||
|
]
|
||||||
|
|
||||||
|
h4 [] [ txt "California Residents" ]
|
||||||
|
p [] [
|
||||||
|
txt "The California Consumer Privacy Act (CCPA) requires us to disclose categories of Personal Information "
|
||||||
|
txt "we collect and how we use it, the categories of sources from whom we collect Personal Information, "
|
||||||
|
txt "and the third parties with whom we share it, which we have explained above."
|
||||||
|
]
|
||||||
|
p [] [
|
||||||
|
txt "We are also required to communicate information about rights California residents have under "
|
||||||
|
txt "California law. You may exercise the following rights:"
|
||||||
]
|
]
|
||||||
ul [] [
|
ul [] [
|
||||||
li [] [
|
li [] [
|
||||||
rawText "Right to Know and Access. You may submit a verifiable request for information regarding the: "
|
txt "Right to Know and Access. You may submit a verifiable request for information regarding the: (1) "
|
||||||
rawText "(1) categories of Personal Information we collect, use, or share; (2) purposes for which "
|
txt "categories of Personal Information we collect, use, or share; (2) purposes for which categories "
|
||||||
rawText "categories of Personal Information are collected or used by us; (3) categories of sources "
|
txt "of Personal Information are collected or used by us; (3) categories of sources from which we "
|
||||||
rawText "from which we collect Personal Information; and (4) specific pieces of Personal Information "
|
txt "collect Personal Information; and (4) specific pieces of Personal Information we have collected "
|
||||||
rawText "we have collected about you."
|
txt "about you."
|
||||||
]
|
]
|
||||||
li [] [
|
li [] [
|
||||||
rawText "Right to Equal Service. We will not discriminate against you if you exercise your privacy "
|
txt "Right to Equal Service. We will not discriminate against you if you exercise your privacy rights."
|
||||||
rawText "rights."
|
|
||||||
]
|
]
|
||||||
li [] [
|
li [] [
|
||||||
rawText "Right to Delete. You may submit a verifiable request to close your account and we will delete "
|
txt "Right to Delete. You may submit a verifiable request to close your account and we will delete "
|
||||||
rawText "Personal Information about you that we have collected."
|
txt "Personal Information about you that we have collected."
|
||||||
]
|
]
|
||||||
li [] [
|
li [] [
|
||||||
rawText "Request that a business that sells a consumer’s personal data, not sell the "
|
txt "Request that a business that sells a consumer’s personal data, not sell the "
|
||||||
rawText "consumer’s personal data."
|
txt "consumer’s personal data."
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
p [] [
|
p [] [
|
||||||
rawText "If you make a request, we have one month to respond to you. If you would like to exercise any of "
|
txt "If you make a request, we have one month to respond to you. If you would like to exercise any of "
|
||||||
rawText "these rights, please contact us."
|
txt "these rights, please contact us."
|
||||||
]
|
]
|
||||||
p [] [ rawText "We do not sell the Personal Information of our users." ]
|
p [] [ txt "We do not sell the Personal Information of our users." ]
|
||||||
p [] [ rawText "For more information about these rights, please contact us." ]
|
p [] [ txt "For more information about these rights, please contact us." ]
|
||||||
|
|
||||||
h5 [] [ rawText "California Online Privacy Protection Act (CalOPPA)" ]
|
h5 [] [ txt "California Online Privacy Protection Act (CalOPPA)" ]
|
||||||
p [] [
|
p [] [
|
||||||
rawText "CalOPPA requires us to disclose categories of Personal Information we collect and how we use it, "
|
txt "CalOPPA requires us to disclose categories of Personal Information we collect and how we use it, the "
|
||||||
rawText "the categories of sources from whom we collect Personal Information, and the third parties with "
|
txt "categories of sources from whom we collect Personal Information, and the third parties with whom we "
|
||||||
rawText "whom we share it, which we have explained above."
|
txt "share it, which we have explained above."
|
||||||
]
|
]
|
||||||
p [] [ rawText "CalOPPA users have the following rights:" ]
|
p [] [ txt "CalOPPA users have the following rights:" ]
|
||||||
ul [] [
|
ul [] [
|
||||||
li [] [
|
li [] [
|
||||||
rawText "Right to Know and Access. You may submit a verifiable request for information regarding the: "
|
txt "Right to Know and Access. You may submit a verifiable request for information regarding the: (1) "
|
||||||
rawText "(1) categories of Personal Information we collect, use, or share; (2) purposes for which "
|
txt "categories of Personal Information we collect, use, or share; (2) purposes for which categories "
|
||||||
rawText "categories of Personal Information are collected or used by us; (3) categories of sources "
|
txt "of Personal Information are collected or used by us; (3) categories of sources from which we "
|
||||||
rawText "from which we collect Personal Information; and (4) specific pieces of Personal Information "
|
txt "collect Personal Information; and (4) specific pieces of Personal Information we have collected "
|
||||||
rawText "we have collected about you."
|
txt "about you."
|
||||||
]
|
]
|
||||||
li [] [
|
li [] [
|
||||||
rawText "Right to Equal Service. We will not discriminate against you if you exercise your privacy "
|
txt "Right to Equal Service. We will not discriminate against you if you exercise your privacy rights."
|
||||||
rawText "rights."
|
|
||||||
]
|
]
|
||||||
li [] [
|
li [] [
|
||||||
rawText "Right to Delete. You may submit a verifiable request to close your account and we will delete "
|
txt "Right to Delete. You may submit a verifiable request to close your account and we will delete "
|
||||||
rawText "Personal Information about you that we have collected."
|
txt "Personal Information about you that we have collected."
|
||||||
]
|
]
|
||||||
li [] [
|
li [] [
|
||||||
rawText "Right to request that a business that sells a consumer’s personal data, not sell the "
|
txt "Right to request that a business that sells a consumer’s personal data, not sell the "
|
||||||
rawText "consumer’s personal data."
|
txt "consumer’s personal data."
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
p [] [
|
p [] [
|
||||||
rawText "If you make a request, we have one month to respond to you. If you would like to exercise any of "
|
txt "If you make a request, we have one month to respond to you. If you would like to exercise any of "
|
||||||
rawText "these rights, please contact us."
|
txt "these rights, please contact us."
|
||||||
]
|
]
|
||||||
p [] [ rawText "We do not sell the Personal Information of our users." ]
|
p [] [ txt "We do not sell the Personal Information of our users." ]
|
||||||
p [] [ rawText "For more information about these rights, please contact us." ]
|
p [] [ txt "For more information about these rights, please contact us." ]
|
||||||
|
|
||||||
h4 [] [ rawText "Contact Us" ]
|
h4 [] [ txt "Contact Us" ]
|
||||||
p [] [ rawText "Don’t hesitate to contact us if you have any questions." ]
|
p [] [ txt "Don’t hesitate to contact us if you have any questions." ]
|
||||||
ul [] [
|
ul [] [
|
||||||
li [] [
|
li [] [
|
||||||
rawText "Via this Link: "
|
txt "Via this Link: "; a [ _href "/how-it-works" ] [ txt "https://noagendacareers.com/how-it-works" ]
|
||||||
a [ _href "/how-it-works" ] [rawText "https://noagendacareers.com/how-it-works" ]
|
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
hr []
|
hr []
|
||||||
|
|
||||||
p [ _class "fst-italic" ] [ rawText "Changes for "; appName; rawText " v3 (December 27<sup>th</sup>, 2022)" ]
|
p [ _class "fst-italic" ] [ txt "Changes for "; appName; txt " v3 (December 27<sup>th</sup>, 2022)" ]
|
||||||
ul [] [
|
ul [] [
|
||||||
li [ _class "fst-italic" ] [ rawText "Removed references to Mastodon" ]
|
li [ _class "fst-italic" ] [ txt "Removed references to Mastodon" ]
|
||||||
li [ _class "fst-italic" ] [ rawText "Added references to job listings" ]
|
li [ _class "fst-italic" ] [ txt "Added references to job listings" ]
|
||||||
li [ _class "fst-italic" ] [ rawText "Changed information regarding e-mail addresses" ]
|
li [ _class "fst-italic" ] [ txt "Changed information regarding e-mail addresses" ]
|
||||||
li [ _class "fst-italic" ] [ rawText "Updated cookie / tracking sections for new architecture" ]
|
li [ _class "fst-italic" ] [ txt "Updated cookie / tracking sections for new architecture" ]
|
||||||
]
|
]
|
||||||
p [ _class "fst-italic" ] [
|
p [ _class "fst-italic" ] [
|
||||||
rawText "Change on September 6<sup>th</sup>, 2021 – replaced “No Agenda Social” with "
|
txt "Change on September 6<sup>th</sup>, 2021 – replaced “No Agenda Social” with generic "
|
||||||
rawText "generic terms for any authorized Mastodon instance."
|
txt "terms for any authorized Mastodon instance."
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
/// The page for terms of service
|
/// The page for terms of service
|
||||||
let termsOfService =
|
let termsOfService =
|
||||||
article [] [
|
article [] [
|
||||||
h3 [] [ rawText "Terms of Service" ]
|
h3 [] [ txt "Terms of Service" ]
|
||||||
p [ _class "fst-italic" ] [ rawText "(as of August 30<sup>th</sup>, 2022)" ]
|
p [ _class "fst-italic" ] [ txt "(as of August 30<sup>th</sup>, 2022)" ]
|
||||||
h4 [] [ rawText "Acceptance of Terms" ]
|
h4 [] [ txt "Acceptance of Terms" ]
|
||||||
p [] [
|
p [] [
|
||||||
rawText "By accessing this web site, you are agreeing to be bound by these Terms and Conditions, and that "
|
txt "By accessing this web site, you are agreeing to be bound by these Terms and Conditions, and that you "
|
||||||
rawText "you are responsible to ensure that your use of this site complies with all applicable laws. Your "
|
txt "are responsible to ensure that your use of this site complies with all applicable laws. Your "
|
||||||
rawText "continued use of this site implies your acceptance of these terms."
|
txt "continued use of this site implies your acceptance of these terms."
|
||||||
]
|
]
|
||||||
h4 [] [ rawText "Description of Service and Registration" ]
|
h4 [] [ txt "Description of Service and Registration" ]
|
||||||
p [] [
|
p [] [
|
||||||
rawText "Jobs, Jobs, Jobs is a service that allows individuals to enter and amend employment profiles and "
|
txt "Jobs, Jobs, Jobs is a service that allows individuals to enter and amend employment profiles and job "
|
||||||
rawText "job listings, restricting access to the details of these to other users of this site, unless the "
|
txt "listings, restricting access to the details of these to other users of this site, unless the "
|
||||||
rawText "individual specifies that this information should be visible publicly. See our "
|
txt "individual specifies that this information should be visible publicly. See our "
|
||||||
a [ _href "/privacy-policy" ] [ str "privacy policy" ]
|
a [ _href "/privacy-policy" ] [ txt "privacy policy" ]
|
||||||
rawText " for details on the personal (user) information we maintain."
|
txt " for details on the personal (user) information we maintain."
|
||||||
]
|
]
|
||||||
h4 [] [ rawText "Liability" ]
|
h4 [] [ txt "Liability" ]
|
||||||
p [] [
|
p [] [
|
||||||
rawText "This service is provided “as is”, and no warranty (express or implied) exists. The "
|
txt "This service is provided “as is”, and no warranty (express or implied) exists. The "
|
||||||
rawText "service and its developers may not be held liable for any damages that may arise through the use "
|
txt "service and its developers may not be held liable for any damages that may arise through the use of "
|
||||||
rawText "of this service."
|
txt "this service."
|
||||||
]
|
]
|
||||||
h4 [] [ rawText "Updates to Terms" ]
|
h4 [] [ txt "Updates to Terms" ]
|
||||||
p [] [
|
p [] [
|
||||||
rawText "These terms and conditions may be updated at any time. When these terms are updated, users will "
|
txt "These terms and conditions may be updated at any time. When these terms are updated, users will be "
|
||||||
rawText "be notified via a notice on the dashboard page. Additionally, the date at the top of this page "
|
txt "notified via a notice on the dashboard page. Additionally, the date at the top of this page will be "
|
||||||
rawText "will be updated, and any substantive updates will also be accompanied by a summary of those "
|
txt "updated, and any substantive updates will also be accompanied by a summary of those changes."
|
||||||
rawText "changes."
|
|
||||||
]
|
]
|
||||||
hr []
|
hr []
|
||||||
p [] [
|
p [] [
|
||||||
rawText "You may also wish to review our "
|
txt "You may also wish to review our "; a [ _href "/privacy-policy" ] [ txt "privacy policy" ]
|
||||||
a [ _href "/privacy-policy" ] [ rawText "privacy policy" ]
|
txt " to learn how we handle your data."
|
||||||
rawText " to learn how we handle your data."
|
|
||||||
]
|
]
|
||||||
hr []
|
hr []
|
||||||
p [ _class "fst-italic" ] [
|
p [ _class "fst-italic" ] [
|
||||||
rawText "Change on August 30<sup>th</sup>, 2022 – added references to job listings, removed "
|
txt "Change on August 30<sup>th</sup>, 2022 – added references to job listings, removed references "
|
||||||
rawText "references to Mastodon instances."
|
txt "to Mastodon instances."
|
||||||
]
|
]
|
||||||
p [ _class "fst-italic" ] [
|
p [ _class "fst-italic" ] [
|
||||||
rawText "Change on September 6<sup>th</sup>, 2021 – replaced “No Agenda Social” with a "
|
txt "Change on September 6<sup>th</sup>, 2021 – replaced “No Agenda Social” with a list "
|
||||||
rawText "list of all No Agenda-affiliated Mastodon instances."
|
txt "of all No Agenda-affiliated Mastodon instances."
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
|
@ -54,7 +54,7 @@ let private links ctx =
|
||||||
a [ _href url
|
a [ _href url
|
||||||
_onclick "jjj.hideMenu()"
|
_onclick "jjj.hideMenu()"
|
||||||
if url = ctx.CurrentUrl then _class "jjj-current-page"
|
if url = ctx.CurrentUrl then _class "jjj-current-page"
|
||||||
] [ i [ _class $"mdi mdi-{icon}"; _ariaHidden "true" ] []; rawText text ]
|
] [ i [ _class $"mdi mdi-{icon}"; _ariaHidden "true" ] []; txt text ]
|
||||||
nav [ _class "jjj-nav" ] [
|
nav [ _class "jjj-nav" ] [
|
||||||
if ctx.IsLoggedOn then
|
if ctx.IsLoggedOn then
|
||||||
navLink "/citizen/dashboard" "view-dashboard-variant" "Dashboard"
|
navLink "/citizen/dashboard" "view-dashboard-variant" "Dashboard"
|
||||||
|
@ -76,12 +76,10 @@ let private links ctx =
|
||||||
|
|
||||||
/// Generate mobile and desktop side navigation areas
|
/// Generate mobile and desktop side navigation areas
|
||||||
let private sideNavs ctx = [
|
let private sideNavs ctx = [
|
||||||
div [ _id "mobileMenu"
|
div [ _id "mobileMenu"; _class "jjj-mobile-menu offcanvas offcanvas-end"; _tabindex "-1"
|
||||||
_class "jjj-mobile-menu offcanvas offcanvas-end"
|
|
||||||
_tabindex "-1"
|
|
||||||
_ariaLabelledBy "mobileMenuLabel" ] [
|
_ariaLabelledBy "mobileMenuLabel" ] [
|
||||||
div [ _class "offcanvas-header" ] [
|
div [ _class "offcanvas-header" ] [
|
||||||
h5 [ _id "mobileMenuLabel" ] [ rawText "Menu" ]
|
h5 [ _id "mobileMenuLabel" ] [ txt "Menu" ]
|
||||||
button [
|
button [
|
||||||
_class "btn-close text-reset"; _type "button"; _data "bs-dismiss" "offcanvas"; _ariaLabel "Close"
|
_class "btn-close text-reset"; _type "button"; _data "bs-dismiss" "offcanvas"; _ariaLabel "Close"
|
||||||
] []
|
] []
|
||||||
|
@ -89,8 +87,8 @@ let private sideNavs ctx = [
|
||||||
div [ _class "offcanvas-body" ] [ links ctx ]
|
div [ _class "offcanvas-body" ] [ links ctx ]
|
||||||
]
|
]
|
||||||
aside [ _class "jjj-full-menu d-none d-md-block p-3" ] [
|
aside [ _class "jjj-full-menu d-none d-md-block p-3" ] [
|
||||||
p [ _class "home-link pb-3" ] [ a [ _href "/" ] [ rawText "Jobs, Jobs, Jobs" ] ]
|
p [ _class "home-link pb-3" ] [ a [ _href "/" ] [ txt "Jobs, Jobs, Jobs" ] ]
|
||||||
p [] [ rawText " " ]
|
emptyP
|
||||||
links ctx
|
links ctx
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
@ -98,18 +96,14 @@ let private sideNavs ctx = [
|
||||||
/// Title bars for mobile and desktop
|
/// Title bars for mobile and desktop
|
||||||
let private titleBars = [
|
let private titleBars = [
|
||||||
nav [ _class "d-flex d-md-none navbar navbar-dark" ] [
|
nav [ _class "d-flex d-md-none navbar navbar-dark" ] [
|
||||||
span [ _class "navbar-text" ] [ a [ _href "/" ] [ rawText "Jobs, Jobs, Jobs" ] ]
|
span [ _class "navbar-text" ] [ a [ _href "/" ] [ txt "Jobs, Jobs, Jobs" ] ]
|
||||||
button [ _class "btn"
|
button [ _class "btn"; _data "bs-toggle" "offcanvas"; _data "bs-target" "#mobileMenu"
|
||||||
_data "bs-toggle" "offcanvas"
|
|
||||||
_data "bs-target" "#mobileMenu"
|
|
||||||
_ariaControls "mobileMenu" ] [ i [ _class "mdi mdi-menu" ] [] ]
|
_ariaControls "mobileMenu" ] [ i [ _class "mdi mdi-menu" ] [] ]
|
||||||
]
|
]
|
||||||
nav [ _class "d-none d-md-flex navbar navbar-light bg-light"] [
|
nav [ _class "d-none d-md-flex navbar navbar-light bg-light"] [
|
||||||
span [] [ rawText " " ]
|
span [] [ txt " " ]
|
||||||
span [ _class "navbar-text" ] [
|
span [ _class "navbar-text" ] [
|
||||||
rawText "(…and Jobs – "
|
txt "(…and Jobs – "; audioClip "pelosi-jobs" (txt "Let’s Vote for Jobs!"); txt ")"
|
||||||
audioClip "pelosi-jobs" (rawText "Let’s Vote for Jobs!")
|
|
||||||
rawText ")"
|
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
@ -127,9 +121,9 @@ let private htmlFoot =
|
||||||
} |> Seq.reduce (+)
|
} |> Seq.reduce (+)
|
||||||
footer [] [
|
footer [] [
|
||||||
p [ _class "text-muted" ] [
|
p [ _class "text-muted" ] [
|
||||||
str "Jobs, Jobs, Jobs v"; str version; rawText " • "
|
txt $"Jobs, Jobs, Jobs v{version} • "
|
||||||
a [ _href "/privacy-policy" ] [ str "Privacy Policy" ]; rawText " • "
|
a [ _href "/privacy-policy" ] [ txt "Privacy Policy" ]; txt " • "
|
||||||
a [ _href "/terms-of-service" ] [ str "Terms of Service" ]
|
a [ _href "/terms-of-service" ] [ txt "Terms of Service" ]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -143,9 +137,8 @@ let private messages ctx =
|
||||||
div [ _class $"alert alert-{level} alert-dismissable fade show d-flex justify-content-between p-2 mb-1 mt-1"
|
div [ _class $"alert alert-{level} alert-dismissable fade show d-flex justify-content-between p-2 mb-1 mt-1"
|
||||||
_roleAlert ] [
|
_roleAlert ] [
|
||||||
p [ _class "mb-0" ] [
|
p [ _class "mb-0" ] [
|
||||||
if level <> "success" then
|
if level <> "success" then strong [] [ txt $"{parts[0].ToUpperInvariant ()}: " ]
|
||||||
strong [] [ rawText (parts[0].ToUpperInvariant ()); rawText ": " ]
|
txt message
|
||||||
rawText message
|
|
||||||
]
|
]
|
||||||
button [ _type "button"; _class "btn-close"; _data "bs-dismiss" "alert"; _ariaLabel "Close" ] []
|
button [ _type "button"; _class "btn-close"; _data "bs-dismiss" "alert"; _ariaLabel "Close" ] []
|
||||||
])
|
])
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
module JobsJobsJobs.Views.Listing
|
module JobsJobsJobs.Views.Listing
|
||||||
|
|
||||||
open Giraffe.ViewEngine
|
open Giraffe.ViewEngine
|
||||||
open Giraffe.ViewEngine.Htmx
|
|
||||||
open JobsJobsJobs.Domain
|
open JobsJobsJobs.Domain
|
||||||
open JobsJobsJobs.Domain.SharedTypes
|
open JobsJobsJobs.Domain.SharedTypes
|
||||||
open JobsJobsJobs.ViewModels
|
open JobsJobsJobs.ViewModels
|
||||||
|
@ -11,15 +10,14 @@ open JobsJobsJobs.ViewModels
|
||||||
|
|
||||||
/// Job listing edit page
|
/// Job listing edit page
|
||||||
let edit (m : EditListingForm) continents isNew csrf =
|
let edit (m : EditListingForm) continents isNew csrf =
|
||||||
article [] [
|
pageWithTitle $"""{if isNew then "Add a" else "Edit"} Job Listing""" [
|
||||||
h3 [ _class "pb-3" ] [ rawText (if isNew then "Add a" else "Edit"); rawText " Job Listing" ]
|
|
||||||
form [ _class "row g-3"; _method "POST"; _action "/listing/save" ] [
|
form [ _class "row g-3"; _method "POST"; _action "/listing/save" ] [
|
||||||
antiForgery csrf
|
antiForgery csrf
|
||||||
input [ _type "hidden"; _name (nameof m.Id); _value m.Id ]
|
input [ _type "hidden"; _name (nameof m.Id); _value m.Id ]
|
||||||
div [ _class "col-12 col-sm-10 col-md-8 col-lg-6" ] [
|
div [ _class "col-12 col-sm-10 col-md-8 col-lg-6" ] [
|
||||||
textBox [ _type "text"; _maxlength "255"; _autofocus ] (nameof m.Title) m.Title "Title" true
|
textBox [ _type "text"; _maxlength "255"; _autofocus ] (nameof m.Title) m.Title "Title" true
|
||||||
div [ _class "form-text" ] [
|
div [ _class "form-text" ] [
|
||||||
rawText "No need to put location here; it will always be show to seekers with continent and region"
|
txt "No need to put location here; it will always be show to seekers with continent and region"
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
div [ _class "col-12 col-sm-6 col-md-4" ] [
|
div [ _class "col-12 col-sm-6 col-md-4" ] [
|
||||||
|
@ -27,7 +25,7 @@ let edit (m : EditListingForm) continents isNew csrf =
|
||||||
]
|
]
|
||||||
div [ _class "col-12 col-sm-6 col-md-8" ] [
|
div [ _class "col-12 col-sm-6 col-md-8" ] [
|
||||||
textBox [ _type "text"; _maxlength "255" ] (nameof m.Region) m.Region "Region" true
|
textBox [ _type "text"; _maxlength "255" ] (nameof m.Region) m.Region "Region" true
|
||||||
div [ _class "form-text" ] [ rawText "Country, state, geographic area, etc." ]
|
div [ _class "form-text" ] [ txt "Country, state, geographic area, etc." ]
|
||||||
]
|
]
|
||||||
div [ _class "col-12" ] [
|
div [ _class "col-12" ] [
|
||||||
checkBox [] (nameof m.RemoteWork) m.RemoteWork "This opportunity is for remote work"
|
checkBox [] (nameof m.RemoteWork) m.RemoteWork "This opportunity is for remote work"
|
||||||
|
@ -36,21 +34,19 @@ let edit (m : EditListingForm) continents isNew csrf =
|
||||||
div [ _class "col-12 col-md-4" ] [
|
div [ _class "col-12 col-md-4" ] [
|
||||||
textBox [ _type "date" ] (nameof m.NeededBy) m.NeededBy "Needed By" false
|
textBox [ _type "date" ] (nameof m.NeededBy) m.NeededBy "Needed By" false
|
||||||
]
|
]
|
||||||
div [ _class "col-12" ] [
|
div [ _class "col-12" ] [ submitButton "content-save-outline" "Save" ]
|
||||||
button [ _type "submit"; _class "btn btn-primary" ] [
|
|
||||||
i [ _class "mdi mdi-content-save-outline" ] []; rawText " Save"
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
open System.Net
|
||||||
|
|
||||||
|
/// Page to expire a job listing
|
||||||
let expire (m : ExpireListingForm) (listing : Listing) csrf =
|
let expire (m : ExpireListingForm) (listing : Listing) csrf =
|
||||||
article [] [
|
pageWithTitle $"Expire Job Listing ({WebUtility.HtmlEncode listing.Title})" [
|
||||||
h3 [ _class "pb-3" ] [ rawText "Expire Job Listing ("; str listing.Title; rawText ")" ]
|
|
||||||
p [ _class "fst-italic" ] [
|
p [ _class "fst-italic" ] [
|
||||||
rawText "Expiring this listing will remove it from search results. You will be able to see it via your "
|
txt "Expiring this listing will remove it from search results. You will be able to see it via your "
|
||||||
rawText "“My Job Listings” page, but you will not be able to “un-expire” it."
|
txt "“My Job Listings” page, but you will not be able to “un-expire” it."
|
||||||
]
|
]
|
||||||
form [ _class "row g-3"; _method "POST"; _action "/listing/expire" ] [
|
form [ _class "row g-3"; _method "POST"; _action "/listing/expire" ] [
|
||||||
antiForgery csrf
|
antiForgery csrf
|
||||||
|
@ -61,16 +57,12 @@ let expire (m : ExpireListingForm) (listing : Listing) csrf =
|
||||||
]
|
]
|
||||||
div [ _class "col-12"; _id "successRow" ] [
|
div [ _class "col-12"; _id "successRow" ] [
|
||||||
p [] [
|
p [] [
|
||||||
rawText "Consider telling your fellow citizens about your experience! Comments entered here will "
|
txt "Consider telling your fellow citizens about your experience! Comments entered here will be "
|
||||||
rawText "be visible to logged-on users here, but not to the general public."
|
txt "visible to logged-on users here, but not to the general public."
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
markdownEditor [] (nameof m.SuccessStory) m.SuccessStory "Your Success Story"
|
markdownEditor [] (nameof m.SuccessStory) m.SuccessStory "Your Success Story"
|
||||||
div [ _class "col-12" ] [
|
div [ _class "col-12" ] [ submitButton "text-box-remove-outline" "Expire Listing" ]
|
||||||
button [ _type "submit"; _class "btn btn-primary" ] [
|
|
||||||
i [ _class "mdi mdi-text-box-remove-outline" ] []; rawText " Expire Listing"
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
]
|
||||||
jsOnLoad "jjj.listing.toggleFromHere()"
|
jsOnLoad "jjj.listing.toggleFromHere()"
|
||||||
]
|
]
|
||||||
|
@ -80,17 +72,15 @@ let expire (m : ExpireListingForm) (listing : Listing) csrf =
|
||||||
let mine (listings : ListingForView list) tz =
|
let mine (listings : ListingForView list) tz =
|
||||||
let active = listings |> List.filter (fun it -> not it.Listing.IsExpired)
|
let active = listings |> List.filter (fun it -> not it.Listing.IsExpired)
|
||||||
let expired = listings |> List.filter (fun it -> it.Listing.IsExpired)
|
let expired = listings |> List.filter (fun it -> it.Listing.IsExpired)
|
||||||
article [] [
|
pageWithTitle "My Job Listings" [
|
||||||
h3 [ _class "pb-3" ] [ rawText "My Job Listings" ]
|
p [] [ a [ _href "/listing/new/edit"; _class "btn btn-outline-primary" ] [ txt "Add a New Job Listing" ] ]
|
||||||
p [] [ a [ _href "/listing/new/edit"; _class "btn btn-outline-primary" ] [ rawText "Add a New Job Listing" ] ]
|
if not (List.isEmpty expired) then h4 [ _class "pb-2" ] [ txt "Active Job Listings" ]
|
||||||
if not (List.isEmpty expired) then h4 [ _class "pb-2" ] [ rawText "Active Job Listings" ]
|
if List.isEmpty active then p [ _class "pb-3 fst-italic" ] [ txt "You have no active job listings" ]
|
||||||
if List.isEmpty active then
|
|
||||||
p [ _class "pb-3 fst-italic" ] [ rawText "You have no active job listings" ]
|
|
||||||
else
|
else
|
||||||
table [ _class "pb-3 table table-sm table-hover pt-3" ] [
|
table [ _class "pb-3 table table-sm table-hover pt-3" ] [
|
||||||
thead [] [
|
thead [] [
|
||||||
[ "Action"; "Title"; "Continent / Region"; "Created"; "Updated" ]
|
[ "Action"; "Title"; "Continent / Region"; "Created"; "Updated" ]
|
||||||
|> List.map (fun it -> th [ _scope "col" ] [ rawText it ])
|
|> List.map (fun it -> th [ _scope "col" ] [ txt it ])
|
||||||
|> tr []
|
|> tr []
|
||||||
]
|
]
|
||||||
active
|
active
|
||||||
|
@ -98,9 +88,9 @@ let mine (listings : ListingForView list) tz =
|
||||||
let listId = ListingId.toString it.Listing.Id
|
let listId = ListingId.toString it.Listing.Id
|
||||||
tr [] [
|
tr [] [
|
||||||
td [] [
|
td [] [
|
||||||
a [ _href $"/listing/{listId}/edit" ] [ rawText "Edit" ]; rawText " ~ "
|
a [ _href $"/listing/{listId}/edit" ] [ txt "Edit" ]; txt " ~ "
|
||||||
a [ _href $"/listing/{listId}/view" ] [ rawText "View" ]; rawText " ~ "
|
a [ _href $"/listing/{listId}/view" ] [ txt "View" ]; txt " ~ "
|
||||||
a [ _href $"/listing/{listId}/expire" ] [ rawText "Expire" ]
|
a [ _href $"/listing/{listId}/expire" ] [ txt "Expire" ]
|
||||||
]
|
]
|
||||||
td [] [ str it.Listing.Title ]
|
td [] [ str it.Listing.Title ]
|
||||||
td [] [ str it.ContinentName; rawText " / "; str it.Listing.Region ]
|
td [] [ str it.ContinentName; rawText " / "; str it.Listing.Region ]
|
||||||
|
@ -110,17 +100,17 @@ let mine (listings : ListingForView list) tz =
|
||||||
|> tbody []
|
|> tbody []
|
||||||
]
|
]
|
||||||
if not (List.isEmpty expired) then
|
if not (List.isEmpty expired) then
|
||||||
h4 [ _class "pb-2" ] [ rawText "Expired Job Listings" ]
|
h4 [ _class "pb-2" ] [ txt "Expired Job Listings" ]
|
||||||
table [ _class "table table-sm table-hover pt-3" ] [
|
table [ _class "table table-sm table-hover pt-3" ] [
|
||||||
thead [] [
|
thead [] [
|
||||||
[ "Action"; "Title"; "Filled Here?"; "Expired" ]
|
[ "Action"; "Title"; "Filled Here?"; "Expired" ]
|
||||||
|> List.map (fun it -> th [ _scope "col" ] [ rawText it ])
|
|> List.map (fun it -> th [ _scope "col" ] [ txt it ])
|
||||||
|> tr []
|
|> tr []
|
||||||
]
|
]
|
||||||
expired
|
expired
|
||||||
|> List.map (fun it ->
|
|> List.map (fun it ->
|
||||||
tr [] [
|
tr [] [
|
||||||
td [] [ a [ _href $"/listing/{ListingId.toString it.Listing.Id}/view" ] [rawText "View" ] ]
|
td [] [ a [ _href $"/listing/{ListingId.toString it.Listing.Id}/view" ] [ txt "View" ] ]
|
||||||
td [] [ str it.Listing.Title ]
|
td [] [ str it.Listing.Title ]
|
||||||
td [] [ str (yesOrNo (defaultArg it.Listing.WasFilledHere false)) ]
|
td [] [ str (yesOrNo (defaultArg it.Listing.WasFilledHere false)) ]
|
||||||
td [] [ str (fullDateTime it.Listing.UpdatedOn tz) ]
|
td [] [ str (fullDateTime it.Listing.UpdatedOn tz) ]
|
||||||
|
@ -136,12 +126,11 @@ let private neededBy dt =
|
||||||
(LocalDatePattern.CreateWithCurrentCulture "MMMM d, yyyy").Format dt
|
(LocalDatePattern.CreateWithCurrentCulture "MMMM d, yyyy").Format dt
|
||||||
|
|
||||||
let search (m : ListingSearchForm) continents (listings : ListingForView list option) =
|
let search (m : ListingSearchForm) continents (listings : ListingForView list option) =
|
||||||
article [] [
|
pageWithTitle "Help Wanted" [
|
||||||
h3 [ _class "pb-3" ] [ rawText "Help Wanted" ]
|
|
||||||
if Option.isNone listings then
|
if Option.isNone listings then
|
||||||
p [] [
|
p [] [
|
||||||
rawText "Enter relevant criteria to find results, or just click “Search” to see all "
|
txt "Enter relevant criteria to find results, or just click “Search” to see all active job "
|
||||||
rawText "current job listings."
|
txt "listings."
|
||||||
]
|
]
|
||||||
collapsePanel "Search Criteria" [
|
collapsePanel "Search Criteria" [
|
||||||
form [ _class "container"; _method "GET"; _action "/help-wanted" ] [
|
form [ _class "container"; _method "GET"; _action "/help-wanted" ] [
|
||||||
|
@ -152,61 +141,61 @@ let search (m : ListingSearchForm) continents (listings : ListingForView list op
|
||||||
]
|
]
|
||||||
div [ _class "col-12 col-sm-6 col-md-4 col-lg-3" ] [
|
div [ _class "col-12 col-sm-6 col-md-4 col-lg-3" ] [
|
||||||
textBox [ _maxlength "1000" ] (nameof m.Region) m.Region "Region" false
|
textBox [ _maxlength "1000" ] (nameof m.Region) m.Region "Region" false
|
||||||
div [ _class "form-text" ] [ rawText "(free-form text)" ]
|
div [ _class "form-text" ] [ txt "(free-form text)" ]
|
||||||
]
|
]
|
||||||
div [ _class "col-12 col-sm-6 col-offset-md-2 col-lg-3 col-offset-lg-0" ] [
|
div [ _class "col-12 col-sm-6 col-offset-md-2 col-lg-3 col-offset-lg-0" ] [
|
||||||
label [ _class "jjj-label" ] [ rawText "Seeking Remote Work?" ]; br []
|
label [ _class "jjj-label" ] [ txt "Seeking Remote Work?" ]; br []
|
||||||
div [ _class "form-check form-check-inline" ] [
|
div [ _class "form-check form-check-inline" ] [
|
||||||
input [ _type "radio"; _id "remoteNull"; _name (nameof m.RemoteWork); _value ""
|
input [ _type "radio"; _id "remoteNull"; _name (nameof m.RemoteWork); _value ""
|
||||||
_class "form-check-input"; if m.RemoteWork = "" then _checked ]
|
_class "form-check-input"; if m.RemoteWork = "" then _checked ]
|
||||||
label [ _class "form-check-label"; _for "remoteNull" ] [ rawText "No Selection" ]
|
label [ _class "form-check-label"; _for "remoteNull" ] [ txt "No Selection" ]
|
||||||
]
|
]
|
||||||
div [ _class "form-check form-check-inline" ] [
|
div [ _class "form-check form-check-inline" ] [
|
||||||
input [ _type "radio"; _id "remoteYes"; _name (nameof m.RemoteWork); _value "yes"
|
input [ _type "radio"; _id "remoteYes"; _name (nameof m.RemoteWork); _value "yes"
|
||||||
_class "form-check-input"; if m.RemoteWork = "yes" then _checked ]
|
_class "form-check-input"; if m.RemoteWork = "yes" then _checked ]
|
||||||
label [ _class "form-check-label"; _for "remoteYes" ] [ rawText "Yes" ]
|
label [ _class "form-check-label"; _for "remoteYes" ] [ txt "Yes" ]
|
||||||
]
|
]
|
||||||
div [ _class "form-check form-check-inline" ] [
|
div [ _class "form-check form-check-inline" ] [
|
||||||
input [ _type "radio"; _id "remoteNo"; _name (nameof m.RemoteWork); _value "no"
|
input [ _type "radio"; _id "remoteNo"; _name (nameof m.RemoteWork); _value "no"
|
||||||
_class "form-check-input"; if m.RemoteWork = "no" then _checked ]
|
_class "form-check-input"; if m.RemoteWork = "no" then _checked ]
|
||||||
label [ _class "form-check-label"; _for "remoteNo" ] [ rawText "No" ]
|
label [ _class "form-check-label"; _for "remoteNo" ] [ txt "No" ]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
div [ _class "col-12 col-sm-6 col-lg-3" ] [
|
div [ _class "col-12 col-sm-6 col-lg-3" ] [
|
||||||
textBox [ _maxlength "1000" ] (nameof m.Text) m.Text "Job Listing Text" false
|
textBox [ _maxlength "1000" ] (nameof m.Text) m.Text "Job Listing Text" false
|
||||||
div [ _class "form-text" ] [ rawText "(free-form text)" ]
|
div [ _class "form-text" ] [ txt "(free-form text)" ]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
div [ _class "row" ] [
|
div [ _class "row" ] [
|
||||||
div [ _class "col" ] [
|
div [ _class "col" ] [
|
||||||
br []
|
br []
|
||||||
button [ _type "submit"; _class "btn btn-outline-primary" ] [ rawText "Search" ]
|
button [ _type "submit"; _class "btn btn-outline-primary" ] [ txt "Search" ]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
match listings with
|
match listings with
|
||||||
| Some r when List.isEmpty r ->
|
| Some r when List.isEmpty r ->
|
||||||
p [ _class "pt-3" ] [ rawText "No job listings found for the specified criteria" ]
|
p [ _class "pt-3" ] [ txt "No job listings found for the specified criteria" ]
|
||||||
| Some r ->
|
| Some r ->
|
||||||
table [ _class "table table-sm table-hover pt-3" ] [
|
table [ _class "table table-sm table-hover pt-3" ] [
|
||||||
thead [] [
|
thead [] [
|
||||||
tr [] [
|
tr [] [
|
||||||
th [ _scope "col" ] [ rawText "Listing" ]
|
th [ _scope "col" ] [ txt "Listing" ]
|
||||||
th [ _scope "col" ] [ rawText "Title" ]
|
th [ _scope "col" ] [ txt "Title" ]
|
||||||
th [ _scope "col" ] [ rawText "Location" ]
|
th [ _scope "col" ] [ txt "Location" ]
|
||||||
th [ _scope "col"; _class "text-center" ] [ rawText "Remote?" ]
|
th [ _scope "col"; _class "text-center" ] [ txt "Remote?" ]
|
||||||
th [ _scope "col"; _class "text-center" ] [ rawText "Needed By" ]
|
th [ _scope "col"; _class "text-center" ] [ txt "Needed By" ]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
r |> List.map (fun it ->
|
r |> List.map (fun it ->
|
||||||
tr [] [
|
tr [] [
|
||||||
td [] [ a [ _href $"/listing/{ListingId.toString it.Listing.Id}/view" ] [ rawText "View" ] ]
|
td [] [ a [ _href $"/listing/{ListingId.toString it.Listing.Id}/view" ] [ txt "View" ] ]
|
||||||
td [] [ str it.Listing.Title ]
|
td [] [ str it.Listing.Title ]
|
||||||
td [] [ str it.ContinentName; rawText " / "; str it.Listing.Region ]
|
td [] [ str it.ContinentName; rawText " / "; str it.Listing.Region ]
|
||||||
td [ _class "text-center" ] [ str (yesOrNo it.Listing.IsRemote) ]
|
td [ _class "text-center" ] [ str (yesOrNo it.Listing.IsRemote) ]
|
||||||
td [ _class "text-center" ] [
|
td [ _class "text-center" ] [
|
||||||
match it.Listing.NeededBy with Some needed -> str (neededBy needed) | None -> rawText "N/A"
|
match it.Listing.NeededBy with Some needed -> str (neededBy needed) | None -> txt "N/A"
|
||||||
]
|
]
|
||||||
])
|
])
|
||||||
|> tbody []
|
|> tbody []
|
||||||
|
@ -221,20 +210,18 @@ let view (it : ListingForView) =
|
||||||
str it.Listing.Title
|
str it.Listing.Title
|
||||||
if it.Listing.IsExpired then
|
if it.Listing.IsExpired then
|
||||||
span [ _class "jjj-heading-label" ] [
|
span [ _class "jjj-heading-label" ] [
|
||||||
rawText " "; span [ _class "badge bg-warning text-dark" ] [ rawText "Expired" ]
|
txt " "; span [ _class "badge bg-warning text-dark" ] [ txt "Expired" ]
|
||||||
if defaultArg it.Listing.WasFilledHere false then
|
if defaultArg it.Listing.WasFilledHere false then
|
||||||
rawText " "
|
txt " "; span [ _class "badge bg-success" ] [ txt "Filled via Jobs, Jobs, Jobs" ]
|
||||||
span [ _class "badge bg-success" ] [ rawText "Filled via Jobs, Jobs, Jobs" ]
|
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
h4 [ _class "pb-3 text-muted" ] [ str it.ContinentName; rawText " / "; str it.Listing.Region ]
|
h4 [ _class "pb-3 text-muted" ] [ str it.ContinentName; rawText " / "; str it.Listing.Region ]
|
||||||
p [] [
|
p [] [
|
||||||
match it.Listing.NeededBy with
|
match it.Listing.NeededBy with
|
||||||
| Some needed ->
|
| Some needed ->
|
||||||
strong [] [ em [] [ rawText "NEEDED BY "; str ((neededBy needed).ToUpperInvariant ()) ] ]
|
strong [] [ em [] [ txt "NEEDED BY "; str ((neededBy needed).ToUpperInvariant ()) ] ]; txt " • "
|
||||||
rawText " • "
|
|
||||||
| None -> ()
|
| None -> ()
|
||||||
rawText "Listed by "; strong [ _class "me-4" ] [ str (Citizen.name it.Citizen) ]; br []
|
txt "Listed by "; strong [ _class "me-4" ] [ str (Citizen.name it.Citizen) ]; br []
|
||||||
span [ _class "ms-3" ] []; yield! contactInfo it.Citizen false
|
span [ _class "ms-3" ] []; yield! contactInfo it.Citizen false
|
||||||
]
|
]
|
||||||
hr []
|
hr []
|
||||||
|
|
|
@ -14,29 +14,25 @@ let skillEdit (skills : SkillForm array) =
|
||||||
div [ _id $"skillRow{idx}"; _class "row pb-3" ] [
|
div [ _id $"skillRow{idx}"; _class "row pb-3" ] [
|
||||||
div [ _class "col-2 col-md-1 align-self-center" ] [
|
div [ _class "col-2 col-md-1 align-self-center" ] [
|
||||||
button [ _class "btn btn-sm btn-outline-danger rounded-pill"; _title "Delete"
|
button [ _class "btn btn-sm btn-outline-danger rounded-pill"; _title "Delete"
|
||||||
_onclick $"jjj.profile.removeSkill(idx)" ] [
|
_onclick $"jjj.profile.removeSkill(idx)" ] [ txt " − " ]
|
||||||
rawText " − "
|
|
||||||
]
|
|
||||||
]
|
]
|
||||||
div [ _class "col-10 col-md-6" ] [
|
div [ _class "col-10 col-md-6" ] [
|
||||||
div [ _class "form-floating" ] [
|
div [ _class "form-floating" ] [
|
||||||
input [ _type "text"; _id $"skillDesc{idx}"; _name $"Skills[{idx}].Description"
|
input [ _type "text"; _id $"skillDesc{idx}"; _name $"Skills[{idx}].Description"
|
||||||
_class "form-control"; _placeholder "A skill (language, design technique, process, etc.)"
|
_class "form-control"; _placeholder "A skill (language, design technique, process, etc.)"
|
||||||
_maxlength "200"; _value skill.Description; _required ]
|
_maxlength "200"; _value skill.Description; _required ]
|
||||||
label [ _class "jjj-required"; _for $"skillDesc{idx}" ] [ rawText "Skill" ]
|
label [ _class "jjj-required"; _for $"skillDesc{idx}" ] [ txt "Skill" ]
|
||||||
]
|
]
|
||||||
if idx < 1 then
|
if idx < 1 then div [ _class "form-text" ] [ txt "A skill (language, design technique, process, etc.)" ]
|
||||||
div [ _class "form-text" ] [ rawText "A skill (language, design technique, process, etc.)" ]
|
|
||||||
]
|
]
|
||||||
div [ _class "col-12 col-md-5" ] [
|
div [ _class "col-12 col-md-5" ] [
|
||||||
div [ _class "form-floating" ] [
|
div [ _class "form-floating" ] [
|
||||||
input [ _type "text"; _id $"skillNotes{idx}"; _name $"Skills[{idx}].Notes"; _class "form-control"
|
input [ _type "text"; _id $"skillNotes{idx}"; _name $"Skills[{idx}].Notes"; _class "form-control"
|
||||||
_maxlength "1000"; _placeholder "A further description of the skill (1,000 characters max)"
|
_maxlength "1000"; _placeholder "A further description of the skill (1,000 characters max)"
|
||||||
_value skill.Notes ]
|
_value skill.Notes ]
|
||||||
label [ _class "jjj-label"; _for $"skillNotes{idx}" ] [ rawText "Notes" ]
|
label [ _class "jjj-label"; _for $"skillNotes{idx}" ] [ txt "Notes" ]
|
||||||
]
|
]
|
||||||
if idx < 1 then
|
if idx < 1 then div [ _class "form-text" ] [ txt "A further description of the skill" ]
|
||||||
div [ _class "form-text" ] [ rawText "A further description of the skill" ]
|
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
template [ _id "newSkill" ] [ mapToInputs -1 { Description = ""; Notes = "" } ]
|
template [ _id "newSkill" ] [ mapToInputs -1 { Description = ""; Notes = "" } ]
|
||||||
|
@ -44,18 +40,15 @@ let skillEdit (skills : SkillForm array) =
|
||||||
|
|
||||||
/// The profile edit page
|
/// The profile edit page
|
||||||
let edit (m : EditProfileViewModel) continents isNew citizenId csrf =
|
let edit (m : EditProfileViewModel) continents isNew citizenId csrf =
|
||||||
article [] [
|
pageWithTitle "My Employment Profile" [
|
||||||
h3 [ _class "pb-3" ] [ rawText "My Employment Profile" ]
|
|
||||||
form [ _class "row g-3"; _action "/profile/save"; _hxPost "/profile/save" ] [
|
form [ _class "row g-3"; _action "/profile/save"; _hxPost "/profile/save" ] [
|
||||||
antiForgery csrf
|
antiForgery csrf
|
||||||
div [ _class "col-12" ] [
|
div [ _class "col-12" ] [
|
||||||
checkBox [] (nameof m.IsSeekingEmployment) m.IsSeekingEmployment "I am currently seeking employment"
|
checkBox [] (nameof m.IsSeekingEmployment) m.IsSeekingEmployment "I am currently seeking employment"
|
||||||
if m.IsSeekingEmployment then
|
if m.IsSeekingEmployment then
|
||||||
p [] [
|
p [ _class "fst-italic " ] [
|
||||||
em [] [
|
txt "If you have found employment, consider "
|
||||||
rawText "If you have found employment, consider "
|
a [ _href "/success-story/new/edit" ] [ txt "telling your fellow citizens about it!" ]
|
||||||
a [ _href "/success-story/new/edit" ] [ rawText "telling your fellow citizens about it!" ]
|
|
||||||
]
|
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
div [ _class "col-12 col-sm-6 col-md-4" ] [
|
div [ _class "col-12 col-sm-6 col-md-4" ] [
|
||||||
|
@ -63,7 +56,7 @@ let edit (m : EditProfileViewModel) continents isNew citizenId csrf =
|
||||||
]
|
]
|
||||||
div [ _class "col-12 col-sm-6 col-md-8" ] [
|
div [ _class "col-12 col-sm-6 col-md-8" ] [
|
||||||
textBox [ _type "text"; _maxlength "255" ] (nameof m.Region) m.Region "Region" true
|
textBox [ _type "text"; _maxlength "255" ] (nameof m.Region) m.Region "Region" true
|
||||||
div [ _class "form-text" ] [ rawText "Country, state, geographic area, etc." ]
|
div [ _class "form-text" ] [ txt "Country, state, geographic area, etc." ]
|
||||||
]
|
]
|
||||||
markdownEditor [ _required ] (nameof m.Biography) m.Biography "Professional Biography"
|
markdownEditor [ _required ] (nameof m.Biography) m.Biography "Professional Biography"
|
||||||
div [ _class "col-12 col-offset-md-2 col-md-4" ] [
|
div [ _class "col-12 col-offset-md-2 col-md-4" ] [
|
||||||
|
@ -75,22 +68,19 @@ let edit (m : EditProfileViewModel) continents isNew citizenId csrf =
|
||||||
div [ _class "col-12" ] [
|
div [ _class "col-12" ] [
|
||||||
hr []
|
hr []
|
||||||
h4 [ _class "pb-2" ] [
|
h4 [ _class "pb-2" ] [
|
||||||
rawText "Skills "
|
txt "Skills "
|
||||||
button [ _type "button"; _class "btn btn-sm btn-outline-primary rounded-pill"
|
button [ _type "button"; _class "btn btn-sm btn-outline-primary rounded-pill"
|
||||||
_onclick "jjj.profile.addSkill()" ] [
|
_onclick "jjj.profile.addSkill()" ] [ txt "Add a Skill" ]
|
||||||
rawText "Add a Skill"
|
|
||||||
]
|
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
yield! skillEdit m.Skills
|
yield! skillEdit m.Skills
|
||||||
div [ _class "col-12" ] [
|
div [ _class "col-12" ] [
|
||||||
hr []
|
hr []
|
||||||
h4 [] [ rawText "Experience" ]
|
h4 [] [ txt "Experience" ]
|
||||||
p [] [
|
p [] [
|
||||||
rawText "This application does not have a place to individually list your chronological job "
|
txt "This application does not have a place to individually list your chronological job history; "
|
||||||
rawText "history; however, you can use this area to list prior jobs, their dates, and anything "
|
txt "however, you can use this area to list prior jobs, their dates, and anything else you want to "
|
||||||
rawText "else you want to include that’s not already a part of your Professional Biography "
|
txt "include that’s not already a part of your Professional Biography above."
|
||||||
rawText "above."
|
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
markdownEditor [] (nameof m.Experience) (defaultArg m.Experience "") "Experience"
|
markdownEditor [] (nameof m.Experience) (defaultArg m.Experience "") "Experience"
|
||||||
|
@ -103,21 +93,19 @@ let edit (m : EditProfileViewModel) continents isNew citizenId csrf =
|
||||||
"Show my profile to anyone who has the direct link to it"
|
"Show my profile to anyone who has the direct link to it"
|
||||||
]
|
]
|
||||||
div [ _class "col-12" ] [
|
div [ _class "col-12" ] [
|
||||||
button [ _type "submit"; _class "btn btn-primary" ] [
|
submitButton "content-save-outline" "Save"
|
||||||
i [ _class "mdi mdi-content-save-outline" ] []; rawText " Save"
|
|
||||||
]
|
|
||||||
if not isNew then
|
if not isNew then
|
||||||
rawText " "
|
txt " "
|
||||||
a [ _class "btn btn-outline-secondary"; _href $"/profile/{CitizenId.toString citizenId}/view" ] [
|
a [ _class "btn btn-outline-secondary"; _href $"/profile/{CitizenId.toString citizenId}/view" ] [
|
||||||
i [ _color "#6c757d"; _class "mdi mdi-file-account-outline" ] []
|
i [ _color "#6c757d"; _class "mdi mdi-file-account-outline" ] []
|
||||||
rawText " View Your User Profile"
|
txt " View Your User Profile"
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
hr []
|
hr []
|
||||||
p [ _class "text-muted fst-italic" ] [
|
p [ _class "text-muted fst-italic" ] [
|
||||||
rawText "(If you want to delete your profile, or your entire account, "
|
txt "(If you want to delete your profile, or your entire account, "
|
||||||
a [ _href "/citizen/so-long" ] [ rawText "see your deletion options here" ]; rawText ".)"
|
a [ _href "/citizen/so-long" ] [ txt "see your deletion options here" ]; txt ".)"
|
||||||
]
|
]
|
||||||
jsOnLoad $"jjj.profile.nextIndex = {m.Skills.Length}"
|
jsOnLoad $"jjj.profile.nextIndex = {m.Skills.Length}"
|
||||||
]
|
]
|
||||||
|
@ -125,12 +113,11 @@ let edit (m : EditProfileViewModel) continents isNew citizenId csrf =
|
||||||
|
|
||||||
/// The public search page
|
/// The public search page
|
||||||
let publicSearch (m : PublicSearchForm) continents (results : PublicSearchResult list option) =
|
let publicSearch (m : PublicSearchForm) continents (results : PublicSearchResult list option) =
|
||||||
article [] [
|
pageWithTitle "People Seeking Work" [
|
||||||
h3 [ _class "pb-3" ] [ rawText "People Seeking Work" ]
|
|
||||||
if Option.isNone results then
|
if Option.isNone results then
|
||||||
p [] [
|
p [] [
|
||||||
rawText "Enter one or more criteria to filter results, or just click “Search” to list all "
|
txt "Enter one or more criteria to filter results, or just click “Search” to list all "
|
||||||
rawText "publicly searchable profiles."
|
txt "publicly searchable profiles."
|
||||||
]
|
]
|
||||||
collapsePanel "Search Criteria" [
|
collapsePanel "Search Criteria" [
|
||||||
form [ _class "container"; _method "GET"; _action "/profile/seeking" ] [
|
form [ _class "container"; _method "GET"; _action "/profile/seeking" ] [
|
||||||
|
@ -141,62 +128,62 @@ let publicSearch (m : PublicSearchForm) continents (results : PublicSearchResult
|
||||||
]
|
]
|
||||||
div [ _class "col-12 col-sm-6 col-md-4 col-lg-3" ] [
|
div [ _class "col-12 col-sm-6 col-md-4 col-lg-3" ] [
|
||||||
textBox [ _maxlength "1000" ] (nameof m.Region) m.Region "Region" false
|
textBox [ _maxlength "1000" ] (nameof m.Region) m.Region "Region" false
|
||||||
div [ _class "form-text" ] [ rawText "(free-form text)" ]
|
div [ _class "form-text" ] [ txt "(free-form text)" ]
|
||||||
]
|
]
|
||||||
div [ _class "col-12 col-sm-6 col-offset-md-2 col-lg-3 col-offset-lg-0" ] [
|
div [ _class "col-12 col-sm-6 col-offset-md-2 col-lg-3 col-offset-lg-0" ] [
|
||||||
label [ _class "jjj-label" ] [ rawText "Seeking Remote Work?" ]; br []
|
label [ _class "jjj-label" ] [ txt "Seeking Remote Work?" ]; br []
|
||||||
div [ _class "form-check form-check-inline" ] [
|
div [ _class "form-check form-check-inline" ] [
|
||||||
input [ _type "radio"; _id "remoteNull"; _name (nameof m.RemoteWork); _value ""
|
input [ _type "radio"; _id "remoteNull"; _name (nameof m.RemoteWork); _value ""
|
||||||
_class "form-check-input"; if m.RemoteWork = "" then _checked ]
|
_class "form-check-input"; if m.RemoteWork = "" then _checked ]
|
||||||
label [ _class "form-check-label"; _for "remoteNull" ] [ rawText "No Selection" ]
|
label [ _class "form-check-label"; _for "remoteNull" ] [ txt "No Selection" ]
|
||||||
]
|
]
|
||||||
div [ _class "form-check form-check-inline" ] [
|
div [ _class "form-check form-check-inline" ] [
|
||||||
input [ _type "radio"; _id "remoteYes"; _name (nameof m.RemoteWork); _value "yes"
|
input [ _type "radio"; _id "remoteYes"; _name (nameof m.RemoteWork); _value "yes"
|
||||||
_class "form-check-input"; if m.RemoteWork = "yes" then _checked ]
|
_class "form-check-input"; if m.RemoteWork = "yes" then _checked ]
|
||||||
label [ _class "form-check-label"; _for "remoteYes" ] [ rawText "Yes" ]
|
label [ _class "form-check-label"; _for "remoteYes" ] [ txt "Yes" ]
|
||||||
]
|
]
|
||||||
div [ _class "form-check form-check-inline" ] [
|
div [ _class "form-check form-check-inline" ] [
|
||||||
input [ _type "radio"; _id "remoteNo"; _name (nameof m.RemoteWork); _value "no"
|
input [ _type "radio"; _id "remoteNo"; _name (nameof m.RemoteWork); _value "no"
|
||||||
_class "form-check-input"; if m.RemoteWork = "no" then _checked ]
|
_class "form-check-input"; if m.RemoteWork = "no" then _checked ]
|
||||||
label [ _class "form-check-label"; _for "remoteNo" ] [ rawText "No" ]
|
label [ _class "form-check-label"; _for "remoteNo" ] [ txt "No" ]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
div [ _class "col-12 col-sm-6 col-lg-3" ] [
|
div [ _class "col-12 col-sm-6 col-lg-3" ] [
|
||||||
textBox [ _maxlength "1000" ] (nameof m.Skill) m.Skill "Skill" false
|
textBox [ _maxlength "1000" ] (nameof m.Skill) m.Skill "Skill" false
|
||||||
div [ _class "form-text" ] [ rawText "(free-form text)" ]
|
div [ _class "form-text" ] [ txt "(free-form text)" ]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
div [ _class "row" ] [
|
div [ _class "row" ] [
|
||||||
div [ _class "col" ] [
|
div [ _class "col" ] [
|
||||||
br []
|
br []
|
||||||
button [ _type "submit"; _class "btn btn-outline-primary" ] [ rawText "Search" ]
|
button [ _type "submit"; _class "btn btn-outline-primary" ] [ txt "Search" ]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
match results with
|
match results with
|
||||||
| Some r when List.isEmpty r -> p [ _class "pt-3" ] [ rawText "No results found for the specified criteria" ]
|
| Some r when List.isEmpty r -> p [ _class "pt-3" ] [ txt "No results found for the specified criteria" ]
|
||||||
| Some r ->
|
| Some r ->
|
||||||
p [ _class "py-3" ] [
|
p [ _class "py-3" ] [
|
||||||
rawText "These profiles match your search criteria. To learn more about these people, join the merry "
|
txt "These profiles match your search criteria. To learn more about these people, join the merry band "
|
||||||
rawText "band of human resources in the "
|
txt "of human resources in the "
|
||||||
a [ _href "https://noagendashow.net"; _target "_blank"; _rel "noopener" ] [ rawText "No Agenda" ]
|
a [ _href "https://noagendashow.net"; _target "_blank"; _rel "noopener" ] [ txt "No Agenda" ]
|
||||||
rawText " tribe!"
|
txt " tribe!"
|
||||||
]
|
]
|
||||||
table [ _class "table table-sm table-hover" ] [
|
table [ _class "table table-sm table-hover" ] [
|
||||||
thead [] [
|
thead [] [
|
||||||
tr [] [
|
tr [] [
|
||||||
th [ _scope "col" ] [ rawText "Continent" ]
|
th [ _scope "col" ] [ txt "Continent" ]
|
||||||
th [ _scope "col"; _class "text-center" ] [ rawText "Region" ]
|
th [ _scope "col"; _class "text-center" ] [ txt "Region" ]
|
||||||
th [ _scope "col"; _class "text-center" ] [ rawText "Remote?" ]
|
th [ _scope "col"; _class "text-center" ] [ txt "Remote?" ]
|
||||||
th [ _scope "col"; _class "text-center" ] [ rawText "Skills" ]
|
th [ _scope "col"; _class "text-center" ] [ txt "Skills" ]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
r |> List.map (fun profile ->
|
r |> List.map (fun profile ->
|
||||||
tr [] [
|
tr [] [
|
||||||
td [] [ str profile.Continent ]
|
td [] [ str profile.Continent ]
|
||||||
td [] [ str profile.Region ]
|
td [] [ str profile.Region ]
|
||||||
td [ _class "text-center" ] [ rawText (yesOrNo profile.RemoteWork) ]
|
td [ _class "text-center" ] [ txt (yesOrNo profile.RemoteWork) ]
|
||||||
profile.Skills
|
profile.Skills
|
||||||
|> List.collect (fun skill -> [ str skill; br [] ])
|
|> List.collect (fun skill -> [ str skill; br [] ])
|
||||||
|> td []
|
|> td []
|
||||||
|
@ -209,12 +196,11 @@ let publicSearch (m : PublicSearchForm) continents (results : PublicSearchResult
|
||||||
|
|
||||||
/// Logged-on search page
|
/// Logged-on search page
|
||||||
let search (m : ProfileSearchForm) continents tz (results : ProfileSearchResult list option) =
|
let search (m : ProfileSearchForm) continents tz (results : ProfileSearchResult list option) =
|
||||||
article [] [
|
pageWithTitle "Search Profiles" [
|
||||||
h3 [ _class "pb-3" ] [ rawText "Search Profiles" ]
|
|
||||||
if Option.isNone results then
|
if Option.isNone results then
|
||||||
p [] [
|
p [] [
|
||||||
rawText "Enter one or more criteria to filter results, or just click “Search” to list all "
|
txt "Enter one or more criteria to filter results, or just click “Search” to list all "
|
||||||
rawText "profiles."
|
txt "profiles."
|
||||||
]
|
]
|
||||||
collapsePanel "Search Criteria" [
|
collapsePanel "Search Criteria" [
|
||||||
form [ _class "container"; _method "GET"; _action "/profile/search" ] [
|
form [ _class "container"; _method "GET"; _action "/profile/search" ] [
|
||||||
|
@ -224,63 +210,63 @@ let search (m : ProfileSearchForm) continents tz (results : ProfileSearchResult
|
||||||
continentList [] "ContinentId" continents (Some "Any") m.ContinentId false
|
continentList [] "ContinentId" continents (Some "Any") m.ContinentId false
|
||||||
]
|
]
|
||||||
div [ _class "col-12 col-sm-6 col-offset-md-2 col-lg-3 col-offset-lg-0" ] [
|
div [ _class "col-12 col-sm-6 col-offset-md-2 col-lg-3 col-offset-lg-0" ] [
|
||||||
label [ _class "jjj-label" ] [ rawText "Seeking Remote Work?" ]; br []
|
label [ _class "jjj-label" ] [ txt "Seeking Remote Work?" ]; br []
|
||||||
div [ _class "form-check form-check-inline" ] [
|
div [ _class "form-check form-check-inline" ] [
|
||||||
input [ _type "radio"; _id "remoteNull"; _name (nameof m.RemoteWork); _value ""
|
input [ _type "radio"; _id "remoteNull"; _name (nameof m.RemoteWork); _value ""
|
||||||
_class "form-check-input"; if m.RemoteWork = "" then _checked ]
|
_class "form-check-input"; if m.RemoteWork = "" then _checked ]
|
||||||
label [ _class "form-check-label"; _for "remoteNull" ] [ rawText "No Selection" ]
|
label [ _class "form-check-label"; _for "remoteNull" ] [ txt "No Selection" ]
|
||||||
]
|
]
|
||||||
div [ _class "form-check form-check-inline" ] [
|
div [ _class "form-check form-check-inline" ] [
|
||||||
input [ _type "radio"; _id "remoteYes"; _name (nameof m.RemoteWork); _value "yes"
|
input [ _type "radio"; _id "remoteYes"; _name (nameof m.RemoteWork); _value "yes"
|
||||||
_class "form-check-input"; if m.RemoteWork = "yes" then _checked ]
|
_class "form-check-input"; if m.RemoteWork = "yes" then _checked ]
|
||||||
label [ _class "form-check-label"; _for "remoteYes" ] [ rawText "Yes" ]
|
label [ _class "form-check-label"; _for "remoteYes" ] [ txt "Yes" ]
|
||||||
]
|
]
|
||||||
div [ _class "form-check form-check-inline" ] [
|
div [ _class "form-check form-check-inline" ] [
|
||||||
input [ _type "radio"; _id "remoteNo"; _name (nameof m.RemoteWork); _value "no"
|
input [ _type "radio"; _id "remoteNo"; _name (nameof m.RemoteWork); _value "no"
|
||||||
_class "form-check-input"; if m.RemoteWork = "no" then _checked ]
|
_class "form-check-input"; if m.RemoteWork = "no" then _checked ]
|
||||||
label [ _class "form-check-label"; _for "remoteNo" ] [ rawText "No" ]
|
label [ _class "form-check-label"; _for "remoteNo" ] [ txt "No" ]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
div [ _class "col-12 col-sm-6 col-lg-3" ] [
|
div [ _class "col-12 col-sm-6 col-lg-3" ] [
|
||||||
textBox [ _maxlength "1000" ] (nameof m.Skill) m.Skill "Skill" false
|
textBox [ _maxlength "1000" ] (nameof m.Skill) m.Skill "Skill" false
|
||||||
div [ _class "form-text" ] [ rawText "(free-form text)" ]
|
div [ _class "form-text" ] [ txt "(free-form text)" ]
|
||||||
]
|
]
|
||||||
div [ _class "col-12 col-sm-6 col-lg-3" ] [
|
div [ _class "col-12 col-sm-6 col-lg-3" ] [
|
||||||
textBox [ _maxlength "1000" ] (nameof m.BioExperience) m.BioExperience "Bio / Experience" false
|
textBox [ _maxlength "1000" ] (nameof m.BioExperience) m.BioExperience "Bio / Experience" false
|
||||||
div [ _class "form-text" ] [ rawText "(free-form text)" ]
|
div [ _class "form-text" ] [ txt "(free-form text)" ]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
div [ _class "row" ] [
|
div [ _class "row" ] [
|
||||||
div [ _class "col" ] [
|
div [ _class "col" ] [
|
||||||
br []
|
br []
|
||||||
button [ _type "submit"; _class "btn btn-outline-primary" ] [ rawText "Search" ]
|
button [ _type "submit"; _class "btn btn-outline-primary" ] [ txt "Search" ]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
match results with
|
match results with
|
||||||
| Some r when List.isEmpty r -> p [ _class "pt-3" ] [ rawText "No results found for the specified criteria" ]
|
| Some r when List.isEmpty r -> p [ _class "pt-3" ] [ txt "No results found for the specified criteria" ]
|
||||||
| Some r ->
|
| Some r ->
|
||||||
// Bootstrap utility classes to only show at medium or above
|
// Bootstrap utility classes to only show at medium or above
|
||||||
let isWide = "d-none d-md-table-cell"
|
let isWide = "d-none d-md-table-cell"
|
||||||
table [ _class "table table-sm table-hover pt-3" ] [
|
table [ _class "table table-sm table-hover pt-3" ] [
|
||||||
thead [] [
|
thead [] [
|
||||||
tr [] [
|
tr [] [
|
||||||
th [ _scope "col" ] [ rawText "Profile" ]
|
th [ _scope "col" ] [ txt "Profile" ]
|
||||||
th [ _scope "col" ] [ rawText "Name" ]
|
th [ _scope "col" ] [ txt "Name" ]
|
||||||
th [ _scope "col"; _class $"{isWide} text-center" ] [ rawText "Seeking?" ]
|
th [ _scope "col"; _class $"{isWide} text-center" ] [ txt "Seeking?" ]
|
||||||
th [ _scope "col"; _class "text-center" ] [ rawText "Remote?" ]
|
th [ _scope "col"; _class "text-center" ] [ txt "Remote?" ]
|
||||||
th [ _scope "col"; _class $"{isWide} text-center" ] [ rawText "Full-Time?" ]
|
th [ _scope "col"; _class $"{isWide} text-center" ] [ txt "Full-Time?" ]
|
||||||
th [ _scope "col"; _class isWide ] [ rawText "Last Updated" ]
|
th [ _scope "col"; _class isWide ] [ txt "Last Updated" ]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
r |> List.map (fun profile ->
|
r |> List.map (fun profile ->
|
||||||
tr [] [
|
tr [] [
|
||||||
td [] [ a [ _href $"/profile/{CitizenId.toString profile.CitizenId}/view" ] [ rawText "View" ] ]
|
td [] [ a [ _href $"/profile/{CitizenId.toString profile.CitizenId}/view" ] [ txt "View" ] ]
|
||||||
td [ if profile.SeekingEmployment then _class "fw-bold" ] [ str profile.DisplayName ]
|
td [ if profile.SeekingEmployment then _class "fw-bold" ] [ str profile.DisplayName ]
|
||||||
td [ _class $"{isWide} text-center" ] [ rawText (yesOrNo profile.SeekingEmployment) ]
|
td [ _class $"{isWide} text-center" ] [ txt (yesOrNo profile.SeekingEmployment) ]
|
||||||
td [ _class "text-center" ] [ rawText (yesOrNo profile.RemoteWork) ]
|
td [ _class "text-center" ] [ txt (yesOrNo profile.RemoteWork) ]
|
||||||
td [ _class $"{isWide} text-center" ] [ rawText (yesOrNo profile.FullTime) ]
|
td [ _class $"{isWide} text-center" ] [ txt (yesOrNo profile.FullTime) ]
|
||||||
td [ _class isWide ] [ str (fullDate profile.LastUpdatedOn tz) ]
|
td [ _class isWide ] [ str (fullDate profile.LastUpdatedOn tz) ]
|
||||||
])
|
])
|
||||||
|> tbody []
|
|> tbody []
|
||||||
|
@ -296,38 +282,34 @@ let view (citizen : Citizen) (profile : Profile) (continentName : string) curren
|
||||||
str (Citizen.name citizen)
|
str (Citizen.name citizen)
|
||||||
if profile.IsSeekingEmployment then
|
if profile.IsSeekingEmployment then
|
||||||
span [ _class "jjj-heading-label" ] [
|
span [ _class "jjj-heading-label" ] [
|
||||||
rawText " "; span [ _class "badge bg-dark" ] [ rawText "Currently Seeking Employment" ]
|
txt " "; span [ _class "badge bg-dark" ] [ txt "Currently Seeking Employment" ]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
h4 [] [ str $"{continentName}, {profile.Region}" ]
|
h4 [] [ str $"{continentName}, {profile.Region}" ]
|
||||||
contactInfo citizen (Option.isNone currentId)
|
contactInfo citizen (Option.isNone currentId)
|
||||||
|> div [ _class "pb-3" ]
|
|> div [ _class "pb-3" ]
|
||||||
p [] [
|
p [] [
|
||||||
rawText (if profile.IsFullTime then "I" else "Not i"); rawText "nterested in full-time employment"
|
txt (if profile.IsFullTime then "I" else "Not i"); txt "nterested in full-time employment • "
|
||||||
rawText " • "
|
txt (if profile.IsRemote then "I" else "Not i"); txt "nterested in remote opportunities"
|
||||||
rawText (if profile.IsRemote then "I" else "Not i"); rawText "nterested in remote opportunities"
|
|
||||||
]
|
]
|
||||||
hr []
|
hr []
|
||||||
div [] [ md2html profile.Biography ]
|
div [] [ md2html profile.Biography ]
|
||||||
if not (List.isEmpty profile.Skills) then
|
if not (List.isEmpty profile.Skills) then
|
||||||
hr []
|
hr []
|
||||||
h4 [ _class "pb-3" ] [ rawText "Skills" ]
|
h4 [ _class "pb-3" ] [ txt "Skills" ]
|
||||||
profile.Skills
|
profile.Skills
|
||||||
|> List.map (fun skill ->
|
|> List.map (fun skill ->
|
||||||
li [] [
|
li [] [
|
||||||
str skill.Description
|
str skill.Description
|
||||||
match skill.Notes with
|
match skill.Notes with Some notes -> txt " ("; str notes; txt ")" | None -> ()
|
||||||
| Some notes ->
|
|
||||||
rawText " ("; str notes; rawText ")"
|
|
||||||
| None -> ()
|
|
||||||
])
|
])
|
||||||
|> ul []
|
|> ul []
|
||||||
match profile.Experience with
|
match profile.Experience with
|
||||||
| Some exp -> hr []; h4 [ _class "pb-3" ] [ rawText "Experience / Employment History" ]; div [] [ md2html exp ]
|
| Some exp -> hr []; h4 [ _class "pb-3" ] [ txt "Experience / Employment History" ]; div [] [ md2html exp ]
|
||||||
| None -> ()
|
| None -> ()
|
||||||
if Option.isSome currentId && currentId.Value = citizen.Id then
|
if Option.isSome currentId && currentId.Value = citizen.Id then
|
||||||
br []; br []
|
br []; br []
|
||||||
a [ _href "/profile/edit"; _class "btn btn-primary" ] [
|
a [ _href "/profile/edit"; _class "btn btn-primary" ] [
|
||||||
i [ _class "mdi mdi-pencil" ] []; rawText " Edit Your Profile"
|
i [ _class "mdi mdi-pencil" ] []; txt " Edit Your Profile"
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
|
@ -9,13 +9,12 @@ open JobsJobsJobs.ViewModels
|
||||||
|
|
||||||
/// The add/edit success story page
|
/// The add/edit success story page
|
||||||
let edit (m : EditSuccessForm) isNew pgTitle csrf =
|
let edit (m : EditSuccessForm) isNew pgTitle csrf =
|
||||||
article [] [
|
pageWithTitle pgTitle [
|
||||||
h3 [ _class "pb-3" ] [ rawText pgTitle ]
|
|
||||||
if isNew then
|
if isNew then
|
||||||
p [] [
|
p [] [
|
||||||
rawText "Congratulations on your employment! Your fellow citizens would enjoy hearing how it all came "
|
txt "Congratulations on your employment! Your fellow citizens would enjoy hearing how it all came "
|
||||||
rawText "about; tell us about it below! "
|
txt "about; tell us about it below! "
|
||||||
em [] [ rawText "(These will be visible to other users, but not to the general public.)" ]
|
em [] [ txt "(These will be visible to other users, but not to the general public.)" ]
|
||||||
]
|
]
|
||||||
form [ _class "row g-3"; _method "POST"; _action "/success-story/save" ] [
|
form [ _class "row g-3"; _method "POST"; _action "/success-story/save" ] [
|
||||||
antiForgery csrf
|
antiForgery csrf
|
||||||
|
@ -25,13 +24,11 @@ let edit (m : EditSuccessForm) isNew pgTitle csrf =
|
||||||
]
|
]
|
||||||
markdownEditor [] (nameof m.Story) m.Story "The Success Story"
|
markdownEditor [] (nameof m.Story) m.Story "The Success Story"
|
||||||
div [ _class "col-12" ] [
|
div [ _class "col-12" ] [
|
||||||
button [ _type "submit"; _class "btn btn-primary" ] [
|
submitButton "content-save-outline" "Save"
|
||||||
i [ _class "mdi mdi-content-save-outline" ] []; rawText " Save"
|
|
||||||
]
|
|
||||||
if isNew then
|
if isNew then
|
||||||
p [ _class "fst-italic" ] [
|
p [ _class "fst-italic" ] [
|
||||||
rawText "(Saving this will set “Seeking Employment” to “No” on your "
|
txt "(Saving this will set “Seeking Employment” to “No” on your "
|
||||||
rawText "profile.)"
|
txt "profile.)"
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
@ -40,28 +37,27 @@ let edit (m : EditSuccessForm) isNew pgTitle csrf =
|
||||||
|
|
||||||
/// The list of success stories
|
/// The list of success stories
|
||||||
let list (m : StoryEntry list) citizenId tz =
|
let list (m : StoryEntry list) citizenId tz =
|
||||||
article [] [
|
pageWithTitle "Success Stories" [
|
||||||
h3 [ _class "pb-3" ] [ rawText "Success Stories" ]
|
|
||||||
if List.isEmpty m then
|
if List.isEmpty m then
|
||||||
p [] [ rawText "There are no success stories recorded "; em [] [ rawText "(yet)" ] ]
|
p [] [ txt "There are no success stories recorded "; em [] [ txt "(yet)" ] ]
|
||||||
else
|
else
|
||||||
table [ _class "table table-sm table-hover" ] [
|
table [ _class "table table-sm table-hover" ] [
|
||||||
thead [] [
|
thead [] [
|
||||||
[ "Story"; "From"; "Found Here?"; "Recorded On" ]
|
[ "Story"; "From"; "Found Here?"; "Recorded On" ]
|
||||||
|> List.map (fun it -> th [ _scope "col" ] [ rawText it ])
|
|> List.map (fun it -> th [ _scope "col" ] [ txt it ])
|
||||||
|> tr []
|
|> tr []
|
||||||
]
|
]
|
||||||
m |> List.map (fun story ->
|
m |> List.map (fun story ->
|
||||||
tr [] [
|
tr [] [
|
||||||
td [] [
|
td [] [
|
||||||
let theId = SuccessId.toString story.Id
|
let theId = SuccessId.toString story.Id
|
||||||
if story.HasStory then a [ _href $"/success-story/{theId}/view" ] [ rawText "View" ]
|
if story.HasStory then a [ _href $"/success-story/{theId}/view" ] [ txt "View" ]
|
||||||
else em [] [ rawText "None" ]
|
else em [] [ txt "None" ]
|
||||||
if story.CitizenId = citizenId then
|
if story.CitizenId = citizenId then
|
||||||
rawText " ~ "; a [ _href $"/success-story/{theId}/edit" ] [ rawText "Edit" ]
|
txt " ~ "; a [ _href $"/success-story/{theId}/edit" ] [ txt "Edit" ]
|
||||||
]
|
]
|
||||||
td [] [ str story.CitizenName ]
|
td [] [ str story.CitizenName ]
|
||||||
td [] [ if story.FromHere then strong [] [ rawText "Yes" ] else rawText "No" ]
|
td [] [ if story.FromHere then strong [] [ txt "Yes" ] else txt "No" ]
|
||||||
td [] [ str (fullDate story.RecordedOn tz) ]
|
td [] [ str (fullDate story.RecordedOn tz) ]
|
||||||
])
|
])
|
||||||
|> tbody []
|
|> tbody []
|
||||||
|
@ -73,14 +69,13 @@ let list (m : StoryEntry list) citizenId tz =
|
||||||
let view (it : Success) citizenName tz =
|
let view (it : Success) citizenName tz =
|
||||||
article [] [
|
article [] [
|
||||||
h3 [] [
|
h3 [] [
|
||||||
str citizenName; rawText "’s Success Story"
|
str citizenName; txt "’s Success Story"
|
||||||
if it.IsFromHere then
|
if it.IsFromHere then
|
||||||
span [ _class "jjj-heading-label" ] [
|
span [ _class "jjj-heading-label" ] [
|
||||||
rawText " "
|
txt " "
|
||||||
span [ _class "badge bg-success" ] [
|
span [ _class "badge bg-success" ] [
|
||||||
rawText "Via "
|
txt "Via "; txt (if it.Source = "profile" then "employment profile" else "job listing")
|
||||||
rawText (if it.Source = "profile" then "employment profile" else "job listing")
|
txt " on Jobs, Jobs, Jobs"
|
||||||
rawText " on Jobs, Jobs, Jobs"
|
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
Loading…
Reference in New Issue
Block a user