Version 3 #40
@ -58,16 +58,16 @@ module private CacheHelpers =
|
||||
|
||||
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 () =
|
||||
|
||||
// ~~~ INITIALIZATION ~~~
|
||||
|
||||
do
|
||||
task {
|
||||
let conn = connection ()
|
||||
let dataSource = dataSource ()
|
||||
let! exists =
|
||||
conn
|
||||
dataSource
|
||||
|> Sql.query $"
|
||||
SELECT EXISTS
|
||||
(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")
|
||||
if not exists then
|
||||
let! _ =
|
||||
conn
|
||||
dataSource
|
||||
|> Sql.query
|
||||
"CREATE TABLE session (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
@ -92,10 +92,10 @@ type DistributedCache () =
|
||||
|
||||
/// Get an entry, updating it for sliding expiration
|
||||
let getEntry key = backgroundTask {
|
||||
let conn = connection ()
|
||||
let dataSource = dataSource ()
|
||||
let idParam = "@id", Sql.string key
|
||||
let! tryEntry =
|
||||
conn
|
||||
dataSource
|
||||
|> Sql.query "SELECT * FROM session WHERE id = @id"
|
||||
|> Sql.parameters [ idParam ]
|
||||
|> Sql.executeAsync (fun row ->
|
||||
@ -118,7 +118,7 @@ type DistributedCache () =
|
||||
else true, { entry with ExpireAt = now.Plus slideExp }
|
||||
if needsRefresh then
|
||||
let! _ =
|
||||
conn
|
||||
dataSource
|
||||
|> Sql.query "UPDATE session SET expire_at = @expireAt WHERE id = @id"
|
||||
|> Sql.parameters [ expireParam item.ExpireAt; idParam ]
|
||||
|> Sql.executeNonQueryAsync
|
||||
@ -135,7 +135,7 @@ type DistributedCache () =
|
||||
let now = getNow ()
|
||||
if lastPurge.Plus (Duration.FromMinutes 30L) < now then
|
||||
let! _ =
|
||||
connection ()
|
||||
dataSource ()
|
||||
|> Sql.query "DELETE FROM session WHERE expire_at < @expireAt"
|
||||
|> Sql.parameters [ expireParam now ]
|
||||
|> Sql.executeNonQueryAsync
|
||||
@ -145,7 +145,7 @@ type DistributedCache () =
|
||||
/// Remove a cache entry
|
||||
let removeEntry key = backgroundTask {
|
||||
let! _ =
|
||||
connection ()
|
||||
dataSource ()
|
||||
|> Sql.query "DELETE FROM session WHERE id = @id"
|
||||
|> Sql.parameters [ "@id", Sql.string key ]
|
||||
|> Sql.executeNonQueryAsync
|
||||
@ -170,7 +170,7 @@ type DistributedCache () =
|
||||
let slide = Duration.FromHours 1
|
||||
now.Plus slide, Some slide, None
|
||||
let! _ =
|
||||
connection ()
|
||||
dataSource ()
|
||||
|> Sql.query
|
||||
"INSERT INTO session (
|
||||
id, payload, expire_at, sliding_expiration, absolute_expiration
|
||||
|
@ -37,12 +37,12 @@ module DataConnection =
|
||||
open Npgsql
|
||||
|
||||
/// The data source for the document store
|
||||
let mutable private dataSource : NpgsqlDataSource option = None
|
||||
let mutable private theDataSource : NpgsqlDataSource option = None
|
||||
|
||||
/// Get a connection
|
||||
let connection () =
|
||||
match dataSource with
|
||||
| Some ds -> ds.OpenConnection () |> Sql.existingConnection
|
||||
/// Get the data source as the start of a SQL statement
|
||||
let dataSource () =
|
||||
match theDataSource with
|
||||
| Some ds -> Sql.fromDataSource ds
|
||||
| None -> invalidOp "Connection.setUp() must be called before accessing the database"
|
||||
|
||||
/// Create tables
|
||||
@ -66,7 +66,7 @@ module DataConnection =
|
||||
$"CREATE INDEX IF NOT EXISTS idx_success_citizen ON {Table.Success} ((data -> 'citizenId'))"
|
||||
]
|
||||
let! _ =
|
||||
connection ()
|
||||
dataSource ()
|
||||
|> Sql.executeTransactionAsync (sql |> List.map (fun sql -> sql, [ [] ]))
|
||||
()
|
||||
}
|
||||
@ -75,7 +75,7 @@ module DataConnection =
|
||||
let setUp (cfg : IConfiguration) = backgroundTask {
|
||||
let builder = NpgsqlDataSourceBuilder (cfg.GetConnectionString "PostgreSQL")
|
||||
let _ = builder.UseNodaTime ()
|
||||
dataSource <- Some (builder.Build ())
|
||||
theDataSource <- Some (builder.Build ())
|
||||
do! createTables ()
|
||||
}
|
||||
|
||||
@ -166,7 +166,7 @@ module Citizens =
|
||||
|
||||
/// Delete a citizen by their ID
|
||||
let deleteById citizenId =
|
||||
doDeleteById citizenId (connection ())
|
||||
doDeleteById citizenId (dataSource ())
|
||||
|
||||
/// Save a citizen
|
||||
let private saveCitizen (citizen : Citizen) connProps =
|
||||
@ -178,7 +178,7 @@ module Citizens =
|
||||
|
||||
/// Purge expired tokens
|
||||
let private purgeExpiredTokens now = backgroundTask {
|
||||
let connProps = connection ()
|
||||
let connProps = dataSource ()
|
||||
let! info =
|
||||
Sql.query $"SELECT * FROM {Table.SecurityInfo} WHERE data ->> 'tokenExpires' IS NOT NULL" connProps
|
||||
|> Sql.executeAsync toDocument<SecurityInfo>
|
||||
@ -202,7 +202,7 @@ module Citizens =
|
||||
|
||||
/// Find a citizen by their ID
|
||||
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 _
|
||||
| None -> return None
|
||||
@ -210,11 +210,11 @@ module Citizens =
|
||||
|
||||
/// Save a 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
|
||||
let register citizen (security : SecurityInfo) = backgroundTask {
|
||||
let connProps = connection ()
|
||||
let connProps = dataSource ()
|
||||
use conn = Sql.createConnection connProps
|
||||
use! txn = conn.BeginTransactionAsync ()
|
||||
try
|
||||
@ -245,7 +245,7 @@ module Citizens =
|
||||
/// Confirm a citizen's account
|
||||
let confirmAccount token = backgroundTask {
|
||||
do! checkForPurge true
|
||||
let connProps = connection ()
|
||||
let connProps = dataSource ()
|
||||
match! tryConfirmToken token connProps with
|
||||
| Some info ->
|
||||
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)
|
||||
let denyAccount token = backgroundTask {
|
||||
do! checkForPurge true
|
||||
let connProps = connection ()
|
||||
let connProps = dataSource ()
|
||||
match! tryConfirmToken token connProps with
|
||||
| Some info ->
|
||||
do! doDeleteById info.Id connProps
|
||||
@ -269,7 +269,7 @@ module Citizens =
|
||||
let tryLogOn email password (pwVerify : Citizen -> string -> bool option) (pwHash : Citizen -> string -> string)
|
||||
now = backgroundTask {
|
||||
do! checkForPurge false
|
||||
let connProps = connection ()
|
||||
let connProps = dataSource ()
|
||||
let! tryCitizen =
|
||||
connProps
|
||||
|> Sql.query $"
|
||||
@ -307,6 +307,37 @@ module Citizens =
|
||||
| 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
|
||||
[<RequireQualifiedAccess>]
|
||||
@ -314,13 +345,13 @@ module Continents =
|
||||
|
||||
/// Retrieve all continents
|
||||
let all () =
|
||||
connection ()
|
||||
dataSource ()
|
||||
|> Sql.query $"SELECT * FROM {Table.Continent} ORDER BY data ->> 'name'"
|
||||
|> Sql.executeAsync toDocument<Continent>
|
||||
|
||||
/// Retrieve a continent by its ID
|
||||
let findById continentId =
|
||||
connection () |> getDocument<Continent> Table.Continent (ContinentId.toString continentId)
|
||||
dataSource () |> getDocument<Continent> Table.Continent (ContinentId.toString continentId)
|
||||
|
||||
|
||||
open JobsJobsJobs.Domain.SharedTypes
|
||||
@ -345,14 +376,14 @@ module Listings =
|
||||
|
||||
/// Find all job listings posted by the given citizen
|
||||
let findByCitizen citizenId =
|
||||
connection ()
|
||||
dataSource ()
|
||||
|> Sql.query $"{viewSql} WHERE l.data ->> 'citizenId' = @citizenId AND l.data ->> 'isLegacy' = 'false'"
|
||||
|> Sql.parameters [ "@citizenId", Sql.string (CitizenId.toString citizenId) ]
|
||||
|> Sql.executeAsync toListingForView
|
||||
|
||||
/// Find a listing by its ID
|
||||
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 _
|
||||
| None -> return None
|
||||
@ -361,7 +392,7 @@ module Listings =
|
||||
/// Find a listing by its ID for viewing (includes continent information)
|
||||
let findByIdForView listingId = backgroundTask {
|
||||
let! tryListing =
|
||||
connection ()
|
||||
dataSource ()
|
||||
|> Sql.query $"{viewSql} WHERE l.id = @id AND l.data ->> 'isLegacy' = 'false'"
|
||||
|> Sql.parameters [ "@id", Sql.string (ListingId.toString listingId) ]
|
||||
|> Sql.executeAsync toListingForView
|
||||
@ -370,7 +401,7 @@ module Listings =
|
||||
|
||||
/// Save a 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
|
||||
let search (search : ListingSearchForm) =
|
||||
@ -384,7 +415,7 @@ module Listings =
|
||||
if search.Text <> "" then
|
||||
"l.data ->> 'text' ILIKE @text", [ "@text", like search.Text ]
|
||||
]
|
||||
connection ()
|
||||
dataSource ()
|
||||
|> Sql.query $"
|
||||
{viewSql}
|
||||
WHERE l.data ->> 'isExpired' = 'false' AND l.data ->> 'isLegacy' = 'false'
|
||||
@ -399,14 +430,14 @@ module Profiles =
|
||||
|
||||
/// Count the current profiles
|
||||
let count () =
|
||||
connection ()
|
||||
dataSource ()
|
||||
|> Sql.query $"SELECT COUNT(id) AS the_count FROM {Table.Profile} WHERE data ->> 'isLegacy' = 'false'"
|
||||
|> Sql.executeRowAsync (fun row -> row.int64 "the_count")
|
||||
|
||||
/// Delete a profile by its ID
|
||||
let deleteById citizenId = backgroundTask {
|
||||
let! _ =
|
||||
connection ()
|
||||
dataSource ()
|
||||
|> Sql.query $"DELETE FROM {Table.Profile} WHERE id = @id"
|
||||
|> Sql.parameters [ "@id", Sql.string (CitizenId.toString citizenId) ]
|
||||
|> Sql.executeNonQueryAsync
|
||||
@ -415,7 +446,7 @@ module Profiles =
|
||||
|
||||
/// Find a profile by citizen ID
|
||||
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 _
|
||||
| None -> return None
|
||||
@ -424,7 +455,7 @@ module Profiles =
|
||||
/// Find a profile by citizen ID for viewing (includes citizen and continent information)
|
||||
let findByIdForView citizenId = backgroundTask {
|
||||
let! tryCitizen =
|
||||
connection ()
|
||||
dataSource ()
|
||||
|> Sql.query $"
|
||||
SELECT p.*, c.data AS cit_data, o.data AS cont_data
|
||||
FROM {Table.Profile} p
|
||||
@ -443,7 +474,7 @@ module Profiles =
|
||||
|
||||
/// Save a 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)
|
||||
let search (search : ProfileSearchForm) = backgroundTask {
|
||||
@ -462,7 +493,7 @@ module Profiles =
|
||||
[ "@text", like search.BioExperience ]
|
||||
]
|
||||
let! results =
|
||||
connection ()
|
||||
dataSource ()
|
||||
|> Sql.query $"
|
||||
SELECT p.*, c.data AS cit_data
|
||||
FROM {Table.Profile} p
|
||||
@ -498,7 +529,7 @@ module Profiles =
|
||||
WHERE x ->> 'description' ILIKE @description)",
|
||||
[ "@description", like search.Skill ]
|
||||
]
|
||||
connection ()
|
||||
dataSource ()
|
||||
|> Sql.query $"
|
||||
SELECT p.*, c.data AS cont_data
|
||||
FROM {Table.Profile} p
|
||||
@ -525,7 +556,7 @@ module Successes =
|
||||
|
||||
// Retrieve all success stories
|
||||
let all () =
|
||||
connection ()
|
||||
dataSource ()
|
||||
|> Sql.query $"
|
||||
SELECT s.*, c.data AS cit_data
|
||||
FROM {Table.Success} s
|
||||
@ -544,9 +575,9 @@ module Successes =
|
||||
|
||||
/// Find a success story by its ID
|
||||
let findById successId =
|
||||
connection () |> getDocument<Success> Table.Success (SuccessId.toString successId)
|
||||
dataSource () |> getDocument<Success> Table.Success (SuccessId.toString successId)
|
||||
|
||||
/// Save a success story
|
||||
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>
|
||||
<PackageReference Include="FSharp.SystemTextJson" Version="0.19.13" />
|
||||
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.0.0" />
|
||||
<PackageReference Include="Npgsql" Version="7.0.0" />
|
||||
<PackageReference Include="Npgsql.FSharp" Version="5.5.0" />
|
||||
<PackageReference Include="Npgsql.FSharp" Version="5.6.0" />
|
||||
<PackageReference Include="Npgsql.NodaTime" Version="7.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
@ -6,20 +6,34 @@ open MailKit.Net.Smtp
|
||||
open MailKit.Security
|
||||
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
|
||||
let sendAccountConfirmation citizen security = backgroundTask {
|
||||
let name = Citizen.name citizen
|
||||
let token = WebUtility.UrlEncode security.Token.Value
|
||||
use client = new SmtpClient ()
|
||||
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 token = WebUtility.UrlEncode security.Token.Value
|
||||
use! client = smtpClient ()
|
||||
use msg = createMessage citizen "Account Confirmation Request"
|
||||
|
||||
let text =
|
||||
[ $"ITM, {name}!"
|
||||
[ $"ITM, {Citizen.name citizen}!"
|
||||
""
|
||||
"This e-mail address was recently used to establish an account on"
|
||||
"Jobs, Jobs, Jobs (noagendacareers.com). Before this account can be"
|
||||
@ -45,3 +59,36 @@ let sendAccountConfirmation citizen security = backgroundTask {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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]
|
||||
let confirm token next ctx = task {
|
||||
let confirm token : HttpHandler = fun next ctx -> task {
|
||||
let! isConfirmed = Citizens.confirmAccount token
|
||||
return! Citizen.confirmAccount isConfirmed |> render "Account Confirmation" next ctx
|
||||
}
|
||||
|
||||
// GET: /citizen/dashboard
|
||||
let dashboard = requireUser >=> fun next ctx -> task {
|
||||
let dashboard : HttpHandler = requireUser >=> fun next ctx -> task {
|
||||
let citizenId = currentCitizenId ctx
|
||||
let! citizen = Citizens.findById citizenId
|
||||
let! profile = Profiles.findById citizenId
|
||||
@ -284,13 +296,38 @@ module Citizen =
|
||||
}
|
||||
|
||||
// GET: /citizen/deny/[token]
|
||||
let deny token next ctx = task {
|
||||
let deny token : HttpHandler = fun next ctx -> task {
|
||||
let! wasDeleted = Citizens.denyAccount token
|
||||
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
|
||||
let logOff = requireUser >=> fun next ctx -> task {
|
||||
let logOff : HttpHandler = requireUser >=> fun next ctx -> task {
|
||||
do! ctx.SignOutAsync CookieAuthenticationDefaults.AuthenticationScheme
|
||||
do! addSuccess "Log off successful" ctx
|
||||
return! redirectToGet "/" next ctx
|
||||
@ -396,6 +433,15 @@ module Citizen =
|
||||
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
|
||||
let saveAccount : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task {
|
||||
let! theForm = ctx.BindFormAsync<AccountProfileForm> ()
|
||||
@ -807,20 +853,24 @@ let allEndpoints = [
|
||||
]
|
||||
subRoute "/citizen" [
|
||||
GET_HEAD [
|
||||
route "/account" Citizen.account
|
||||
routef "/confirm/%s" Citizen.confirm
|
||||
route "/dashboard" Citizen.dashboard
|
||||
routef "/deny/%s" Citizen.deny
|
||||
route "/log-off" Citizen.logOff
|
||||
route "/log-on" Citizen.logOn
|
||||
route "/register" Citizen.register
|
||||
route "/so-long" Citizen.soLong
|
||||
route "/account" Citizen.account
|
||||
routef "/cancel-reset/%s" Citizen.cancelReset
|
||||
routef "/confirm/%s" Citizen.confirm
|
||||
route "/dashboard" Citizen.dashboard
|
||||
routef "/deny/%s" Citizen.deny
|
||||
route "/forgot-password" Citizen.forgotPassword
|
||||
route "/log-off" Citizen.logOff
|
||||
route "/log-on" Citizen.logOn
|
||||
route "/register" Citizen.register
|
||||
routef "/reset-password/%s" Citizen.resetPassword
|
||||
route "/so-long" Citizen.soLong
|
||||
]
|
||||
POST [
|
||||
route "/delete" Citizen.delete
|
||||
route "/log-on" Citizen.doLogOn
|
||||
route "/register" Citizen.doRegistration
|
||||
route "/save-account" Citizen.saveAccount
|
||||
route "/delete" Citizen.delete
|
||||
route "/forgot-password" Citizen.doForgotPassword
|
||||
route "/log-on" Citizen.doLogOn
|
||||
route "/register" Citizen.doRegistration
|
||||
route "/save-account" Citizen.saveAccount
|
||||
]
|
||||
]
|
||||
subRoute "/listing" [
|
||||
|
@ -33,9 +33,9 @@
|
||||
|
||||
<ItemGroup>
|
||||
<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.Htmx" Version="1.8.4" />
|
||||
<PackageReference Include="Giraffe.ViewEngine.Htmx" Version="1.8.5" />
|
||||
<PackageReference Include="MailKit" Version="3.3.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.0" />
|
||||
<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
|
||||
[<CLIMutable; NoComparison; NoEquality>]
|
||||
type LogOnViewModel =
|
||||
|
@ -13,9 +13,7 @@ let contactEdit (contacts : OtherContactForm array) =
|
||||
div [ _id $"contactRow{idx}"; _class "row pb-3" ] [
|
||||
div [ _class "col-2 col-md-1" ] [
|
||||
button [ _type "button"; _class "btn btn-sm btn-outline-danger rounded-pill mt-3"; _title "Delete"
|
||||
_onclick $"jjj.citizen.removeContact({idx})" ] [
|
||||
rawText " − "
|
||||
]
|
||||
_onclick $"jjj.citizen.removeContact({idx})" ] [ txt " − " ]
|
||||
]
|
||||
div [ _class "col-10 col-md-4 col-xl-3" ] [
|
||||
div [ _class "form-floating" ] [
|
||||
@ -23,37 +21,37 @@ let contactEdit (contacts : OtherContactForm array) =
|
||||
_value contact.ContactType; _placeholder "Type"; _required ] [
|
||||
let optionFor value label =
|
||||
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 Email "E-mail Address"
|
||||
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 "form-floating" ] [
|
||||
input [ _type "text"; _id $"contactName{idx}"; _name $"Contacts[{idx}].Name"; _class "form-control"
|
||||
_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
|
||||
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 "form-floating" ] [
|
||||
input [ _type "text"; _id $"contactValue{idx}"; _name $"Contacts[{idx}].Value"
|
||||
_class "form-control"; _maxlength "1000"; _value contact.Value; _placeholder "Contact"
|
||||
_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 "form-check mt-3" ] [
|
||||
input [ _type "checkbox"; _id $"contactIsPublic{idx}"; _name $"Contacts[{idx}].IsPublic";
|
||||
_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
|
||||
let account (m : AccountProfileForm) csrf =
|
||||
article [] [
|
||||
h3 [ _class "pb-3" ] [ rawText "Account Profile" ]
|
||||
pageWithTitle "Account Profile" [
|
||||
p [] [
|
||||
rawText "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 "
|
||||
rawText "displayed."
|
||||
txt "This information is visible to all fellow logged-on citizens. For publicly-visible employment "
|
||||
txt "profiles and job listings, the “Display Name” fields and any public contacts will be "
|
||||
txt "displayed."
|
||||
]
|
||||
form [ _class "row g-3"; _method "POST"; _action "/citizen/save-account" ] [
|
||||
antiForgery csrf
|
||||
@ -81,38 +78,32 @@ let account (m : AccountProfileForm) csrf =
|
||||
]
|
||||
div [ _class "col-6 col-xl-4" ] [
|
||||
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" ] [
|
||||
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" ] [
|
||||
textBox [ _type "password"; _minlength "8" ] (nameof m.NewPasswordConfirm) "" "Confirm 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-12" ] [
|
||||
hr []
|
||||
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"
|
||||
_onclick "jjj.citizen.addContact()" ] [
|
||||
rawText "Add a Contact Method"
|
||||
]
|
||||
_onclick "jjj.citizen.addContact()" ] [ txt "Add a Contact Method" ]
|
||||
]
|
||||
]
|
||||
yield! contactEdit m.Contacts
|
||||
div [ _class "col-12" ] [
|
||||
button [ _type "submit"; _class "btn btn-primary" ] [
|
||||
i [ _class "mdi mdi-content-save-outline" ] [ rawText " Save" ]
|
||||
]
|
||||
]
|
||||
div [ _class "col-12" ] [ submitButton "content-save-outline" "Save" ]
|
||||
]
|
||||
hr []
|
||||
p [ _class "text-muted fst-italic" ] [
|
||||
rawText "(If you want to delete your profile, or your entire account, "
|
||||
a [ _href "/citizen/so-long" ] [ rawText "see your deletion options here" ]; rawText ".)"
|
||||
txt "(If you want to delete your profile, or your entire account, "
|
||||
a [ _href "/citizen/so-long" ] [ rawText "see your deletion options here" ]; txt ".)"
|
||||
]
|
||||
jsOnLoad $"
|
||||
jjj.citizen.nextIndex = {m.Contacts.Length}
|
||||
@ -122,157 +113,171 @@ let account (m : AccountProfileForm) csrf =
|
||||
|
||||
/// The account confirmation page
|
||||
let confirmAccount isConfirmed =
|
||||
article [] [
|
||||
h3 [ _class "pb-3" ] [ rawText "Account Confirmation" ]
|
||||
pageWithTitle "Account Confirmation" [
|
||||
p [] [
|
||||
if isConfirmed then
|
||||
rawText "Your account was confirmed successfully! You may "
|
||||
a [ _href "/citizen/log-on" ] [ rawText "log on here" ]; rawText "."
|
||||
txt "Your account was confirmed successfully! You may "
|
||||
a [ _href "/citizen/log-on" ] [ rawText "log on here" ]; txt "."
|
||||
else
|
||||
rawText "The confirmation token did not match any pending accounts. Confirmation tokens are only valid "
|
||||
rawText "for 3 days; if the token expired, you will need to re-register, which "
|
||||
a [ _href "/citizen/register" ] [ rawText "you can do here" ]; rawText "."
|
||||
txt "The confirmation token did not match any pending accounts. Confirmation tokens are only valid for "
|
||||
txt "3 days; if the token expired, you will need to re-register, which "
|
||||
a [ _href "/citizen/register" ] [ txt "you can do here" ]; txt "."
|
||||
]
|
||||
]
|
||||
|
||||
/// The citizen's dashboard page
|
||||
let dashboard (citizen : Citizen) (profile : Profile option) profileCount tz =
|
||||
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 "col" ] [
|
||||
div [ _class "card h-100" ] [
|
||||
h5 [ _class "card-header" ] [ rawText "Your Profile" ]
|
||||
h5 [ _class "card-header" ] [ txt "Your Profile" ]
|
||||
div [ _class "card-body" ] [
|
||||
match profile with
|
||||
| Some prfl ->
|
||||
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" ] [
|
||||
rawText "Your profile currently lists "; str $"{List.length prfl.Skills}"
|
||||
rawText " skill"; rawText (if List.length prfl.Skills <> 1 then "s" else "")
|
||||
rawText "."
|
||||
txt $"Your profile currently lists {List.length prfl.Skills} skill"
|
||||
txt (if List.length prfl.Skills <> 1 then "s" else ""); txt "."
|
||||
if prfl.IsSeekingEmployment then
|
||||
br []; br []
|
||||
rawText "Your profile indicates that you are seeking employment. Once you find it, "
|
||||
a [ _href "/success-story/add" ] [ rawText "tell your fellow citizens about it!" ]
|
||||
txt "Your profile indicates that you are seeking employment. Once you find it, "
|
||||
a [ _href "/success-story/add" ] [ txt "tell your fellow citizens about it!" ]
|
||||
]
|
||||
| None ->
|
||||
p [ _class "card-text" ] [
|
||||
rawText "You do not have an employment profile established; click below (or "
|
||||
rawText "“Edit Profile” in the menu) to get started!"
|
||||
txt "You do not have an employment profile established; click below (or “Edit "
|
||||
txt "Profile” in the menu) to get started!"
|
||||
]
|
||||
]
|
||||
div [ _class "card-footer" ] [
|
||||
match profile with
|
||||
| Some p ->
|
||||
| Some _ ->
|
||||
a [ _href $"/profile/{CitizenId.toString citizen.Id}/view"
|
||||
_class "btn btn-outline-secondary" ] [
|
||||
rawText "View Profile"
|
||||
]; rawText " "
|
||||
a [ _href "/profile/edit"; _class "btn btn-outline-secondary" ] [ rawText "Edit Profile" ]
|
||||
_class "btn btn-outline-secondary" ] [ txt "View Profile" ]; txt " "
|
||||
a [ _href "/profile/edit"; _class "btn btn-outline-secondary" ] [ txt "Edit Profile" ]
|
||||
| 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 "card h-100" ] [
|
||||
h5 [ _class "card-header" ] [ rawText "Other Citizens" ]
|
||||
h5 [ _class "card-header" ] [ txt "Other Citizens" ]
|
||||
div [ _class "card-body" ] [
|
||||
h6 [ _class "card-subtitle mb-3 text-muted fst-italic" ] [
|
||||
rawText (if profileCount = 0L then "No" else $"{profileCount} Total")
|
||||
rawText " Employment Profile"; rawText (if profileCount <> 1 then "s" else "")
|
||||
txt (if profileCount = 0L then "No" else $"{profileCount} Total")
|
||||
txt " Employment Profile"; txt (if profileCount <> 1 then "s" else "")
|
||||
]
|
||||
p [ _class "card-text" ] [
|
||||
if profileCount = 1 && Option.isSome profile then
|
||||
"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 "You can click below, but you will not find anything…"
|
||||
|> rawText
|
||||
|> txt
|
||||
]
|
||||
]
|
||||
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 [] [
|
||||
rawText "To see how this application works, check out “How It Works” in the sidebar (last "
|
||||
rawText "updated August 29<sup>th</sup>, 2021)."
|
||||
txt "To see how this application works, check out “How It Works” in the sidebar (last updated "
|
||||
txt "August 29<sup>th</sup>, 2021)."
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
/// The account deletion success page
|
||||
let deleted =
|
||||
article [] [
|
||||
h3 [ _class "pb-3" ] [ rawText "Account Deletion Success" ]
|
||||
p [] [ rawText " " ]
|
||||
p [] [ rawText "Your account has been successfully deleted." ]
|
||||
p [] [ rawText " " ]
|
||||
p [] [ rawText "Thank you for participating, and thank you for your courage. #GitmoNation" ]
|
||||
pageWithTitle "Account Deletion Success" [
|
||||
emptyP; p [] [ txt "Your account has been successfully deleted." ]
|
||||
emptyP; p [] [ txt "Thank you for participating, and thank you for your courage. #GitmoNation" ]
|
||||
]
|
||||
|
||||
|
||||
/// The profile or account deletion page
|
||||
let deletionOptions csrf =
|
||||
article [] [
|
||||
h3 [ _class "pb-3" ] [ rawText "Account Deletion Options" ]
|
||||
h4 [ _class "pb-3" ] [ rawText "Option 1 – Delete Your Profile" ]
|
||||
pageWithTitle "Account Deletion Options" [
|
||||
h4 [ _class "pb-3" ] [ txt "Option 1 – Delete Your Profile" ]
|
||||
p [] [
|
||||
rawText "Utilizing this option will remove your current employment profile and skills. This will preserve "
|
||||
rawText "any job listings you may have posted, or any success stories you may have written, and preserves "
|
||||
rawText "this application’s knowledge of you. This is what you want to use if you want to clear out "
|
||||
rawText "your profile and start again (and remove the current one from others’ view)."
|
||||
txt "Utilizing this option will remove your current employment profile and skills. This will preserve any "
|
||||
txt "job listings you may have posted, or any success stories you may have written, and preserves this "
|
||||
txt "this application’s knowledge of you. This is what you want to use if you want to clear out your "
|
||||
txt "profile and start again (and remove the current one from others’ view)."
|
||||
]
|
||||
form [ _class "text-center"; _method "POST"; _action "/profile/delete" ] [
|
||||
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 []
|
||||
h4 [ _class "pb-3" ] [ rawText "Option 2 – Delete Your Account" ]
|
||||
h4 [ _class "pb-3" ] [ txt "Option 2 – Delete Your Account" ]
|
||||
p [] [
|
||||
rawText "This option will make it like you never visited this site. It will delete your profile, skills, "
|
||||
rawText "job listings, success stories, and account. This is what you want to use if you want to disappear "
|
||||
rawText "from this application."
|
||||
txt "This option will make it like you never visited this site. It will delete your profile, skills, job "
|
||||
txt "listings, success stories, and account. This is what you want to use if you want to disappear from "
|
||||
txt "this application."
|
||||
]
|
||||
form [ _class "text-center"; _method "POST"; _action "/citizen/delete" ] [
|
||||
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
|
||||
let denyAccount wasDeleted =
|
||||
article [] [
|
||||
h3 [ _class "pb-3" ] [ rawText "Account Deletion" ]
|
||||
pageWithTitle "Account Deletion" [
|
||||
p [] [
|
||||
if wasDeleted then
|
||||
rawText "The account was deleted successfully; sorry for the trouble."
|
||||
if wasDeleted then txt "The account was deleted successfully; sorry for the trouble."
|
||||
else
|
||||
rawText "The confirmation token did not match any pending accounts; if this was an inadvertently "
|
||||
rawText "created account, it has likely already been deleted."
|
||||
txt "The confirmation token did not match any pending accounts; if this was an inadvertently created "
|
||||
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
|
||||
let logOn (m : LogOnViewModel) csrf =
|
||||
article [] [
|
||||
h3 [ _class "pb-3" ] [ rawText "Log On" ]
|
||||
pageWithTitle "Log On" [
|
||||
match m.ErrorMessage with
|
||||
| Some msg ->
|
||||
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
|
||||
rawText "If this is a new account, it must be confirmed before it can be used; otherwise, you need "
|
||||
rawText "to "
|
||||
a [ _href "/citizen/forgot-password" ] [ rawText "request an unlock code" ]
|
||||
rawText " before you may log on."
|
||||
txt "If this is a new account, it must be confirmed before it can be used; otherwise, you need to "
|
||||
a [ _href "/citizen/forgot-password" ] [ txt "request an unlock code" ]
|
||||
txt " before you may log on."
|
||||
]
|
||||
| None -> ()
|
||||
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" ] [
|
||||
textBox [ _type "password" ] (nameof m.Password) "" "Password" true
|
||||
]
|
||||
div [ _class "col-12" ] [
|
||||
button [ _class "btn btn-primary"; _type "submit" ] [
|
||||
i [ _class "mdi mdi-login" ] []; rawText " Log On"
|
||||
]
|
||||
]
|
||||
div [ _class "col-12" ] [ submitButton "login" "Log On" ]
|
||||
]
|
||||
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" ] [
|
||||
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
|
||||
let register q1 q2 (m : RegisterViewModel) csrf =
|
||||
article [] [
|
||||
h3 [ _class "pb-3" ] [ rawText "Register" ]
|
||||
pageWithTitle "Register" [
|
||||
form [ _class "row g-3"; _hxPost "/citizen/register" ] [
|
||||
antiForgery csrf
|
||||
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" ] [
|
||||
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" ] [
|
||||
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" ] [
|
||||
hr []
|
||||
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" ] [
|
||||
@ -339,30 +339,34 @@ let register q1 q2 (m : RegisterViewModel) csrf =
|
||||
textBox [ _type "text"; _maxlength "30" ] (nameof m.Question2Answer) m.Question2Answer q2 true
|
||||
input [ _type "hidden"; _name (nameof m.Question2Index); _value (string m.Question2Index ) ]
|
||||
]
|
||||
div [ _class "col-12" ] [
|
||||
button [ _type "submit"; _class "btn btn-primary" ] [
|
||||
i [ _class "mdi mdi-content-save-outline" ] []; rawText " Save"
|
||||
]
|
||||
]
|
||||
div [ _class "col-12" ] [ submitButton "content-save-outline" "Save" ]
|
||||
jsOnLoad $"jjj.citizen.validatePasswords('{nameof m.Password}', 'ConfirmPassword', true)"
|
||||
]
|
||||
]
|
||||
|
||||
/// The confirmation page for user registration
|
||||
let registered =
|
||||
article [] [
|
||||
h3 [ _class "pb-3" ] [ rawText "Registration Successful" ]
|
||||
pageWithTitle "Registration Successful" [
|
||||
p [] [
|
||||
rawText "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 "
|
||||
rawText "able to log on using the e-mail address and password you provided."
|
||||
txt "You have been successfully registered with Jobs, Jobs, Jobs. Check your e-mail for a confirmation "
|
||||
txt "link; it will be valid for the next 72 hours (3 days). Once you confirm your account, you will be "
|
||||
txt "able to log on using the e-mail address and password you provided."
|
||||
]
|
||||
p [] [
|
||||
rawText "If the account is not confirmed within the 72-hour window, it will be deleted, and you will need "
|
||||
rawText "to register again."
|
||||
txt "If the account is not confirmed within the 72-hour window, it will be deleted, and you will need to "
|
||||
txt "register again."
|
||||
]
|
||||
p [] [
|
||||
rawText "If you encounter issues, feel free to reach out to @danieljsummers on No Agenda Social for "
|
||||
rawText "assistance."
|
||||
txt "If you encounter issues, feel free to reach out to @danieljsummers on No Agenda Social for 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) =
|
||||
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
|
||||
let textBox attrs name value fieldLabel isRequired =
|
||||
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
|
||||
if isRequired then _required
|
||||
] |> 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
|
||||
@ -33,7 +43,7 @@ let checkBox attrs name isChecked checkLabel =
|
||||
[ _type "checkbox"; _id name; _name name; _class "form-check-input"; _value "true"
|
||||
if isChecked then _checked ]
|
||||
|> 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
|
||||
@ -46,26 +56,32 @@ let continentList attrs name (continents : Continent list) emptyLabel selectedVa
|
||||
|> List.map (fun c ->
|
||||
let theId = ContinentId.toString c.Id
|
||||
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
|
||||
let jsOnLoad js =
|
||||
script [] [
|
||||
rawText """document.addEventListener("DOMContentLoaded", function () { """; rawText js; rawText " })"
|
||||
]
|
||||
script [] [ txt """document.addEventListener("DOMContentLoaded", function () { """; txt js; txt " })" ]
|
||||
|
||||
/// Create a Markdown editor
|
||||
let markdownEditor attrs name value editorLabel =
|
||||
div [ _class "col-12"; _id $"{name}EditRow" ] [
|
||||
nav [ _class "nav nav-pills pb-1" ] [
|
||||
button [ _type "button"; _id $"{name}EditButton"; _class "btn btn-primary btn-sm rounded-pill" ] [
|
||||
rawText "Markdown"
|
||||
txt "Markdown"
|
||||
]
|
||||
rawText " "
|
||||
button [ _type "button"; _id $"{name}PreviewButton"
|
||||
_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"
|
||||
@ -73,9 +89,9 @@ let markdownEditor attrs name value editorLabel =
|
||||
div [ _id $"{name}Edit"; _class "form-floating jjj-shown" ] [
|
||||
textarea (List.append attrs
|
||||
[ _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}')"
|
||||
]
|
||||
@ -87,7 +103,7 @@ let collapsePanel header content =
|
||||
h6 [ _class "card-title" ] [
|
||||
// TODO: toggle collapse
|
||||
//a [ _href "#"; _class "{ 'cp-c': collapsed, 'cp-o': !collapsed }"; @click.prevent="toggle">{{headerText}} ]
|
||||
rawText header
|
||||
txt header
|
||||
]
|
||||
yield! content
|
||||
]
|
||||
@ -99,7 +115,7 @@ let yesOrNo value =
|
||||
|
||||
/// Markdown as a raw HTML text node
|
||||
let md2html value =
|
||||
rawText (MarkdownString.toHtml value)
|
||||
(MarkdownString.toHtml >> txt) value
|
||||
|
||||
/// Display a citizen's contact information
|
||||
let contactInfo citizen isPublic =
|
||||
|
@ -5,558 +5,539 @@ open Giraffe.ViewEngine
|
||||
/// The home page
|
||||
let home =
|
||||
article [] [
|
||||
p [] [ rawText " " ]
|
||||
emptyP
|
||||
p [] [
|
||||
rawText "Welcome to Jobs, Jobs, Jobs (AKA No Agenda Careers), where citizens of Gitmo Nation can assist "
|
||||
rawText "one another in finding employment. This will enable them to continue providing value-for-value to "
|
||||
rawText "Adam and John, as they continue their work deconstructing the misinformation that passes for news "
|
||||
rawText "on a day-to-day basis."
|
||||
txt "Welcome to Jobs, Jobs, Jobs (AKA No Agenda Careers), where citizens of Gitmo Nation can assist one "
|
||||
txt "another in finding employment. This will enable them to continue providing value-for-value to Adam "
|
||||
txt "and John, as they continue their work deconstructing the misinformation that passes for news on a "
|
||||
txt "day-to-day basis."
|
||||
]
|
||||
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" ] [
|
||||
rawText "The Best Podcast in the Universe"
|
||||
txt "The Best Podcast in the Universe"
|
||||
]
|
||||
rawText " "; em [] [ audioClip "thats-true" (rawText "(that’s true!)") ]
|
||||
rawText " and find out what you’re missing."
|
||||
txt " "; em [] [ audioClip "thats-true" (txt "(that’s true!)") ]
|
||||
txt " and find out what you’re missing."
|
||||
]
|
||||
]
|
||||
|
||||
/// Online help / documentation
|
||||
let howItWorks =
|
||||
article [] [
|
||||
h3 [] [ rawText "How It Works" ]
|
||||
p [] [ rawText "TODO: convert and update for v3"]
|
||||
pageWithTitle "How It Works" [
|
||||
p [] [ txt "TODO: convert and update for v3"]
|
||||
]
|
||||
|
||||
/// The privacy policy
|
||||
let privacyPolicy =
|
||||
let appName = rawText "Jobs, Jobs, Jobs"
|
||||
let appName = txt "Jobs, Jobs, Jobs"
|
||||
article [] [
|
||||
h3 [] [ rawText "Privacy Policy" ]
|
||||
p [ _class "fst-italic" ] [ rawText "(as of December 27<sup>th</sup>, 2022)" ]
|
||||
h3 [] [ txt "Privacy Policy" ]
|
||||
p [ _class "fst-italic" ] [ txt "(as of December 27<sup>th</sup>, 2022)" ]
|
||||
|
||||
p [] [
|
||||
appName; rawText " (“we,” “our,” or “us”) is committed to protecting "
|
||||
rawText "your privacy. This Privacy Policy explains how your personal information is collected, used, and "
|
||||
rawText "disclosed by "; appName; rawText "."
|
||||
appName; txt " (“we,” “our,” or “us”) is committed to protecting your "
|
||||
txt "privacy. This Privacy Policy explains how your personal information is collected, used, and disclosed "
|
||||
txt "disclosed by "; appName; txt "."
|
||||
]
|
||||
p [] [
|
||||
rawText "This Privacy Policy applies to our website, and its associated subdomains (collectively, our "
|
||||
rawText "“Service”) alongside our application, "; appName; rawText ". By accessing or using "
|
||||
rawText "our Service, you signify that you have read, understood, and agree to our collection, storage, "
|
||||
rawText "use, and disclosure of your personal information as described in this Privacy Policy and our "
|
||||
rawText "Terms of Service."
|
||||
txt "This Privacy Policy applies to our website, and its associated subdomains (collectively, our "
|
||||
txt "“Service”) alongside our application, "; appName; rawText ". By accessing or using our "
|
||||
txt "Service, you signify that you have read, understood, and agree to our collection, storage, use, and "
|
||||
txt "disclosure of your personal information as described in this Privacy Policy and our Terms of Service."
|
||||
]
|
||||
|
||||
h4 [] [ rawText "Definitions and key terms" ]
|
||||
h4 [] [ txt "Definitions and key terms" ]
|
||||
p [] [
|
||||
rawText "To help explain things as clearly as possible in this Privacy Policy, every time any of these "
|
||||
rawText "terms are referenced, are strictly defined as:"
|
||||
txt "To help explain things as clearly as possible in this Privacy Policy, every time any of these terms "
|
||||
txt "are referenced, are strictly defined as:"
|
||||
]
|
||||
ul [] [
|
||||
li [] [
|
||||
rawText "Cookie: small amount of data generated by a website and saved by your web browser. It is used "
|
||||
rawText "to identify your browser, provide analytics, remember information about you such as your "
|
||||
rawText "language preference or login information."
|
||||
txt "Cookie: small amount of data generated by a website and saved by your web browser. It is used to "
|
||||
txt "identify your browser, provide analytics, remember information about you such as your language "
|
||||
txt "preference or login information."
|
||||
]
|
||||
li [] [
|
||||
rawText "Company: when this policy mentions “Company,” “we,” “us,” "
|
||||
rawText "or “our,” it refers to "; appName; rawText ", that is responsible for your "
|
||||
rawText "information under this Privacy Policy."
|
||||
txt "Company: when this policy mentions “Company,” “we,” “us,” or "
|
||||
txt "“our,” it refers to "; appName; txt ", that is responsible for your information under "
|
||||
txt "this Privacy Policy."
|
||||
]
|
||||
li [] [
|
||||
rawText "Country: where "; appName; rawText " or the owners/founders of "; appName
|
||||
rawText " are based, in this case is US."
|
||||
txt "Country: where "; appName; txt " or the owners/founders of "; appName
|
||||
txt " are based, in this case is US."
|
||||
]
|
||||
li [] [
|
||||
rawText "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 "Customer: refers to the company, organization or person that signs up to use the "; appName
|
||||
txt " Service to manage the relationships with your consumers or service users."
|
||||
]
|
||||
li [] [
|
||||
rawText "Device: any internet connected device such as a phone, tablet, computer or any other device "
|
||||
rawText "that can be used to visit "; appName; rawText " and use the services."
|
||||
txt "Device: any internet connected device such as a phone, tablet, computer or any other device that "
|
||||
txt "can be used to visit "; appName; txt " and use the services."
|
||||
]
|
||||
li [] [
|
||||
rawText "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 "
|
||||
rawText "can often be used to identify the location from which a device is connecting to the Internet."
|
||||
txt "IP address: Every device connected to the Internet is assigned a number known as an Internet "
|
||||
txt "protocol (IP) address. These numbers are usually assigned in geographic blocks. An IP address can "
|
||||
txt "often be used to identify the location from which a device is connecting to the Internet."
|
||||
]
|
||||
li [] [
|
||||
rawText "Personnel: refers to those individuals who are employed by "; appName; rawText " or are under "
|
||||
rawText "contract to perform a service on behalf of one of the parties."
|
||||
txt "Personnel: refers to those individuals who are employed by "; appName; txt " or are under "
|
||||
txt "contract to perform a service on behalf of one of the parties."
|
||||
]
|
||||
li [] [
|
||||
rawText "Personal Data: any information that directly, indirectly, or in connection with other "
|
||||
rawText "information — including a personal identification number — allows for the identification or "
|
||||
rawText "identifiability of a natural person."
|
||||
txt "Personal Data: any information that directly, indirectly, or in connection with other information "
|
||||
txt "— including a personal identification number — allows for the identification or identifiability "
|
||||
txt "of a natural person."
|
||||
]
|
||||
li [] [
|
||||
rawText "Service: refers to the service provided by "; appName; rawText " as described in the relative "
|
||||
rawText "terms (if available) and on this platform."
|
||||
txt "Service: refers to the service provided by "; appName; txt " as described in the relative terms "
|
||||
txt "(if available) and on this platform."
|
||||
]
|
||||
li [] [
|
||||
rawText "Third-party service: refers to advertisers, contest sponsors, promotional and marketing "
|
||||
rawText "partners, and others who provide our content or whose products or services we think may "
|
||||
rawText "interest you."
|
||||
txt "Third-party service: refers to advertisers, contest sponsors, promotional and marketing partners, "
|
||||
txt "and others who provide our content or whose products or services we think may interest you."
|
||||
]
|
||||
li [] [
|
||||
rawText "Website: "; appName; rawText "’s site, which can be accessed via this URL: "
|
||||
a [ _href "/" ] [ rawText "https://noagendacareers.com/" ]
|
||||
txt "Website: "; appName; txt "’s site, which can be accessed via this URL: "
|
||||
a [ _href "/" ] [ txt "https://noagendacareers.com/" ]
|
||||
]
|
||||
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 [] [
|
||||
rawText "We collect information from you when you visit our website, register on our site, or fill out a "
|
||||
rawText "form."
|
||||
txt "We collect information from you when you visit our website, register on our site, or fill out a form."
|
||||
]
|
||||
ul [] [
|
||||
li [] [ rawText "Name / Username" ]
|
||||
li [] [ rawText "Coarse Geographic Location" ]
|
||||
li [] [ rawText "Employment History" ]
|
||||
li [] [ rawText "Job Listing Information" ]
|
||||
li [] [ txt "Name / Username" ]
|
||||
li [] [ txt "Coarse Geographic Location" ]
|
||||
li [] [ txt "Employment History" ]
|
||||
li [] [ txt "Job Listing Information" ]
|
||||
]
|
||||
|
||||
h4 [] [ rawText "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:" ]
|
||||
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:" ]
|
||||
h4 [] [ txt "How Do We Use The Information We Collect?" ]
|
||||
p [] [ txt "Any of the information we collect from you may be used in one of the following ways:" ]
|
||||
ul [] [
|
||||
li [] [
|
||||
rawText "Personal data collected must be processed in a fair, legal, and transparent way and should "
|
||||
rawText "only be used in a way that a person would reasonably expect."
|
||||
txt "To personalize your experience (your information helps us to better respond to your individual "
|
||||
txt "needs)"
|
||||
]
|
||||
li [] [
|
||||
rawText "Personal data should only be collected to fulfil a specific purpose and it should only be "
|
||||
rawText "used for that purpose. Organizations must specify why they need the personal data when they "
|
||||
rawText "collect it."
|
||||
txt "To improve our website (we continually strive to improve our website offerings based on the "
|
||||
txt "information and feedback we receive from you)"
|
||||
]
|
||||
li [] [ rawText "Personal data should be held no longer than necessary to fulfil its purpose." ]
|
||||
li [] [
|
||||
rawText "People covered by the GDPR have the right to access their own personal data. They can also "
|
||||
rawText "request a copy of their data, and that their data be updated, deleted, restricted, or moved "
|
||||
rawText "to another organization."
|
||||
txt "To improve customer service (your information helps us to more effectively respond to your "
|
||||
txt "customer service requests and support needs)"
|
||||
]
|
||||
]
|
||||
|
||||
h5 [] [ rawText "Why is GDPR important?" ]
|
||||
h4 [] [ txt "When does "; appName; txt " use end user information from third parties?" ]
|
||||
p [] [
|
||||
rawText "GDPR adds some new requirements regarding how companies should protect individuals’ "
|
||||
rawText "personal data that they collect and process. It also raises the stakes for compliance by "
|
||||
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 "
|
||||
rawText "is very important and we already have solid security and privacy practices in place that go "
|
||||
rawText "beyond the requirements of this regulation."
|
||||
appName; txt " will collect End User Data necessary to provide the "; appName
|
||||
txt " services to our customers."
|
||||
]
|
||||
p [] [
|
||||
txt "End users may voluntarily provide us with information they have made available on social media "
|
||||
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 [] [
|
||||
rawText "We are committed to helping our customers meet the data subject rights requirements of GDPR. "
|
||||
appName; rawText " processes or stores all personal data in fully vetted, DPA compliant vendors. We do "
|
||||
rawText "store all conversation and personal data for up to 6 years unless your account is deleted. In "
|
||||
rawText "which case, we dispose of all data in accordance with our Terms of Service and Privacy Policy, "
|
||||
rawText "but we will not hold it longer than 60 days."
|
||||
]
|
||||
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."
|
||||
txt "We may disclose personal and non-personal information about you to government or law enforcement "
|
||||
txt "officials or private parties as we, in our sole discretion, believe necessary or appropriate in order "
|
||||
txt "to respond to claims, legal process (including subpoenas), to protect our rights and interests or "
|
||||
txt "those of a third party, the safety of the public or any person, to prevent or stop any illegal, "
|
||||
txt "unethical, or legally actionable activity, or to otherwise comply with applicable court orders, laws, "
|
||||
txt "rules and regulations."
|
||||
]
|
||||
|
||||
h4 [] [ rawText "California Residents" ]
|
||||
h4 [] [ txt "Where and when is information collected from customers and end users?" ]
|
||||
p [] [
|
||||
rawText "The California Consumer Privacy Act (CCPA) requires us to disclose categories of Personal "
|
||||
rawText "Information we collect and how we use it, the categories of sources from whom we collect Personal "
|
||||
rawText "Information, and the third parties with whom we share it, which we have explained above."
|
||||
appName; txt " will collect personal information that you submit to us. We may also receive personal "
|
||||
txt "information about you from third parties as described 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 [] [
|
||||
rawText "We are also required to communicate information about rights California residents have under "
|
||||
rawText "California law. You may exercise the following rights:"
|
||||
txt "Customers have the right to request the restriction of certain uses and disclosures of personally "
|
||||
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 [] [
|
||||
li [] [
|
||||
rawText "Right to Know and Access. You may submit a verifiable request for information regarding the: "
|
||||
rawText "(1) categories of Personal Information we collect, use, or share; (2) purposes for which "
|
||||
rawText "categories of Personal Information are collected or used by us; (3) categories of sources "
|
||||
rawText "from which we collect Personal Information; and (4) specific pieces of Personal Information "
|
||||
rawText "we have collected about you."
|
||||
txt "Right to Know and Access. You may submit a verifiable request for information regarding the: (1) "
|
||||
txt "categories of Personal Information we collect, use, or share; (2) purposes for which categories "
|
||||
txt "of Personal Information are collected or used by us; (3) categories of sources from which we "
|
||||
txt "collect Personal Information; and (4) specific pieces of Personal Information we have collected "
|
||||
txt "about you."
|
||||
]
|
||||
li [] [
|
||||
rawText "Right to Equal Service. We will not discriminate against you if you exercise your privacy "
|
||||
rawText "rights."
|
||||
txt "Right to Equal Service. We will not discriminate against you if you exercise your privacy rights."
|
||||
]
|
||||
li [] [
|
||||
rawText "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 "Right to Delete. You may submit a verifiable request to close your account and we will delete "
|
||||
txt "Personal Information about you that we have collected."
|
||||
]
|
||||
li [] [
|
||||
rawText "Request that a business that sells a consumer’s personal data, not sell the "
|
||||
rawText "consumer’s personal data."
|
||||
txt "Request that a business that sells a consumer’s personal data, not sell the "
|
||||
txt "consumer’s personal data."
|
||||
]
|
||||
]
|
||||
p [] [
|
||||
rawText "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 "If you make a request, we have one month to respond to you. If you would like to exercise any of "
|
||||
txt "these rights, please contact us."
|
||||
]
|
||||
p [] [ rawText "We do not sell the Personal Information of our users." ]
|
||||
p [] [ rawText "For more information about these rights, please contact us." ]
|
||||
p [] [ txt "We do not sell the Personal Information of our users." ]
|
||||
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 [] [
|
||||
rawText "CalOPPA requires us to disclose categories of Personal Information we collect and how we use it, "
|
||||
rawText "the categories of sources from whom we collect Personal Information, and the third parties with "
|
||||
rawText "whom we share it, which we have explained above."
|
||||
txt "CalOPPA requires us to disclose categories of Personal Information we collect and how we use it, the "
|
||||
txt "categories of sources from whom we collect Personal Information, and the third parties with whom we "
|
||||
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 [] [
|
||||
li [] [
|
||||
rawText "Right to Know and Access. You may submit a verifiable request for information regarding the: "
|
||||
rawText "(1) categories of Personal Information we collect, use, or share; (2) purposes for which "
|
||||
rawText "categories of Personal Information are collected or used by us; (3) categories of sources "
|
||||
rawText "from which we collect Personal Information; and (4) specific pieces of Personal Information "
|
||||
rawText "we have collected about you."
|
||||
txt "Right to Know and Access. You may submit a verifiable request for information regarding the: (1) "
|
||||
txt "categories of Personal Information we collect, use, or share; (2) purposes for which categories "
|
||||
txt "of Personal Information are collected or used by us; (3) categories of sources from which we "
|
||||
txt "collect Personal Information; and (4) specific pieces of Personal Information we have collected "
|
||||
txt "about you."
|
||||
]
|
||||
li [] [
|
||||
rawText "Right to Equal Service. We will not discriminate against you if you exercise your privacy "
|
||||
rawText "rights."
|
||||
txt "Right to Equal Service. We will not discriminate against you if you exercise your privacy rights."
|
||||
]
|
||||
li [] [
|
||||
rawText "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 "Right to Delete. You may submit a verifiable request to close your account and we will delete "
|
||||
txt "Personal Information about you that we have collected."
|
||||
]
|
||||
li [] [
|
||||
rawText "Right to request that a business that sells a consumer’s personal data, not sell the "
|
||||
rawText "consumer’s personal data."
|
||||
txt "Right to request that a business that sells a consumer’s personal data, not sell the "
|
||||
txt "consumer’s personal data."
|
||||
]
|
||||
]
|
||||
p [] [
|
||||
rawText "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 "If you make a request, we have one month to respond to you. If you would like to exercise any of "
|
||||
txt "these rights, please contact us."
|
||||
]
|
||||
p [] [ rawText "We do not sell the Personal Information of our users." ]
|
||||
p [] [ rawText "For more information about these rights, please contact us." ]
|
||||
p [] [ txt "We do not sell the Personal Information of our users." ]
|
||||
p [] [ txt "For more information about these rights, please contact us." ]
|
||||
|
||||
h4 [] [ rawText "Contact Us" ]
|
||||
p [] [ rawText "Don’t hesitate to contact us if you have any questions." ]
|
||||
h4 [] [ txt "Contact Us" ]
|
||||
p [] [ txt "Don’t hesitate to contact us if you have any questions." ]
|
||||
ul [] [
|
||||
li [] [
|
||||
rawText "Via this Link: "
|
||||
a [ _href "/how-it-works" ] [rawText "https://noagendacareers.com/how-it-works" ]
|
||||
txt "Via this Link: "; a [ _href "/how-it-works" ] [ txt "https://noagendacareers.com/how-it-works" ]
|
||||
]
|
||||
]
|
||||
|
||||
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 [] [
|
||||
li [ _class "fst-italic" ] [ rawText "Removed references to Mastodon" ]
|
||||
li [ _class "fst-italic" ] [ rawText "Added references to job listings" ]
|
||||
li [ _class "fst-italic" ] [ rawText "Changed information regarding e-mail addresses" ]
|
||||
li [ _class "fst-italic" ] [ rawText "Updated cookie / tracking sections for new architecture" ]
|
||||
li [ _class "fst-italic" ] [ txt "Removed references to Mastodon" ]
|
||||
li [ _class "fst-italic" ] [ txt "Added references to job listings" ]
|
||||
li [ _class "fst-italic" ] [ txt "Changed information regarding e-mail addresses" ]
|
||||
li [ _class "fst-italic" ] [ txt "Updated cookie / tracking sections for new architecture" ]
|
||||
]
|
||||
p [ _class "fst-italic" ] [
|
||||
rawText "Change on September 6<sup>th</sup>, 2021 – replaced “No Agenda Social” with "
|
||||
rawText "generic terms for any authorized Mastodon instance."
|
||||
txt "Change on September 6<sup>th</sup>, 2021 – replaced “No Agenda Social” with generic "
|
||||
txt "terms for any authorized Mastodon instance."
|
||||
]
|
||||
]
|
||||
|
||||
/// The page for terms of service
|
||||
let termsOfService =
|
||||
article [] [
|
||||
h3 [] [ rawText "Terms of Service" ]
|
||||
p [ _class "fst-italic" ] [ rawText "(as of August 30<sup>th</sup>, 2022)" ]
|
||||
h4 [] [ rawText "Acceptance of Terms" ]
|
||||
h3 [] [ txt "Terms of Service" ]
|
||||
p [ _class "fst-italic" ] [ txt "(as of August 30<sup>th</sup>, 2022)" ]
|
||||
h4 [] [ txt "Acceptance of Terms" ]
|
||||
p [] [
|
||||
rawText "By accessing this web site, you are agreeing to be bound by these Terms and Conditions, and that "
|
||||
rawText "you 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 "By accessing this web site, you are agreeing to be bound by these Terms and Conditions, and that you "
|
||||
txt "are responsible to ensure that your use of this site complies with all applicable laws. Your "
|
||||
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 [] [
|
||||
rawText "Jobs, Jobs, Jobs is a service that allows individuals to enter and amend employment profiles and "
|
||||
rawText "job 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 "
|
||||
a [ _href "/privacy-policy" ] [ str "privacy policy" ]
|
||||
rawText " for details on the personal (user) information we maintain."
|
||||
txt "Jobs, Jobs, Jobs is a service that allows individuals to enter and amend employment profiles and job "
|
||||
txt "listings, restricting access to the details of these to other users of this site, unless the "
|
||||
txt "individual specifies that this information should be visible publicly. See our "
|
||||
a [ _href "/privacy-policy" ] [ txt "privacy policy" ]
|
||||
txt " for details on the personal (user) information we maintain."
|
||||
]
|
||||
h4 [] [ rawText "Liability" ]
|
||||
h4 [] [ txt "Liability" ]
|
||||
p [] [
|
||||
rawText "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 "
|
||||
rawText "of this service."
|
||||
txt "This service is provided “as is”, and no warranty (express or implied) exists. The "
|
||||
txt "service and its developers may not be held liable for any damages that may arise through the use of "
|
||||
txt "this service."
|
||||
]
|
||||
h4 [] [ rawText "Updates to Terms" ]
|
||||
h4 [] [ txt "Updates to Terms" ]
|
||||
p [] [
|
||||
rawText "These terms and conditions may be updated at any time. When these terms are updated, users will "
|
||||
rawText "be notified via a notice on the dashboard page. Additionally, the date at the top of this page "
|
||||
rawText "will be updated, and any substantive updates will also be accompanied by a summary of those "
|
||||
rawText "changes."
|
||||
txt "These terms and conditions may be updated at any time. When these terms are updated, users will be "
|
||||
txt "notified via a notice on the dashboard page. Additionally, the date at the top of this page will be "
|
||||
txt "updated, and any substantive updates will also be accompanied by a summary of those changes."
|
||||
]
|
||||
hr []
|
||||
p [] [
|
||||
rawText "You may also wish to review our "
|
||||
a [ _href "/privacy-policy" ] [ rawText "privacy policy" ]
|
||||
rawText " to learn how we handle your data."
|
||||
txt "You may also wish to review our "; a [ _href "/privacy-policy" ] [ txt "privacy policy" ]
|
||||
txt " to learn how we handle your data."
|
||||
]
|
||||
hr []
|
||||
p [ _class "fst-italic" ] [
|
||||
rawText "Change on August 30<sup>th</sup>, 2022 – added references to job listings, removed "
|
||||
rawText "references to Mastodon instances."
|
||||
txt "Change on August 30<sup>th</sup>, 2022 – added references to job listings, removed references "
|
||||
txt "to Mastodon instances."
|
||||
]
|
||||
p [ _class "fst-italic" ] [
|
||||
rawText "Change on September 6<sup>th</sup>, 2021 – replaced “No Agenda Social” with a "
|
||||
rawText "list of all No Agenda-affiliated Mastodon instances."
|
||||
txt "Change on September 6<sup>th</sup>, 2021 – replaced “No Agenda Social” with a list "
|
||||
txt "of all No Agenda-affiliated Mastodon instances."
|
||||
]
|
||||
]
|
||||
|
@ -54,7 +54,7 @@ let private links ctx =
|
||||
a [ _href url
|
||||
_onclick "jjj.hideMenu()"
|
||||
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" ] [
|
||||
if ctx.IsLoggedOn then
|
||||
navLink "/citizen/dashboard" "view-dashboard-variant" "Dashboard"
|
||||
@ -76,12 +76,10 @@ let private links ctx =
|
||||
|
||||
/// Generate mobile and desktop side navigation areas
|
||||
let private sideNavs ctx = [
|
||||
div [ _id "mobileMenu"
|
||||
_class "jjj-mobile-menu offcanvas offcanvas-end"
|
||||
_tabindex "-1"
|
||||
div [ _id "mobileMenu"; _class "jjj-mobile-menu offcanvas offcanvas-end"; _tabindex "-1"
|
||||
_ariaLabelledBy "mobileMenuLabel" ] [
|
||||
div [ _class "offcanvas-header" ] [
|
||||
h5 [ _id "mobileMenuLabel" ] [ rawText "Menu" ]
|
||||
h5 [ _id "mobileMenuLabel" ] [ txt "Menu" ]
|
||||
button [
|
||||
_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 ]
|
||||
]
|
||||
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 [] [ rawText " " ]
|
||||
p [ _class "home-link pb-3" ] [ a [ _href "/" ] [ txt "Jobs, Jobs, Jobs" ] ]
|
||||
emptyP
|
||||
links ctx
|
||||
]
|
||||
]
|
||||
@ -98,18 +96,14 @@ let private sideNavs ctx = [
|
||||
/// Title bars for mobile and desktop
|
||||
let private titleBars = [
|
||||
nav [ _class "d-flex d-md-none navbar navbar-dark" ] [
|
||||
span [ _class "navbar-text" ] [ a [ _href "/" ] [ rawText "Jobs, Jobs, Jobs" ] ]
|
||||
button [ _class "btn"
|
||||
_data "bs-toggle" "offcanvas"
|
||||
_data "bs-target" "#mobileMenu"
|
||||
span [ _class "navbar-text" ] [ a [ _href "/" ] [ txt "Jobs, Jobs, Jobs" ] ]
|
||||
button [ _class "btn"; _data "bs-toggle" "offcanvas"; _data "bs-target" "#mobileMenu"
|
||||
_ariaControls "mobileMenu" ] [ i [ _class "mdi mdi-menu" ] [] ]
|
||||
]
|
||||
nav [ _class "d-none d-md-flex navbar navbar-light bg-light"] [
|
||||
span [] [ rawText " " ]
|
||||
span [] [ txt " " ]
|
||||
span [ _class "navbar-text" ] [
|
||||
rawText "(…and Jobs – "
|
||||
audioClip "pelosi-jobs" (rawText "Let’s Vote for Jobs!")
|
||||
rawText ")"
|
||||
txt "(…and Jobs – "; audioClip "pelosi-jobs" (txt "Let’s Vote for Jobs!"); txt ")"
|
||||
]
|
||||
]
|
||||
]
|
||||
@ -127,9 +121,9 @@ let private htmlFoot =
|
||||
} |> Seq.reduce (+)
|
||||
footer [] [
|
||||
p [ _class "text-muted" ] [
|
||||
str "Jobs, Jobs, Jobs v"; str version; rawText " • "
|
||||
a [ _href "/privacy-policy" ] [ str "Privacy Policy" ]; rawText " • "
|
||||
a [ _href "/terms-of-service" ] [ str "Terms of Service" ]
|
||||
txt $"Jobs, Jobs, Jobs v{version} • "
|
||||
a [ _href "/privacy-policy" ] [ txt "Privacy Policy" ]; txt " • "
|
||||
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"
|
||||
_roleAlert ] [
|
||||
p [ _class "mb-0" ] [
|
||||
if level <> "success" then
|
||||
strong [] [ rawText (parts[0].ToUpperInvariant ()); rawText ": " ]
|
||||
rawText message
|
||||
if level <> "success" then strong [] [ txt $"{parts[0].ToUpperInvariant ()}: " ]
|
||||
txt message
|
||||
]
|
||||
button [ _type "button"; _class "btn-close"; _data "bs-dismiss" "alert"; _ariaLabel "Close" ] []
|
||||
])
|
||||
|
@ -3,7 +3,6 @@
|
||||
module JobsJobsJobs.Views.Listing
|
||||
|
||||
open Giraffe.ViewEngine
|
||||
open Giraffe.ViewEngine.Htmx
|
||||
open JobsJobsJobs.Domain
|
||||
open JobsJobsJobs.Domain.SharedTypes
|
||||
open JobsJobsJobs.ViewModels
|
||||
@ -11,15 +10,14 @@ open JobsJobsJobs.ViewModels
|
||||
|
||||
/// Job listing edit page
|
||||
let edit (m : EditListingForm) continents isNew csrf =
|
||||
article [] [
|
||||
h3 [ _class "pb-3" ] [ rawText (if isNew then "Add a" else "Edit"); rawText " Job Listing" ]
|
||||
pageWithTitle $"""{if isNew then "Add a" else "Edit"} Job Listing""" [
|
||||
form [ _class "row g-3"; _method "POST"; _action "/listing/save" ] [
|
||||
antiForgery csrf
|
||||
input [ _type "hidden"; _name (nameof m.Id); _value m.Id ]
|
||||
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
|
||||
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" ] [
|
||||
@ -27,7 +25,7 @@ let edit (m : EditListingForm) continents isNew csrf =
|
||||
]
|
||||
div [ _class "col-12 col-sm-6 col-md-8" ] [
|
||||
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" ] [
|
||||
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" ] [
|
||||
textBox [ _type "date" ] (nameof m.NeededBy) m.NeededBy "Needed By" false
|
||||
]
|
||||
div [ _class "col-12" ] [
|
||||
button [ _type "submit"; _class "btn btn-primary" ] [
|
||||
i [ _class "mdi mdi-content-save-outline" ] []; rawText " Save"
|
||||
]
|
||||
]
|
||||
div [ _class "col-12" ] [ submitButton "content-save-outline" "Save" ]
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
open System.Net
|
||||
|
||||
/// Page to expire a job listing
|
||||
let expire (m : ExpireListingForm) (listing : Listing) csrf =
|
||||
article [] [
|
||||
h3 [ _class "pb-3" ] [ rawText "Expire Job Listing ("; str listing.Title; rawText ")" ]
|
||||
pageWithTitle $"Expire Job Listing ({WebUtility.HtmlEncode listing.Title})" [
|
||||
p [ _class "fst-italic" ] [
|
||||
rawText "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 "Expiring this listing will remove it from search results. You will be able to see it via your "
|
||||
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" ] [
|
||||
antiForgery csrf
|
||||
@ -61,16 +57,12 @@ let expire (m : ExpireListingForm) (listing : Listing) csrf =
|
||||
]
|
||||
div [ _class "col-12"; _id "successRow" ] [
|
||||
p [] [
|
||||
rawText "Consider telling your fellow citizens about your experience! Comments entered here will "
|
||||
rawText "be visible to logged-on users here, but not to the general public."
|
||||
txt "Consider telling your fellow citizens about your experience! Comments entered here will be "
|
||||
txt "visible to logged-on users here, but not to the general public."
|
||||
]
|
||||
]
|
||||
markdownEditor [] (nameof m.SuccessStory) m.SuccessStory "Your Success Story"
|
||||
div [ _class "col-12" ] [
|
||||
button [ _type "submit"; _class "btn btn-primary" ] [
|
||||
i [ _class "mdi mdi-text-box-remove-outline" ] []; rawText " Expire Listing"
|
||||
]
|
||||
]
|
||||
div [ _class "col-12" ] [ submitButton "text-box-remove-outline" "Expire Listing" ]
|
||||
]
|
||||
jsOnLoad "jjj.listing.toggleFromHere()"
|
||||
]
|
||||
@ -80,17 +72,15 @@ let expire (m : ExpireListingForm) (listing : Listing) csrf =
|
||||
let mine (listings : ListingForView list) tz =
|
||||
let active = listings |> List.filter (fun it -> not it.Listing.IsExpired)
|
||||
let expired = listings |> List.filter (fun it -> it.Listing.IsExpired)
|
||||
article [] [
|
||||
h3 [ _class "pb-3" ] [ rawText "My Job Listings" ]
|
||||
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" ] [ rawText "Active Job Listings" ]
|
||||
if List.isEmpty active then
|
||||
p [ _class "pb-3 fst-italic" ] [ rawText "You have no active job listings" ]
|
||||
pageWithTitle "My Job Listings" [
|
||||
p [] [ a [ _href "/listing/new/edit"; _class "btn btn-outline-primary" ] [ txt "Add a New Job Listing" ] ]
|
||||
if not (List.isEmpty expired) then h4 [ _class "pb-2" ] [ txt "Active Job Listings" ]
|
||||
if List.isEmpty active then p [ _class "pb-3 fst-italic" ] [ txt "You have no active job listings" ]
|
||||
else
|
||||
table [ _class "pb-3 table table-sm table-hover pt-3" ] [
|
||||
thead [] [
|
||||
[ "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 []
|
||||
]
|
||||
active
|
||||
@ -98,9 +88,9 @@ let mine (listings : ListingForView list) tz =
|
||||
let listId = ListingId.toString it.Listing.Id
|
||||
tr [] [
|
||||
td [] [
|
||||
a [ _href $"/listing/{listId}/edit" ] [ rawText "Edit" ]; rawText " ~ "
|
||||
a [ _href $"/listing/{listId}/view" ] [ rawText "View" ]; rawText " ~ "
|
||||
a [ _href $"/listing/{listId}/expire" ] [ rawText "Expire" ]
|
||||
a [ _href $"/listing/{listId}/edit" ] [ txt "Edit" ]; txt " ~ "
|
||||
a [ _href $"/listing/{listId}/view" ] [ txt "View" ]; txt " ~ "
|
||||
a [ _href $"/listing/{listId}/expire" ] [ txt "Expire" ]
|
||||
]
|
||||
td [] [ str it.Listing.Title ]
|
||||
td [] [ str it.ContinentName; rawText " / "; str it.Listing.Region ]
|
||||
@ -110,17 +100,17 @@ let mine (listings : ListingForView list) tz =
|
||||
|> tbody []
|
||||
]
|
||||
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" ] [
|
||||
thead [] [
|
||||
[ "Action"; "Title"; "Filled Here?"; "Expired" ]
|
||||
|> List.map (fun it -> th [ _scope "col" ] [ rawText it ])
|
||||
|> List.map (fun it -> th [ _scope "col" ] [ txt it ])
|
||||
|> tr []
|
||||
]
|
||||
expired
|
||||
|> List.map (fun it ->
|
||||
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 (yesOrNo (defaultArg it.Listing.WasFilledHere false)) ]
|
||||
td [] [ str (fullDateTime it.Listing.UpdatedOn tz) ]
|
||||
@ -136,12 +126,11 @@ let private neededBy dt =
|
||||
(LocalDatePattern.CreateWithCurrentCulture "MMMM d, yyyy").Format dt
|
||||
|
||||
let search (m : ListingSearchForm) continents (listings : ListingForView list option) =
|
||||
article [] [
|
||||
h3 [ _class "pb-3" ] [ rawText "Help Wanted" ]
|
||||
pageWithTitle "Help Wanted" [
|
||||
if Option.isNone listings then
|
||||
p [] [
|
||||
rawText "Enter relevant criteria to find results, or just click “Search” to see all "
|
||||
rawText "current job listings."
|
||||
txt "Enter relevant criteria to find results, or just click “Search” to see all active job "
|
||||
txt "listings."
|
||||
]
|
||||
collapsePanel "Search Criteria" [
|
||||
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" ] [
|
||||
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" ] [
|
||||
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" ] [
|
||||
input [ _type "radio"; _id "remoteNull"; _name (nameof m.RemoteWork); _value ""
|
||||
_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" ] [
|
||||
input [ _type "radio"; _id "remoteYes"; _name (nameof m.RemoteWork); _value "yes"
|
||||
_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" ] [
|
||||
input [ _type "radio"; _id "remoteNo"; _name (nameof m.RemoteWork); _value "no"
|
||||
_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" ] [
|
||||
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 "col" ] [
|
||||
br []
|
||||
button [ _type "submit"; _class "btn btn-outline-primary" ] [ rawText "Search" ]
|
||||
button [ _type "submit"; _class "btn btn-outline-primary" ] [ txt "Search" ]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
match listings with
|
||||
| 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 ->
|
||||
table [ _class "table table-sm table-hover pt-3" ] [
|
||||
thead [] [
|
||||
tr [] [
|
||||
th [ _scope "col" ] [ rawText "Listing" ]
|
||||
th [ _scope "col" ] [ rawText "Title" ]
|
||||
th [ _scope "col" ] [ rawText "Location" ]
|
||||
th [ _scope "col"; _class "text-center" ] [ rawText "Remote?" ]
|
||||
th [ _scope "col"; _class "text-center" ] [ rawText "Needed By" ]
|
||||
th [ _scope "col" ] [ txt "Listing" ]
|
||||
th [ _scope "col" ] [ txt "Title" ]
|
||||
th [ _scope "col" ] [ txt "Location" ]
|
||||
th [ _scope "col"; _class "text-center" ] [ txt "Remote?" ]
|
||||
th [ _scope "col"; _class "text-center" ] [ txt "Needed By" ]
|
||||
]
|
||||
]
|
||||
r |> List.map (fun it ->
|
||||
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.ContinentName; rawText " / "; str it.Listing.Region ]
|
||||
td [ _class "text-center" ] [ str (yesOrNo it.Listing.IsRemote) ]
|
||||
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 []
|
||||
@ -221,20 +210,18 @@ let view (it : ListingForView) =
|
||||
str it.Listing.Title
|
||||
if it.Listing.IsExpired then
|
||||
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
|
||||
rawText " "
|
||||
span [ _class "badge bg-success" ] [ rawText "Filled via Jobs, Jobs, Jobs" ]
|
||||
txt " "; span [ _class "badge bg-success" ] [ txt "Filled via Jobs, Jobs, Jobs" ]
|
||||
]
|
||||
]
|
||||
h4 [ _class "pb-3 text-muted" ] [ str it.ContinentName; rawText " / "; str it.Listing.Region ]
|
||||
p [] [
|
||||
match it.Listing.NeededBy with
|
||||
| Some needed ->
|
||||
strong [] [ em [] [ rawText "NEEDED BY "; str ((neededBy needed).ToUpperInvariant ()) ] ]
|
||||
rawText " • "
|
||||
strong [] [ em [] [ txt "NEEDED BY "; str ((neededBy needed).ToUpperInvariant ()) ] ]; txt " • "
|
||||
| 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
|
||||
]
|
||||
hr []
|
||||
|
@ -14,29 +14,25 @@ let skillEdit (skills : SkillForm array) =
|
||||
div [ _id $"skillRow{idx}"; _class "row pb-3" ] [
|
||||
div [ _class "col-2 col-md-1 align-self-center" ] [
|
||||
button [ _class "btn btn-sm btn-outline-danger rounded-pill"; _title "Delete"
|
||||
_onclick $"jjj.profile.removeSkill(idx)" ] [
|
||||
rawText " − "
|
||||
]
|
||||
_onclick $"jjj.profile.removeSkill(idx)" ] [ txt " − " ]
|
||||
]
|
||||
div [ _class "col-10 col-md-6" ] [
|
||||
div [ _class "form-floating" ] [
|
||||
input [ _type "text"; _id $"skillDesc{idx}"; _name $"Skills[{idx}].Description"
|
||||
_class "form-control"; _placeholder "A skill (language, design technique, process, etc.)"
|
||||
_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
|
||||
div [ _class "form-text" ] [ rawText "A skill (language, design technique, process, etc.)" ]
|
||||
if idx < 1 then div [ _class "form-text" ] [ txt "A skill (language, design technique, process, etc.)" ]
|
||||
]
|
||||
div [ _class "col-12 col-md-5" ] [
|
||||
div [ _class "form-floating" ] [
|
||||
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)"
|
||||
_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
|
||||
div [ _class "form-text" ] [ rawText "A further description of the skill" ]
|
||||
if idx < 1 then div [ _class "form-text" ] [ txt "A further description of the skill" ]
|
||||
]
|
||||
]
|
||||
template [ _id "newSkill" ] [ mapToInputs -1 { Description = ""; Notes = "" } ]
|
||||
@ -44,18 +40,15 @@ let skillEdit (skills : SkillForm array) =
|
||||
|
||||
/// The profile edit page
|
||||
let edit (m : EditProfileViewModel) continents isNew citizenId csrf =
|
||||
article [] [
|
||||
h3 [ _class "pb-3" ] [ rawText "My Employment Profile" ]
|
||||
pageWithTitle "My Employment Profile" [
|
||||
form [ _class "row g-3"; _action "/profile/save"; _hxPost "/profile/save" ] [
|
||||
antiForgery csrf
|
||||
div [ _class "col-12" ] [
|
||||
checkBox [] (nameof m.IsSeekingEmployment) m.IsSeekingEmployment "I am currently seeking employment"
|
||||
if m.IsSeekingEmployment then
|
||||
p [] [
|
||||
em [] [
|
||||
rawText "If you have found employment, consider "
|
||||
a [ _href "/success-story/new/edit" ] [ rawText "telling your fellow citizens about it!" ]
|
||||
]
|
||||
p [ _class "fst-italic " ] [
|
||||
txt "If you have found employment, consider "
|
||||
a [ _href "/success-story/new/edit" ] [ txt "telling your fellow citizens about it!" ]
|
||||
]
|
||||
]
|
||||
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" ] [
|
||||
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"
|
||||
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" ] [
|
||||
hr []
|
||||
h4 [ _class "pb-2" ] [
|
||||
rawText "Skills "
|
||||
txt "Skills "
|
||||
button [ _type "button"; _class "btn btn-sm btn-outline-primary rounded-pill"
|
||||
_onclick "jjj.profile.addSkill()" ] [
|
||||
rawText "Add a Skill"
|
||||
]
|
||||
_onclick "jjj.profile.addSkill()" ] [ txt "Add a Skill" ]
|
||||
]
|
||||
]
|
||||
yield! skillEdit m.Skills
|
||||
div [ _class "col-12" ] [
|
||||
hr []
|
||||
h4 [] [ rawText "Experience" ]
|
||||
h4 [] [ txt "Experience" ]
|
||||
p [] [
|
||||
rawText "This application does not have a place to individually list your chronological job "
|
||||
rawText "history; however, you can use this area to list prior jobs, their dates, and anything "
|
||||
rawText "else you want to include that’s not already a part of your Professional Biography "
|
||||
rawText "above."
|
||||
txt "This application does not have a place to individually list your chronological job history; "
|
||||
txt "however, you can use this area to list prior jobs, their dates, and anything else you want to "
|
||||
txt "include that’s not already a part of your Professional Biography above."
|
||||
]
|
||||
]
|
||||
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"
|
||||
]
|
||||
div [ _class "col-12" ] [
|
||||
button [ _type "submit"; _class "btn btn-primary" ] [
|
||||
i [ _class "mdi mdi-content-save-outline" ] []; rawText " Save"
|
||||
]
|
||||
submitButton "content-save-outline" "Save"
|
||||
if not isNew then
|
||||
rawText " "
|
||||
txt " "
|
||||
a [ _class "btn btn-outline-secondary"; _href $"/profile/{CitizenId.toString citizenId}/view" ] [
|
||||
i [ _color "#6c757d"; _class "mdi mdi-file-account-outline" ] []
|
||||
rawText " View Your User Profile"
|
||||
txt " View Your User Profile"
|
||||
]
|
||||
]
|
||||
]
|
||||
hr []
|
||||
p [ _class "text-muted fst-italic" ] [
|
||||
rawText "(If you want to delete your profile, or your entire account, "
|
||||
a [ _href "/citizen/so-long" ] [ rawText "see your deletion options here" ]; rawText ".)"
|
||||
txt "(If you want to delete your profile, or your entire account, "
|
||||
a [ _href "/citizen/so-long" ] [ txt "see your deletion options here" ]; txt ".)"
|
||||
]
|
||||
jsOnLoad $"jjj.profile.nextIndex = {m.Skills.Length}"
|
||||
]
|
||||
@ -125,12 +113,11 @@ let edit (m : EditProfileViewModel) continents isNew citizenId csrf =
|
||||
|
||||
/// The public search page
|
||||
let publicSearch (m : PublicSearchForm) continents (results : PublicSearchResult list option) =
|
||||
article [] [
|
||||
h3 [ _class "pb-3" ] [ rawText "People Seeking Work" ]
|
||||
pageWithTitle "People Seeking Work" [
|
||||
if Option.isNone results then
|
||||
p [] [
|
||||
rawText "Enter one or more criteria to filter results, or just click “Search” to list all "
|
||||
rawText "publicly searchable profiles."
|
||||
txt "Enter one or more criteria to filter results, or just click “Search” to list all "
|
||||
txt "publicly searchable profiles."
|
||||
]
|
||||
collapsePanel "Search Criteria" [
|
||||
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" ] [
|
||||
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" ] [
|
||||
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" ] [
|
||||
input [ _type "radio"; _id "remoteNull"; _name (nameof m.RemoteWork); _value ""
|
||||
_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" ] [
|
||||
input [ _type "radio"; _id "remoteYes"; _name (nameof m.RemoteWork); _value "yes"
|
||||
_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" ] [
|
||||
input [ _type "radio"; _id "remoteNo"; _name (nameof m.RemoteWork); _value "no"
|
||||
_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" ] [
|
||||
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 "col" ] [
|
||||
br []
|
||||
button [ _type "submit"; _class "btn btn-outline-primary" ] [ rawText "Search" ]
|
||||
button [ _type "submit"; _class "btn btn-outline-primary" ] [ txt "Search" ]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
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 ->
|
||||
p [ _class "py-3" ] [
|
||||
rawText "These profiles match your search criteria. To learn more about these people, join the merry "
|
||||
rawText "band of human resources in the "
|
||||
a [ _href "https://noagendashow.net"; _target "_blank"; _rel "noopener" ] [ rawText "No Agenda" ]
|
||||
rawText " tribe!"
|
||||
txt "These profiles match your search criteria. To learn more about these people, join the merry band "
|
||||
txt "of human resources in the "
|
||||
a [ _href "https://noagendashow.net"; _target "_blank"; _rel "noopener" ] [ txt "No Agenda" ]
|
||||
txt " tribe!"
|
||||
]
|
||||
table [ _class "table table-sm table-hover" ] [
|
||||
thead [] [
|
||||
tr [] [
|
||||
th [ _scope "col" ] [ rawText "Continent" ]
|
||||
th [ _scope "col"; _class "text-center" ] [ rawText "Region" ]
|
||||
th [ _scope "col"; _class "text-center" ] [ rawText "Remote?" ]
|
||||
th [ _scope "col"; _class "text-center" ] [ rawText "Skills" ]
|
||||
th [ _scope "col" ] [ txt "Continent" ]
|
||||
th [ _scope "col"; _class "text-center" ] [ txt "Region" ]
|
||||
th [ _scope "col"; _class "text-center" ] [ txt "Remote?" ]
|
||||
th [ _scope "col"; _class "text-center" ] [ txt "Skills" ]
|
||||
]
|
||||
]
|
||||
r |> List.map (fun profile ->
|
||||
tr [] [
|
||||
td [] [ str profile.Continent ]
|
||||
td [] [ str profile.Region ]
|
||||
td [ _class "text-center" ] [ rawText (yesOrNo profile.RemoteWork) ]
|
||||
td [ _class "text-center" ] [ txt (yesOrNo profile.RemoteWork) ]
|
||||
profile.Skills
|
||||
|> List.collect (fun skill -> [ str skill; br [] ])
|
||||
|> td []
|
||||
@ -209,12 +196,11 @@ let publicSearch (m : PublicSearchForm) continents (results : PublicSearchResult
|
||||
|
||||
/// Logged-on search page
|
||||
let search (m : ProfileSearchForm) continents tz (results : ProfileSearchResult list option) =
|
||||
article [] [
|
||||
h3 [ _class "pb-3" ] [ rawText "Search Profiles" ]
|
||||
pageWithTitle "Search Profiles" [
|
||||
if Option.isNone results then
|
||||
p [] [
|
||||
rawText "Enter one or more criteria to filter results, or just click “Search” to list all "
|
||||
rawText "profiles."
|
||||
txt "Enter one or more criteria to filter results, or just click “Search” to list all "
|
||||
txt "profiles."
|
||||
]
|
||||
collapsePanel "Search Criteria" [
|
||||
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
|
||||
]
|
||||
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" ] [
|
||||
input [ _type "radio"; _id "remoteNull"; _name (nameof m.RemoteWork); _value ""
|
||||
_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" ] [
|
||||
input [ _type "radio"; _id "remoteYes"; _name (nameof m.RemoteWork); _value "yes"
|
||||
_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" ] [
|
||||
input [ _type "radio"; _id "remoteNo"; _name (nameof m.RemoteWork); _value "no"
|
||||
_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" ] [
|
||||
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" ] [
|
||||
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 "col" ] [
|
||||
br []
|
||||
button [ _type "submit"; _class "btn btn-outline-primary" ] [ rawText "Search" ]
|
||||
button [ _type "submit"; _class "btn btn-outline-primary" ] [ txt "Search" ]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
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 ->
|
||||
// Bootstrap utility classes to only show at medium or above
|
||||
let isWide = "d-none d-md-table-cell"
|
||||
table [ _class "table table-sm table-hover pt-3" ] [
|
||||
thead [] [
|
||||
tr [] [
|
||||
th [ _scope "col" ] [ rawText "Profile" ]
|
||||
th [ _scope "col" ] [ rawText "Name" ]
|
||||
th [ _scope "col"; _class $"{isWide} text-center" ] [ rawText "Seeking?" ]
|
||||
th [ _scope "col"; _class "text-center" ] [ rawText "Remote?" ]
|
||||
th [ _scope "col"; _class $"{isWide} text-center" ] [ rawText "Full-Time?" ]
|
||||
th [ _scope "col"; _class isWide ] [ rawText "Last Updated" ]
|
||||
th [ _scope "col" ] [ txt "Profile" ]
|
||||
th [ _scope "col" ] [ txt "Name" ]
|
||||
th [ _scope "col"; _class $"{isWide} text-center" ] [ txt "Seeking?" ]
|
||||
th [ _scope "col"; _class "text-center" ] [ txt "Remote?" ]
|
||||
th [ _scope "col"; _class $"{isWide} text-center" ] [ txt "Full-Time?" ]
|
||||
th [ _scope "col"; _class isWide ] [ txt "Last Updated" ]
|
||||
]
|
||||
]
|
||||
r |> List.map (fun profile ->
|
||||
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 [ _class $"{isWide} text-center" ] [ rawText (yesOrNo profile.SeekingEmployment) ]
|
||||
td [ _class "text-center" ] [ rawText (yesOrNo profile.RemoteWork) ]
|
||||
td [ _class $"{isWide} text-center" ] [ rawText (yesOrNo profile.FullTime) ]
|
||||
td [ _class $"{isWide} text-center" ] [ txt (yesOrNo profile.SeekingEmployment) ]
|
||||
td [ _class "text-center" ] [ txt (yesOrNo profile.RemoteWork) ]
|
||||
td [ _class $"{isWide} text-center" ] [ txt (yesOrNo profile.FullTime) ]
|
||||
td [ _class isWide ] [ str (fullDate profile.LastUpdatedOn tz) ]
|
||||
])
|
||||
|> tbody []
|
||||
@ -296,38 +282,34 @@ let view (citizen : Citizen) (profile : Profile) (continentName : string) curren
|
||||
str (Citizen.name citizen)
|
||||
if profile.IsSeekingEmployment then
|
||||
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}" ]
|
||||
contactInfo citizen (Option.isNone currentId)
|
||||
|> div [ _class "pb-3" ]
|
||||
p [] [
|
||||
rawText (if profile.IsFullTime then "I" else "Not i"); rawText "nterested in full-time employment"
|
||||
rawText " • "
|
||||
rawText (if profile.IsRemote then "I" else "Not i"); rawText "nterested in remote opportunities"
|
||||
txt (if profile.IsFullTime then "I" else "Not i"); txt "nterested in full-time employment • "
|
||||
txt (if profile.IsRemote then "I" else "Not i"); txt "nterested in remote opportunities"
|
||||
]
|
||||
hr []
|
||||
div [] [ md2html profile.Biography ]
|
||||
if not (List.isEmpty profile.Skills) then
|
||||
hr []
|
||||
h4 [ _class "pb-3" ] [ rawText "Skills" ]
|
||||
h4 [ _class "pb-3" ] [ txt "Skills" ]
|
||||
profile.Skills
|
||||
|> List.map (fun skill ->
|
||||
li [] [
|
||||
str skill.Description
|
||||
match skill.Notes with
|
||||
| Some notes ->
|
||||
rawText " ("; str notes; rawText ")"
|
||||
| None -> ()
|
||||
match skill.Notes with Some notes -> txt " ("; str notes; txt ")" | None -> ()
|
||||
])
|
||||
|> ul []
|
||||
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 -> ()
|
||||
if Option.isSome currentId && currentId.Value = citizen.Id then
|
||||
br []; br []
|
||||
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
|
||||
let edit (m : EditSuccessForm) isNew pgTitle csrf =
|
||||
article [] [
|
||||
h3 [ _class "pb-3" ] [ rawText pgTitle ]
|
||||
pageWithTitle pgTitle [
|
||||
if isNew then
|
||||
p [] [
|
||||
rawText "Congratulations on your employment! Your fellow citizens would enjoy hearing how it all came "
|
||||
rawText "about; tell us about it below! "
|
||||
em [] [ rawText "(These will be visible to other users, but not to the general public.)" ]
|
||||
txt "Congratulations on your employment! Your fellow citizens would enjoy hearing how it all came "
|
||||
txt "about; tell us about it below! "
|
||||
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" ] [
|
||||
antiForgery csrf
|
||||
@ -25,13 +24,11 @@ let edit (m : EditSuccessForm) isNew pgTitle csrf =
|
||||
]
|
||||
markdownEditor [] (nameof m.Story) m.Story "The Success Story"
|
||||
div [ _class "col-12" ] [
|
||||
button [ _type "submit"; _class "btn btn-primary" ] [
|
||||
i [ _class "mdi mdi-content-save-outline" ] []; rawText " Save"
|
||||
]
|
||||
submitButton "content-save-outline" "Save"
|
||||
if isNew then
|
||||
p [ _class "fst-italic" ] [
|
||||
rawText "(Saving this will set “Seeking Employment” to “No” on your "
|
||||
rawText "profile.)"
|
||||
txt "(Saving this will set “Seeking Employment” to “No” on your "
|
||||
txt "profile.)"
|
||||
]
|
||||
]
|
||||
]
|
||||
@ -40,28 +37,27 @@ let edit (m : EditSuccessForm) isNew pgTitle csrf =
|
||||
|
||||
/// The list of success stories
|
||||
let list (m : StoryEntry list) citizenId tz =
|
||||
article [] [
|
||||
h3 [ _class "pb-3" ] [ rawText "Success Stories" ]
|
||||
pageWithTitle "Success Stories" [
|
||||
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
|
||||
table [ _class "table table-sm table-hover" ] [
|
||||
thead [] [
|
||||
[ "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 []
|
||||
]
|
||||
m |> List.map (fun story ->
|
||||
tr [] [
|
||||
td [] [
|
||||
let theId = SuccessId.toString story.Id
|
||||
if story.HasStory then a [ _href $"/success-story/{theId}/view" ] [ rawText "View" ]
|
||||
else em [] [ rawText "None" ]
|
||||
if story.HasStory then a [ _href $"/success-story/{theId}/view" ] [ txt "View" ]
|
||||
else em [] [ txt "None" ]
|
||||
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 [] [ 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) ]
|
||||
])
|
||||
|> tbody []
|
||||
@ -73,14 +69,13 @@ let list (m : StoryEntry list) citizenId tz =
|
||||
let view (it : Success) citizenName tz =
|
||||
article [] [
|
||||
h3 [] [
|
||||
str citizenName; rawText "’s Success Story"
|
||||
str citizenName; txt "’s Success Story"
|
||||
if it.IsFromHere then
|
||||
span [ _class "jjj-heading-label" ] [
|
||||
rawText " "
|
||||
txt " "
|
||||
span [ _class "badge bg-success" ] [
|
||||
rawText "Via "
|
||||
rawText (if it.Source = "profile" then "employment profile" else "job listing")
|
||||
rawText " on Jobs, Jobs, Jobs"
|
||||
txt "Via "; txt (if it.Source = "profile" then "employment profile" else "job listing")
|
||||
txt " on Jobs, Jobs, Jobs"
|
||||
]
|
||||
]
|
||||
]
|
||||
|
Loading…
Reference in New Issue
Block a user