diff --git a/app/controllers/AdminOrdersController.asp b/app/controllers/AdminOrdersController.asp new file mode 100644 index 0000000..f87255f --- /dev/null +++ b/app/controllers/AdminOrdersController.asp @@ -0,0 +1,65 @@ +<% +Class AdminOrdersController_Class + Private m_useLayout + Private m_title + + Private Sub Class_Initialize() + m_useLayout = True + m_title = "Orders" + End Sub + + Public Property Get useLayout + useLayout = m_useLayout + End Property + + Public Property Let useLayout(v) + m_useLayout = v + End Property + + Public Property Get Title + Title = m_title + End Property + + Public Property Let Title(v) + m_title = v + End Property + + '------------------------------------------------------------------------------------------------------------------- + ' GET: list orders (searchable by email/jurisdiction #, paginated) with a mailto link to + ' resend the continuation URL for each. + '------------------------------------------------------------------------------------------------------------------- + Public Sub Index() + Dim orders, searchTerm, pageNum, perPage, pageCount, recordCount + perPage = 20 + + searchTerm = Trim(Request.QueryString("q")) + + If IsNumeric(Request.QueryString("page")) And CLng(Request.QueryString("page")) >= 1 Then + pageNum = CLng(Request.QueryString("page")) + Else + pageNum = 1 + End If + + Set orders = OrdersRepository().SearchPaged(searchTerm, perPage, pageNum, pageCount, recordCount) + + ' If the requested page is past the last one (e.g. a stale bookmark after a search + ' narrows the results), clamp to the last real page and re-fetch. + If pageCount > 0 And pageNum > pageCount Then + pageNum = pageCount + Set orders = OrdersRepository().SearchPaged(searchTerm, perPage, pageNum, pageCount, recordCount) + End If + %> + + <% + End Sub + +End Class + +Dim AdminOrdersController_Class__Singleton +Function AdminOrdersController() + If IsEmpty(AdminOrdersController_Class__Singleton) Then + Set AdminOrdersController_Class__Singleton = New AdminOrdersController_Class + End If + Set AdminOrdersController = AdminOrdersController_Class__Singleton +End Function +%> diff --git a/app/controllers/autoload_controllers.asp b/app/controllers/autoload_controllers.asp index e70dc3b..e743b94 100644 --- a/app/controllers/autoload_controllers.asp +++ b/app/controllers/autoload_controllers.asp @@ -10,4 +10,5 @@ - \ No newline at end of file + + \ No newline at end of file diff --git a/app/models/OrdersRepository.asp b/app/models/OrdersRepository.asp index a589249..08c9190 100644 --- a/app/models/OrdersRepository.asp +++ b/app/models/OrdersRepository.asp @@ -18,6 +18,80 @@ Class OrdersRepository_Class CreateOrder = token End Function + ' Returns a Scripting.Dictionary of Scripting.Dictionary rows for every order, newest first. + Public Function GetAll() + Dim rs, results, order + Set results = Server.CreateObject("Scripting.Dictionary") + + Set rs = DAL.Query( _ + "SELECT OrderID, Email, JurisdictionNumber, Token, TokenExpiresAt, TokenUsedAt, CreatedAt FROM Orders ORDER BY OrderID DESC", _ + Empty) + + Do While Not rs.EOF + Set order = Server.CreateObject("Scripting.Dictionary") + order.Add "OrderID", rs("OrderID").Value + order.Add "Email", rs("Email").Value + order.Add "JurisdictionNumber", rs("JurisdictionNumber").Value + order.Add "Token", rs("Token").Value + order.Add "TokenExpiresAt", rs("TokenExpiresAt").Value + order.Add "TokenUsedAt", rs("TokenUsedAt").Value + order.Add "CreatedAt", rs("CreatedAt").Value + results.Add rs("OrderID").Value, order + rs.MoveNext + Loop + + Destroy rs + Set GetAll = results + End Function + + ' Returns a Scripting.Dictionary of Scripting.Dictionary rows (newest first) matching searchTerm + ' against Email or JurisdictionNumber (all rows if searchTerm is blank), one page at a time. + ' page_count/record_count are returned by reference for building pagination controls. + Public Function SearchPaged(searchTerm, per_page, page_num, ByRef page_count, ByRef record_count) + Dim sql, params + sql = "SELECT OrderID, Email, JurisdictionNumber, Token, TokenExpiresAt, TokenUsedAt, CreatedAt FROM Orders" + page_count = 0 + record_count = 0 + + If Len(Trim(searchTerm)) > 0 Then + sql = sql & " WHERE Email LIKE ? OR JurisdictionNumber LIKE ?" + params = Array("%" & searchTerm & "%", "%" & searchTerm & "%") + Else + params = Empty + End If + + sql = sql & " ORDER BY OrderID DESC" + + Dim rs : Set rs = DAL.PagedQuery(sql, params, per_page, page_num) + + If Not rs.EOF Then + rs.PageSize = per_page + rs.AbsolutePage = page_num + page_count = rs.PageCount + record_count = rs.RecordCount + End If + + Dim results, order, x + Set results = Server.CreateObject("Scripting.Dictionary") + x = 0 + Do While (per_page <= 0 Or x < per_page) And Not rs.EOF + Set order = Server.CreateObject("Scripting.Dictionary") + order.Add "OrderID", rs("OrderID").Value + order.Add "Email", rs("Email").Value + order.Add "JurisdictionNumber", rs("JurisdictionNumber").Value + order.Add "Token", rs("Token").Value + order.Add "TokenExpiresAt", rs("TokenExpiresAt").Value + order.Add "TokenUsedAt", rs("TokenUsedAt").Value + order.Add "CreatedAt", rs("CreatedAt").Value + results.Add rs("OrderID").Value, order + x = x + 1 + rs.MoveNext + Loop + + Destroy rs + Set SearchPaged = results + End Function + ' Returns a Scripting.Dictionary describing the order matching the given token, or Nothing if not found. Public Function FindByToken(token) Dim rs diff --git a/app/views/AdminOrders/index.asp b/app/views/AdminOrders/index.asp new file mode 100644 index 0000000..4a7f4c6 --- /dev/null +++ b/app/views/AdminOrders/index.asp @@ -0,0 +1,87 @@ +
+

Orders

+ +
+
+ +
+
+ +
+ <% If Len(searchTerm) > 0 Then %> +
+ Clear +
+ <% End If %> +
+ + + + + + + + + + + + + + <% If orders.Count = 0 Then %> + + <% Else + Dim orderKey, order, tokenStatus, continueUrl, mailtoBody, mailtoHref + For Each orderKey In orders.Keys + Set order = orders(orderKey) + + If Not IsNull(order("TokenUsedAt")) Then + tokenStatus = "Used" + ElseIf order("TokenExpiresAt") < Now() Then + tokenStatus = "Expired" + Else + tokenStatus = "Active" + End If + + continueUrl = "https://pe.kentcommunications.com/order/continue?token=" & MailtoEncode(order("Token")) + mailtoBody = "Continue your order here: " & continueUrl + mailtoHref = "mailto:" & order("Email") & _ + "?subject=" & MailtoEncode("Continue your Purple Envelope order") & _ + "&body=" & MailtoEncode(mailtoBody) + %> + + + + + + + + + <% + Next + End If + %> + +
Order IDEmailJurisdiction #CreatedToken StatusResend Link
<% If Len(searchTerm) > 0 Then %>No orders match your search.<% Else %>No orders found.<% End If %>
<%= H(order("OrderID")) %><%= H(order("Email")) %><%= H(order("JurisdictionNumber")) %><%= H(order("CreatedAt")) %><%= H(tokenStatus) %>Email link
+ + <% If pageCount > 1 Then + Dim qsBase, pn + qsBase = "?q=" & Server.URLEncode(searchTerm) & "&page=" + %> + +

<%= recordCount %> order<% If recordCount <> 1 Then %>s<% End If %> found.

+ <% End If %> +
diff --git a/applicationhost.config b/applicationhost.config index 8d6b56d..0354ca0 100644 --- a/applicationhost.config +++ b/applicationhost.config @@ -162,6 +162,14 @@ + + + + + + + + diff --git a/core/helpers.asp b/core/helpers.asp index 15dc749..fa03af4 100644 --- a/core/helpers.asp +++ b/core/helpers.asp @@ -201,6 +201,14 @@ Function H(s) End Function +'=============================================================================================================================== +' mailto: links require %20 for spaces, not "+" - Server.URLEncode produces "+", so swap it back after encoding. +'=============================================================================================================================== +Function MailtoEncode(s) + MailtoEncode = Replace(Server.URLEncode(s), "+", "%20") +End Function + + '======================================================================================================================= ' Adapted from Tolerable library '======================================================================================================================= diff --git a/core/lib.ControllerRegistry.asp b/core/lib.ControllerRegistry.asp index edaca35..2a184fc 100644 --- a/core/lib.ControllerRegistry.asp +++ b/core/lib.ControllerRegistry.asp @@ -18,6 +18,7 @@ Class ControllerRegistry_Class RegisterController "requestordercontroller" RegisterController "ordercontroller" RegisterController "orderapicontroller" + RegisterController "adminorderscontroller" End Sub Private Sub Class_Terminate() diff --git a/public-admin/Default.asp b/public-admin/Default.asp new file mode 100644 index 0000000..821d1dc --- /dev/null +++ b/public-admin/Default.asp @@ -0,0 +1,13 @@ + + +<% + ' Admin site routes + router.AddRoute "GET", "/", "AdminOrdersController", "Index" + router.AddRoute "GET", "", "AdminOrdersController", "Index" + router.AddRoute "GET", "/orders", "AdminOrdersController", "Index" + router.AddRoute "GET", "/404", "ErrorController", "NotFound" + + ' Dispatch the request (resolves route and executes controller action) + MVC.DispatchRequest Request.ServerVariables("REQUEST_METHOD"), _ + TrimQueryParams(Request.ServerVariables("HTTP_X_ORIGINAL_URL")) +%> diff --git a/public-admin/css/styles.css b/public-admin/css/styles.css new file mode 100644 index 0000000..6bb8f95 --- /dev/null +++ b/public-admin/css/styles.css @@ -0,0 +1,1100 @@ +:root { + --purple-950: #24103a; + --purple-900: #32134f; + --purple-800: #4d1d78; + --purple-700: #6529a0; + --purple-600: #7c3bc0; + --purple-500: #9658d5; + --purple-200: #dcc9ef; + --purple-100: #f0e7f8; + --purple-50: #faf7fd; + --ink: #201a27; + --muted: #6d6574; + --line: #e4dce9; + --surface: #ffffff; + --surface-soft: #f8f5fa; + --success: #247a50; + --shadow-sm: 0 10px 30px rgba(52, 25, 76, 0.08); + --shadow-lg: 0 28px 80px rgba(52, 25, 76, 0.18); + --radius-sm: 12px; + --radius-md: 20px; + --radius-lg: 32px; + --container: 1180px; +} + +* { + box-sizing: border-box; +} + +html { + height: 100%; + scroll-behavior: smooth; +} + +body { + height: 100vh; + min-height: 100vh; + margin: 0; + display: flex; + flex-direction: column; + overflow: hidden; + color: var(--ink); + background: + url("/images/swaggy%20geo.png"); + background-size: auto; + background-position: top left; + background-repeat: repeat; + font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + line-height: 1.6; +} + +button, +input, +select, +textarea { + font: inherit; +} + +a { + color: inherit; + text-decoration: none; +} + +img { + max-width: 100%; +} + +.container { + width: min(calc(100% - 40px), var(--container)); + margin-inline: auto; +} + +.site-header { + position: relative; + z-index: 50; + flex: 0 0 auto; + background: + linear-gradient(rgba(0, 0, 0, 0.18), rgba(0, 0, 0, 0.18)), + url("/images/KCI%20geo.png"); + background-size: cover; + background-position: center; + background-repeat: no-repeat; + border-bottom: 1px solid rgba(255, 255, 255, 0.12); + box-shadow: none; + backdrop-filter: none; +} + +.nav-wrap { + min-height: 78px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 28px; +} + +.brand { + display: inline-flex; + align-items: center; + gap: 12px; +} + +.brand-mark { + width: 44px; + height: 44px; + display: grid; + place-items: center; + color: white; + background: rgba(255, 255, 255, 0.16); + border: 1px solid rgba(255, 255, 255, 0.32); + border-radius: 14px; + font-weight: 800; + letter-spacing: -0.04em; + box-shadow: none; +} + +.brand strong, +.brand small { + display: block; +} + +.brand strong { + font-size: 0.98rem; + line-height: 1.25; +} + +.brand small { + margin-top: 2px; + color: var(--muted); + font-size: 0.72rem; +} + +.main-nav { + display: flex; + align-items: center; + gap: 30px; + color: #4c4352; + font-size: 0.92rem; + font-weight: 600; +} + +.main-nav a:hover { + color: var(--purple-700); +} + +.button { + min-height: 50px; + display: inline-flex; + align-items: center; + justify-content: center; + text-align: center; + line-height: 1.2; + padding: 0 24px; + border: 1px solid transparent; + border-radius: 999px; + cursor: pointer; + font-weight: 700; + transition: + transform 160ms ease, + box-shadow 160ms ease, + background-color 160ms ease; +} + +.button:hover { + transform: translateY(-2px); +} + +.button-small { + min-height: 42px; + padding-inline: 18px; + font-size: 0.88rem; +} + +.button-primary { + color: white; + background: linear-gradient(135deg, var(--purple-800), var(--purple-600)); + box-shadow: 0 12px 24px rgba(101, 41, 160, 0.2); +} + +.button-primary:hover { + box-shadow: 0 16px 30px rgba(101, 41, 160, 0.28); +} + +.button-secondary { + color: var(--purple-800); + background: var(--purple-100); +} + +.button-outline { + color: var(--purple-800); + border-color: var(--purple-200); + background: white; +} + +.button-light { + color: var(--purple-900); + background: white; +} + +.button-full { + width: 100%; +} + +.hero { + position: relative; + overflow: hidden; + padding: 95px 0 105px; + background: + radial-gradient(circle at 76% 22%, rgba(150, 88, 213, 0.18), transparent 30%), + linear-gradient(180deg, #fff 0%, var(--purple-50) 100%); +} + +.hero::before { + content: none; +} + + +.hero-copy { + padding-left: 32px; +} + +.eyebrow { + display: inline-block; + color: var(--purple-700); + font-size: 0.75rem; + font-weight: 800; + letter-spacing: 0.14em; + text-transform: uppercase; +} + +.hero h1 { + max-width: 760px; + margin: 18px 0 22px; + font-size: clamp(2.65rem, 5vw, 4.65rem); + line-height: 1.02; + letter-spacing: -0.055em; +} + +.hero-lead { + max-width: 665px; + margin: 0; + color: var(--muted); + font-size: 1.1rem; +} + +.hero-actions { + display: flex; + flex-wrap: wrap; + gap: 14px; + margin-top: 34px; +} + +.trust-list { + display: flex; + flex-wrap: wrap; + gap: 18px 28px; + margin: 32px 0 0; + padding: 0; + list-style: none; + color: #514859; + font-size: 0.87rem; + font-weight: 600; +} + +.trust-list li::before { + content: "\2713"; + margin-right: 8px; + color: var(--success); + font-weight: 800; +} + +.hero-visual { + position: relative; + min-height: 480px; + display: grid; + place-items: center; +} + +.envelope-card { + min-height: 320px; + background-image: url("/images/back.png"); + background-size: contain; + background-position: center center; + background-repeat: no-repeat; + border: 1px solid rgba(255,255,255,0.34); + border-radius: 24px; + box-shadow: var(--shadow-lg); + transform: rotate(-3deg); +} + +.stat-card { + position: absolute; + min-width: 165px; + padding: 16px 18px; + background: rgba(255,255,255,0.95); + border: 1px solid var(--line); + border-radius: 16px; + box-shadow: var(--shadow-sm); +} + +.stat-card strong, +.stat-card span { + display: block; +} + +.stat-card strong { + color: var(--purple-800); + font-size: 1.15rem; +} + +.stat-card span { + color: var(--muted); + font-size: 0.72rem; +} + +.stat-card-one { + left: -2%; + bottom: 11%; +} + +.stat-card-two { + top: 11%; + right: -2%; +} + +.service-strip { + color: white; + background: #254B74; +} + +.service-strip-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); +} + +.service-strip-grid > div { + min-height: 112px; + display: flex; + flex-direction: column; + justify-content: center; + padding: 24px; + border-right: 1px solid rgba(255,255,255,0.11); +} + +.service-strip-grid > div:last-child { + border-right: 0; +} + +.service-strip strong { + font-size: 0.95rem; +} + +.service-strip span { + color: rgba(255,255,255,0.65); + font-size: 0.78rem; +} + +.section { + padding: 105px 0; +} + +.section-muted { + background: var(--surface-soft); +} + +.section-heading { + max-width: 730px; + margin-bottom: 48px; +} + +.section-heading.centered { + margin-inline: auto; + text-align: center; +} + +.section-heading h2 { + margin: 12px 0 14px; + font-size: clamp(2rem, 3.5vw, 3.25rem); + line-height: 1.08; + letter-spacing: -0.045em; +} + +.section-heading p { + margin: 0; + color: var(--muted); + font-size: 1rem; +} + +.card-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 22px; +} + +.feature-card { + min-height: 300px; + padding: 30px; + background: white; + border: 1px solid var(--line); + border-radius: var(--radius-md); + box-shadow: 0 10px 30px rgba(52, 25, 76, 0.035); +} + +.feature-number { + display: inline-flex; + width: 48px; + height: 48px; + align-items: center; + justify-content: center; + color: var(--purple-700); + background: var(--purple-100); + border-radius: 14px; + font-size: 0.82rem; + font-weight: 800; +} + +.feature-card h3 { + margin: 35px 0 12px; + font-size: 1.18rem; +} + +.feature-card p { + margin: 0; + color: var(--muted); + font-size: 0.9rem; +} + +.process-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 1px; + overflow: hidden; + background: var(--line); + border: 1px solid var(--line); + border-radius: var(--radius-md); +} + +.process-step { + min-height: 270px; + padding: 34px; + background: white; +} + +.process-step > span { + width: 42px; + height: 42px; + display: inline-grid; + place-items: center; + color: white; + background: var(--purple-800); + border-radius: 50%; + font-weight: 800; +} + +.process-step h3 { + margin: 48px 0 10px; + font-size: 1.05rem; +} + +.process-step p { + margin: 0; + color: var(--muted); + font-size: 0.88rem; +} + +.order-section { + background: + radial-gradient(circle at 100% 30%, rgba(124, 59, 192, 0.08), transparent 25%), + white; +} + +.order-layout { + display: grid; + grid-template-columns: minmax(0, 1fr) 370px; + align-items: start; + gap: 32px; +} + +.form-panel { + overflow: hidden; + border: 1px solid var(--line); + border-radius: var(--radius-md); + background: white; + box-shadow: var(--shadow-sm); +} + +.form-section { + margin: 0; + padding: 34px; + border: 0; + border-bottom: 1px solid var(--line); +} + +.form-section:last-child { + border-bottom: 0; +} + +.form-section legend { + width: 100%; + display: flex; + align-items: center; + gap: 13px; + margin-bottom: 28px; + padding: 0; + font-size: 1.08rem; + font-weight: 800; +} + +.form-section legend span { + width: 32px; + height: 32px; + display: inline-grid; + place-items: center; + color: white; + background: var(--purple-800); + border-radius: 50%; + font-size: 0.76rem; +} + +.form-grid { + display: grid; + gap: 22px; +} + +.two-columns { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +label { + color: #3e3545; + font-size: 0.84rem; + font-weight: 700; +} + +input, +select, +textarea { + width: 100%; + margin-top: 8px; + color: var(--ink); + background: white; + border: 1px solid #d9cfe0; + border-radius: 12px; + outline: none; + transition: + border-color 160ms ease, + box-shadow 160ms ease; +} + +input, +select { + height: 50px; + padding: 0 14px; +} + +textarea { + resize: vertical; + padding: 14px; +} + +input:focus, +select:focus, +textarea:focus { + border-color: var(--purple-500); + box-shadow: 0 0 0 4px rgba(124, 59, 192, 0.1); +} + +label small { + display: block; + margin-top: 7px; + color: var(--muted); + font-size: 0.71rem; + font-weight: 500; +} + +.option-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 14px; +} + +.option-card { + min-height: 104px; + display: grid; + grid-template-columns: auto 1fr auto; + align-items: start; + gap: 12px; + padding: 18px; + background: white; + border: 1px solid var(--line); + border-radius: 14px; + cursor: pointer; +} + +.option-card:has(input:checked) { + background: var(--purple-50); + border-color: var(--purple-300, #c8afe2); + box-shadow: inset 0 0 0 1px rgba(124,59,192,0.08); +} + +.option-card input { + width: 18px; + height: 18px; + margin: 2px 0 0; + accent-color: var(--purple-700); +} + +.option-card span, +.option-card strong, +.option-card small { + display: block; +} + +.option-card strong { + color: var(--ink); + font-size: 0.85rem; +} + +.option-card small { + margin-top: 4px; + line-height: 1.35; +} + +.option-card b { + color: var(--purple-800); + font-size: 0.73rem; + white-space: nowrap; +} + +.upload-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 16px; +} + +.upload-box { + position: relative; + min-height: 165px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 24px; + text-align: center; + background: var(--purple-50); + border: 1px dashed #b99bd7; + border-radius: 16px; + cursor: pointer; +} + +.upload-box:hover { + background: var(--purple-100); +} + +.upload-box input { + position: absolute; + width: 1px; + height: 1px; + opacity: 0; + pointer-events: none; +} + +.upload-icon { + width: 42px; + height: 42px; + display: grid; + place-items: center; + margin-bottom: 12px; + color: var(--purple-800); + background: white; + border-radius: 50%; + box-shadow: var(--shadow-sm); + font-size: 1.3rem; +} + +.upload-box strong { + font-size: 0.86rem; +} + +.upload-box small { + margin-top: 4px; +} + +.full-width { + display: block; + margin-top: 22px; +} + +.agreement { + display: flex; + align-items: flex-start; + gap: 10px; + margin-top: 22px; + color: var(--muted); + line-height: 1.45; + font-size: 0.77rem; + font-weight: 500; +} + +.agreement input { + flex: 0 0 18px; + width: 18px; + height: 18px; + margin: 2px 0 0; + accent-color: var(--purple-700); +} + +.summary-sticky { + position: sticky; + top: 105px; + padding: 30px; + color: white; + background: + radial-gradient(circle at 100% 0%, rgba(255,255,255,0.1), transparent 28%), + var(--purple-950); + border-radius: var(--radius-md); + box-shadow: var(--shadow-lg); +} + +.summary-sticky .eyebrow { + color: #cfaeea; +} + +.summary-sticky h3 { + margin: 8px 0 24px; + font-size: 1.55rem; +} + +.summary-line { + display: flex; + justify-content: space-between; + gap: 18px; + padding: 12px 0; + color: rgba(255,255,255,0.72); + border-bottom: 1px solid rgba(255,255,255,0.11); + font-size: 0.82rem; +} + +.summary-line strong { + color: white; +} + +.service-summary:empty { + display: none; +} + +.summary-total { + display: flex; + justify-content: space-between; + align-items: flex-end; + gap: 18px; + padding: 24px 0 10px; +} + +.summary-total span { + color: rgba(255,255,255,0.74); + font-size: 0.8rem; +} + +.summary-total strong { + font-size: 2rem; + letter-spacing: -0.04em; +} + +.summary-note, +.secure-note { + color: rgba(255,255,255,0.58); + font-size: 0.69rem; + line-height: 1.5; +} + +.summary-note { + margin: 0 0 22px; +} + +.secure-note { + margin: 14px 0 0; + text-align: center; +} + +.faq-layout { + display: grid; + grid-template-columns: 0.78fr 1.22fr; + gap: 70px; +} + +.faq-list { + display: grid; + gap: 12px; +} + +details { + padding: 22px 24px; + background: white; + border: 1px solid var(--line); + border-radius: 14px; +} + +summary { + cursor: pointer; + font-weight: 750; +} + +details p { + margin: 14px 0 0; + color: var(--muted); + font-size: 0.87rem; +} + + + +#request-order { + background: transparent; +} + + +.cta-card { + min-height: 250px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 40px; + padding: 52px; + color: white; + background: + radial-gradient(circle at 85% 20%, rgba(255,255,255,0.13), transparent 25%), + url("/images/blurple.png"); + background-size: cover; + background-position: center; + background-repeat: no-repeat; + border-radius: var(--radius-lg); +} + +.cta-card .eyebrow { + color: #d8bcea; +} + + + +.cta-card .button-light { + color: white; + background: rgba(255,255,255,0.14); + border: 1px solid rgba(255,255,255,0.42); + box-shadow: none; +} + +.cta-card .button-light:hover { + background: rgba(255,255,255,0.22); +} +.site-footer { + flex: 0 0 auto; + padding: 3px 0 2px; + color: rgba(255,255,255,0.84); + background: #254B74; + border-top: 1px solid rgba(255, 255, 255, 0.12); +} +.footer-shell { + display: flex; + flex-direction: column; + gap: 2px; +} + +.footer-compact-row { + display: grid; + grid-template-columns: auto 1fr auto; + align-items: center; + gap: 6px; +} + +.footer-logo-inline img { + display: block; + width: 56px; + height: auto; +} + +.footer-inline-content { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 3px 8px; + font-size: 0.52rem; + line-height: 1.1; +} + +.footer-inline-content strong, +.footer-inline-content a, +.footer-bottom a { + color: white; +} + +.footer-inline-content a { + text-decoration: none; +} + +.footer-badge-inline img { + display: block; + width: 56px; + height: auto; +} + +.footer-bottom { + display: flex; + justify-content: space-between; + gap: 8px; + padding-top: 2px; + border-top: 1px solid rgba(255,255,255,0.1); + font-size: 0.46rem; + line-height: 1.1; +} + +@media (max-width: 820px) { + .footer-compact-row { + grid-template-columns: 1fr; + gap: 4px; + justify-items: start; + } + + .footer-logo-inline img { + width: 56px; + } + + .footer-bottom { + flex-direction: column; + } +} +dialog { + width: min(calc(100% - 32px), 500px); + padding: 0; + border: 0; + border-radius: 22px; + box-shadow: var(--shadow-lg); +} + +dialog::backdrop { + background: rgba(21, 9, 32, 0.68); + backdrop-filter: blur(4px); +} + +.dialog-content { + position: relative; + padding: 42px; + text-align: center; +} + +.dialog-close { + position: absolute; + top: 12px; + right: 14px; + width: 36px; + height: 36px; + color: var(--muted); + background: transparent; + border: 0; + border-radius: 50%; + cursor: pointer; + font-size: 1.5rem; +} + +.dialog-icon { + width: 62px; + height: 62px; + display: grid; + place-items: center; + margin: 0 auto 18px; + color: white; + background: var(--success); + border-radius: 50%; + font-size: 1.5rem; +} + +.dialog-content h2 { + margin: 0 0 10px; +} + +.dialog-content p { + margin: 0 0 24px; + color: var(--muted); + font-size: 0.9rem; +} + +@media (max-width: 1020px) { + .main-nav { + display: none; + } + + .hero-grid, + .faq-layout { + grid-template-columns: 1fr; + } + + .hero-copy { + max-width: 780px; + padding-left: 0; + } + + .hero-visual { + min-height: 430px; + } + + .card-grid, + .process-grid { + grid-template-columns: repeat(2, 1fr); + } + + .order-layout { + grid-template-columns: 1fr; + } + + .summary-sticky { + position: static; + } + + .footer-grid { + display: grid; + grid-template-columns: 1.6fr repeat(3, 1fr); + gap: 24px; +} + +@media (max-width: 720px) { + .container { + width: min(calc(100% - 26px), var(--container)); + } + + .site-header .button-outline { + display: none; + } + + .hero { + padding: 68px 0 75px; + } + + .hero h1 { + font-size: 2.65rem; + } + + .hero-visual { + min-height: 330px; + } + + .envelope-card { + min-height: 320px; + background-image: url("/images/back.png"); + background-size: contain; + background-position: center center; + background-repeat: no-repeat; + border: 1px solid rgba(255,255,255,0.34); + border-radius: 24px; + box-shadow: var(--shadow-lg); + transform: rotate(-3deg); +} + + + +main.routekit-main { + flex: 1 1 auto; + height: 0; + min-height: 0; + overflow-y: auto; + overflow-x: hidden; +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public-admin/images/KCI ELECTION printing (white).png b/public-admin/images/KCI ELECTION printing (white).png new file mode 100644 index 0000000..bb3a79a Binary files /dev/null and b/public-admin/images/KCI ELECTION printing (white).png differ diff --git a/public-admin/images/KCI ELECTION printing.png b/public-admin/images/KCI ELECTION printing.png new file mode 100644 index 0000000..cb242d6 Binary files /dev/null and b/public-admin/images/KCI ELECTION printing.png differ diff --git a/public-admin/images/KCI geo.png b/public-admin/images/KCI geo.png new file mode 100644 index 0000000..e5c9603 Binary files /dev/null and b/public-admin/images/KCI geo.png differ diff --git a/public-admin/images/KCI_design print mail.png b/public-admin/images/KCI_design print mail.png new file mode 100644 index 0000000..ce5f2f2 Binary files /dev/null and b/public-admin/images/KCI_design print mail.png differ diff --git a/public-admin/images/back.png b/public-admin/images/back.png new file mode 100644 index 0000000..f3cb56c Binary files /dev/null and b/public-admin/images/back.png differ diff --git a/public-admin/images/blurple.png b/public-admin/images/blurple.png new file mode 100644 index 0000000..3e167fa Binary files /dev/null and b/public-admin/images/blurple.png differ diff --git a/public-admin/images/certified-election-mail-service-provider.png b/public-admin/images/certified-election-mail-service-provider.png new file mode 100644 index 0000000..c620ce3 Binary files /dev/null and b/public-admin/images/certified-election-mail-service-provider.png differ diff --git a/public-admin/images/purple-envelope-colors/Aqua.png b/public-admin/images/purple-envelope-colors/Aqua.png new file mode 100644 index 0000000..2f1104e Binary files /dev/null and b/public-admin/images/purple-envelope-colors/Aqua.png differ diff --git a/public-admin/images/purple-envelope-colors/Black.png b/public-admin/images/purple-envelope-colors/Black.png new file mode 100644 index 0000000..2e793c3 Binary files /dev/null and b/public-admin/images/purple-envelope-colors/Black.png differ diff --git a/public-admin/images/purple-envelope-colors/Blue.png b/public-admin/images/purple-envelope-colors/Blue.png new file mode 100644 index 0000000..d18ee89 Binary files /dev/null and b/public-admin/images/purple-envelope-colors/Blue.png differ diff --git a/public-admin/images/purple-envelope-colors/Brown.png b/public-admin/images/purple-envelope-colors/Brown.png new file mode 100644 index 0000000..006bd1a Binary files /dev/null and b/public-admin/images/purple-envelope-colors/Brown.png differ diff --git a/public-admin/images/purple-envelope-colors/BurlyWood.png b/public-admin/images/purple-envelope-colors/BurlyWood.png new file mode 100644 index 0000000..268efc4 Binary files /dev/null and b/public-admin/images/purple-envelope-colors/BurlyWood.png differ diff --git a/public-admin/images/purple-envelope-colors/Chocolate.png b/public-admin/images/purple-envelope-colors/Chocolate.png new file mode 100644 index 0000000..9dfac61 Binary files /dev/null and b/public-admin/images/purple-envelope-colors/Chocolate.png differ diff --git a/public-admin/images/purple-envelope-colors/Clay.png b/public-admin/images/purple-envelope-colors/Clay.png new file mode 100644 index 0000000..3f76fcc Binary files /dev/null and b/public-admin/images/purple-envelope-colors/Clay.png differ diff --git a/public-admin/images/purple-envelope-colors/Coral.png b/public-admin/images/purple-envelope-colors/Coral.png new file mode 100644 index 0000000..38bcf5c Binary files /dev/null and b/public-admin/images/purple-envelope-colors/Coral.png differ diff --git a/public-admin/images/purple-envelope-colors/CornflowerBlue.png b/public-admin/images/purple-envelope-colors/CornflowerBlue.png new file mode 100644 index 0000000..fa739d5 Binary files /dev/null and b/public-admin/images/purple-envelope-colors/CornflowerBlue.png differ diff --git a/public-admin/images/purple-envelope-colors/Crimson.png b/public-admin/images/purple-envelope-colors/Crimson.png new file mode 100644 index 0000000..52c6916 Binary files /dev/null and b/public-admin/images/purple-envelope-colors/Crimson.png differ diff --git a/public-admin/images/purple-envelope-colors/DarkOrange.png b/public-admin/images/purple-envelope-colors/DarkOrange.png new file mode 100644 index 0000000..db37372 Binary files /dev/null and b/public-admin/images/purple-envelope-colors/DarkOrange.png differ diff --git a/public-admin/images/purple-envelope-colors/DeepPink.png b/public-admin/images/purple-envelope-colors/DeepPink.png new file mode 100644 index 0000000..f6d79fe Binary files /dev/null and b/public-admin/images/purple-envelope-colors/DeepPink.png differ diff --git a/public-admin/images/purple-envelope-colors/DeepSkyBlue.png b/public-admin/images/purple-envelope-colors/DeepSkyBlue.png new file mode 100644 index 0000000..68a858a Binary files /dev/null and b/public-admin/images/purple-envelope-colors/DeepSkyBlue.png differ diff --git a/public-admin/images/purple-envelope-colors/DimGray.png b/public-admin/images/purple-envelope-colors/DimGray.png new file mode 100644 index 0000000..58c995e Binary files /dev/null and b/public-admin/images/purple-envelope-colors/DimGray.png differ diff --git a/public-admin/images/purple-envelope-colors/DodgerBlue.png b/public-admin/images/purple-envelope-colors/DodgerBlue.png new file mode 100644 index 0000000..a456c80 Binary files /dev/null and b/public-admin/images/purple-envelope-colors/DodgerBlue.png differ diff --git a/public-admin/images/purple-envelope-colors/FireBrick.png b/public-admin/images/purple-envelope-colors/FireBrick.png new file mode 100644 index 0000000..16ad0e5 Binary files /dev/null and b/public-admin/images/purple-envelope-colors/FireBrick.png differ diff --git a/public-admin/images/purple-envelope-colors/ForestGreen.png b/public-admin/images/purple-envelope-colors/ForestGreen.png new file mode 100644 index 0000000..2002332 Binary files /dev/null and b/public-admin/images/purple-envelope-colors/ForestGreen.png differ diff --git a/public-admin/images/purple-envelope-colors/Fuchsia.png b/public-admin/images/purple-envelope-colors/Fuchsia.png new file mode 100644 index 0000000..a10be9e Binary files /dev/null and b/public-admin/images/purple-envelope-colors/Fuchsia.png differ diff --git a/public-admin/images/purple-envelope-colors/Gold.png b/public-admin/images/purple-envelope-colors/Gold.png new file mode 100644 index 0000000..ce71191 Binary files /dev/null and b/public-admin/images/purple-envelope-colors/Gold.png differ diff --git a/public-admin/images/purple-envelope-colors/Goldenrod.png b/public-admin/images/purple-envelope-colors/Goldenrod.png new file mode 100644 index 0000000..e8e85bf Binary files /dev/null and b/public-admin/images/purple-envelope-colors/Goldenrod.png differ diff --git a/public-admin/images/purple-envelope-colors/Green.png b/public-admin/images/purple-envelope-colors/Green.png new file mode 100644 index 0000000..8ca0145 Binary files /dev/null and b/public-admin/images/purple-envelope-colors/Green.png differ diff --git a/public-admin/images/purple-envelope-colors/HotPink.png b/public-admin/images/purple-envelope-colors/HotPink.png new file mode 100644 index 0000000..42557bf Binary files /dev/null and b/public-admin/images/purple-envelope-colors/HotPink.png differ diff --git a/public-admin/images/purple-envelope-colors/Indigo.png b/public-admin/images/purple-envelope-colors/Indigo.png new file mode 100644 index 0000000..6447556 Binary files /dev/null and b/public-admin/images/purple-envelope-colors/Indigo.png differ diff --git a/public-admin/images/purple-envelope-colors/LightSeaGreen.png b/public-admin/images/purple-envelope-colors/LightSeaGreen.png new file mode 100644 index 0000000..6dfac24 Binary files /dev/null and b/public-admin/images/purple-envelope-colors/LightSeaGreen.png differ diff --git a/public-admin/images/purple-envelope-colors/Lime.png b/public-admin/images/purple-envelope-colors/Lime.png new file mode 100644 index 0000000..1b16f73 Binary files /dev/null and b/public-admin/images/purple-envelope-colors/Lime.png differ diff --git a/public-admin/images/purple-envelope-colors/LimeGreen.png b/public-admin/images/purple-envelope-colors/LimeGreen.png new file mode 100644 index 0000000..d16db86 Binary files /dev/null and b/public-admin/images/purple-envelope-colors/LimeGreen.png differ diff --git a/public-admin/images/purple-envelope-colors/LtGreen.png b/public-admin/images/purple-envelope-colors/LtGreen.png new file mode 100644 index 0000000..58578b0 Binary files /dev/null and b/public-admin/images/purple-envelope-colors/LtGreen.png differ diff --git a/public-admin/images/purple-envelope-colors/MediumOrchid.png b/public-admin/images/purple-envelope-colors/MediumOrchid.png new file mode 100644 index 0000000..059bacb Binary files /dev/null and b/public-admin/images/purple-envelope-colors/MediumOrchid.png differ diff --git a/public-admin/images/purple-envelope-colors/MediumPurple.png b/public-admin/images/purple-envelope-colors/MediumPurple.png new file mode 100644 index 0000000..01c2abe Binary files /dev/null and b/public-admin/images/purple-envelope-colors/MediumPurple.png differ diff --git a/public-admin/images/purple-envelope-colors/MediumTurquoise.png b/public-admin/images/purple-envelope-colors/MediumTurquoise.png new file mode 100644 index 0000000..3d65ee1 Binary files /dev/null and b/public-admin/images/purple-envelope-colors/MediumTurquoise.png differ diff --git a/public-admin/images/purple-envelope-colors/MintGreen.png b/public-admin/images/purple-envelope-colors/MintGreen.png new file mode 100644 index 0000000..af44a46 Binary files /dev/null and b/public-admin/images/purple-envelope-colors/MintGreen.png differ diff --git a/public-admin/images/purple-envelope-colors/Navy.png b/public-admin/images/purple-envelope-colors/Navy.png new file mode 100644 index 0000000..d8b645e Binary files /dev/null and b/public-admin/images/purple-envelope-colors/Navy.png differ diff --git a/public-admin/images/purple-envelope-colors/Olive.png b/public-admin/images/purple-envelope-colors/Olive.png new file mode 100644 index 0000000..decd5c8 Binary files /dev/null and b/public-admin/images/purple-envelope-colors/Olive.png differ diff --git a/public-admin/images/purple-envelope-colors/OliveDrab.png b/public-admin/images/purple-envelope-colors/OliveDrab.png new file mode 100644 index 0000000..9fe6c9f Binary files /dev/null and b/public-admin/images/purple-envelope-colors/OliveDrab.png differ diff --git a/public-admin/images/purple-envelope-colors/Orange.png b/public-admin/images/purple-envelope-colors/Orange.png new file mode 100644 index 0000000..d361885 Binary files /dev/null and b/public-admin/images/purple-envelope-colors/Orange.png differ diff --git a/public-admin/images/purple-envelope-colors/OrangeRed.png b/public-admin/images/purple-envelope-colors/OrangeRed.png new file mode 100644 index 0000000..de3c15b Binary files /dev/null and b/public-admin/images/purple-envelope-colors/OrangeRed.png differ diff --git a/public-admin/images/purple-envelope-colors/Orchid.png b/public-admin/images/purple-envelope-colors/Orchid.png new file mode 100644 index 0000000..00570c5 Binary files /dev/null and b/public-admin/images/purple-envelope-colors/Orchid.png differ diff --git a/public-admin/images/purple-envelope-colors/PastelBlue.png b/public-admin/images/purple-envelope-colors/PastelBlue.png new file mode 100644 index 0000000..a9c4093 Binary files /dev/null and b/public-admin/images/purple-envelope-colors/PastelBlue.png differ diff --git a/public-admin/images/purple-envelope-colors/PastelPink.png b/public-admin/images/purple-envelope-colors/PastelPink.png new file mode 100644 index 0000000..1a17f03 Binary files /dev/null and b/public-admin/images/purple-envelope-colors/PastelPink.png differ diff --git a/public-admin/images/purple-envelope-colors/PastelPurple.png b/public-admin/images/purple-envelope-colors/PastelPurple.png new file mode 100644 index 0000000..4981d92 Binary files /dev/null and b/public-admin/images/purple-envelope-colors/PastelPurple.png differ diff --git a/public-admin/images/purple-envelope-colors/PastelYellow.png b/public-admin/images/purple-envelope-colors/PastelYellow.png new file mode 100644 index 0000000..f2a75ed Binary files /dev/null and b/public-admin/images/purple-envelope-colors/PastelYellow.png differ diff --git a/public-admin/images/purple-envelope-colors/Peru.png b/public-admin/images/purple-envelope-colors/Peru.png new file mode 100644 index 0000000..ed015f0 Binary files /dev/null and b/public-admin/images/purple-envelope-colors/Peru.png differ diff --git a/public-admin/images/purple-envelope-colors/Pink.png b/public-admin/images/purple-envelope-colors/Pink.png new file mode 100644 index 0000000..2803322 Binary files /dev/null and b/public-admin/images/purple-envelope-colors/Pink.png differ diff --git a/public-admin/images/purple-envelope-colors/Purple.png b/public-admin/images/purple-envelope-colors/Purple.png new file mode 100644 index 0000000..350c8e4 Binary files /dev/null and b/public-admin/images/purple-envelope-colors/Purple.png differ diff --git a/public-admin/images/purple-envelope-colors/Red.png b/public-admin/images/purple-envelope-colors/Red.png new file mode 100644 index 0000000..92e54e3 Binary files /dev/null and b/public-admin/images/purple-envelope-colors/Red.png differ diff --git a/public-admin/images/purple-envelope-colors/RoyalBlue.png b/public-admin/images/purple-envelope-colors/RoyalBlue.png new file mode 100644 index 0000000..e547d26 Binary files /dev/null and b/public-admin/images/purple-envelope-colors/RoyalBlue.png differ diff --git a/public-admin/images/purple-envelope-colors/SaddleBrown.png b/public-admin/images/purple-envelope-colors/SaddleBrown.png new file mode 100644 index 0000000..90a42d3 Binary files /dev/null and b/public-admin/images/purple-envelope-colors/SaddleBrown.png differ diff --git a/public-admin/images/purple-envelope-colors/SeaGreen.png b/public-admin/images/purple-envelope-colors/SeaGreen.png new file mode 100644 index 0000000..1e3a388 Binary files /dev/null and b/public-admin/images/purple-envelope-colors/SeaGreen.png differ diff --git a/public-admin/images/purple-envelope-colors/Sienna.png b/public-admin/images/purple-envelope-colors/Sienna.png new file mode 100644 index 0000000..ce304b3 Binary files /dev/null and b/public-admin/images/purple-envelope-colors/Sienna.png differ diff --git a/public-admin/images/purple-envelope-colors/Silver.png b/public-admin/images/purple-envelope-colors/Silver.png new file mode 100644 index 0000000..7dc50cc Binary files /dev/null and b/public-admin/images/purple-envelope-colors/Silver.png differ diff --git a/public-admin/images/purple-envelope-colors/SkyBlue.png b/public-admin/images/purple-envelope-colors/SkyBlue.png new file mode 100644 index 0000000..a24abae Binary files /dev/null and b/public-admin/images/purple-envelope-colors/SkyBlue.png differ diff --git a/public-admin/images/purple-envelope-colors/SteelBlue.png b/public-admin/images/purple-envelope-colors/SteelBlue.png new file mode 100644 index 0000000..319391f Binary files /dev/null and b/public-admin/images/purple-envelope-colors/SteelBlue.png differ diff --git a/public-admin/images/purple-envelope-colors/Tan.png b/public-admin/images/purple-envelope-colors/Tan.png new file mode 100644 index 0000000..1ad76ad Binary files /dev/null and b/public-admin/images/purple-envelope-colors/Tan.png differ diff --git a/public-admin/images/purple-envelope-colors/Teal.png b/public-admin/images/purple-envelope-colors/Teal.png new file mode 100644 index 0000000..c4eb6eb Binary files /dev/null and b/public-admin/images/purple-envelope-colors/Teal.png differ diff --git a/public-admin/images/purple-envelope-colors/Tomato.png b/public-admin/images/purple-envelope-colors/Tomato.png new file mode 100644 index 0000000..cbd49e0 Binary files /dev/null and b/public-admin/images/purple-envelope-colors/Tomato.png differ diff --git a/public-admin/images/purple-envelope-colors/Turquoise.png b/public-admin/images/purple-envelope-colors/Turquoise.png new file mode 100644 index 0000000..954ec62 Binary files /dev/null and b/public-admin/images/purple-envelope-colors/Turquoise.png differ diff --git a/public-admin/images/purple-envelope-colors/Violet.png b/public-admin/images/purple-envelope-colors/Violet.png new file mode 100644 index 0000000..cbe23bf Binary files /dev/null and b/public-admin/images/purple-envelope-colors/Violet.png differ diff --git a/public-admin/images/purple-envelope-colors/White.png b/public-admin/images/purple-envelope-colors/White.png new file mode 100644 index 0000000..702eab1 Binary files /dev/null and b/public-admin/images/purple-envelope-colors/White.png differ diff --git a/public-admin/images/purple-envelope-colors/Yellow.png b/public-admin/images/purple-envelope-colors/Yellow.png new file mode 100644 index 0000000..3591316 Binary files /dev/null and b/public-admin/images/purple-envelope-colors/Yellow.png differ diff --git a/public-admin/images/swaggy backer.png b/public-admin/images/swaggy backer.png new file mode 100644 index 0000000..b131b1b Binary files /dev/null and b/public-admin/images/swaggy backer.png differ diff --git a/public-admin/images/swaggy geo.png b/public-admin/images/swaggy geo.png new file mode 100644 index 0000000..75b63d7 Binary files /dev/null and b/public-admin/images/swaggy geo.png differ diff --git a/public-admin/web.config b/public-admin/web.config new file mode 100644 index 0000000..8b171e9 --- /dev/null +++ b/public-admin/web.config @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public-admin/web.config.production.template b/public-admin/web.config.production.template new file mode 100644 index 0000000..539b9dd --- /dev/null +++ b/public-admin/web.config.production.template @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/run_site.cmd b/run_site.cmd index 65d049b..a19a71c 100644 --- a/run_site.cmd +++ b/run_site.cmd @@ -1,4 +1,11 @@ @echo off setlocal set "ASPC_STARTER_ROOT=%~dp0" -"C:\Program Files (x86)\IIS Express\iisexpress.exe" /config:"%~dp0applicationhost.config" \ No newline at end of file +set "IISEXPRESS=C:\Program Files (x86)\IIS Express\iisexpress.exe" +set "CONFIG=%~dp0applicationhost.config" + +rem IIS Express only starts one site per process when /site is specified, so the +rem public-facing site is launched in its own window and the admin site runs in +rem this one. Close both windows to stop both sites. +start "Public Site (port 8080)" "%IISEXPRESS%" /config:"%CONFIG%" /site:"Development Web Site" +"%IISEXPRESS%" /config:"%CONFIG%" /site:"Admin Web Site" diff --git a/scripts/build-release.ps1 b/scripts/build-release.ps1 index c65f054..92df434 100644 --- a/scripts/build-release.ps1 +++ b/scripts/build-release.ps1 @@ -30,7 +30,7 @@ $ErrorActionPreference = 'Stop' # the migration runner (+ its generator siblings, harmless to ship alongside it). Only # db\migrations is kept under db - never db\webdata.accdb, even if something ever put a # stray copy there. -$KeepTopLevel = @('app', 'core', 'public', 'scripts', 'db') +$KeepTopLevel = @('app', 'core', 'public', 'public-admin', 'scripts', 'db') $KeepUnderDb = @('migrations') $repoRoot = Split-Path $PSScriptRoot -Parent @@ -82,6 +82,16 @@ try { Copy-Item $templatePath $liveConfig -Force Remove-Item (Join-Path $OutDir 'public\web.config.production.template') -ErrorAction SilentlyContinue + $adminTemplatePath = Join-Path $repoRoot 'public-admin\web.config.production.template' + if(Test-Path (Join-Path $OutDir 'public-admin')){ + if(Test-Path $adminTemplatePath){ + $adminLiveConfig = Join-Path $OutDir 'public-admin\web.config' + Copy-Item $adminTemplatePath $adminLiveConfig -Force + Remove-Item (Join-Path $OutDir 'public-admin\web.config.production.template') -ErrorAction SilentlyContinue + Write-Host " public-admin\web.config swapped in from web.config.production.template" + } + } + Write-Host "Release built at $OutDir" Write-Host " public\web.config swapped in from web.config.production.template" } finally { diff --git a/scripts/deploy-iis-remote-apply.ps1 b/scripts/deploy-iis-remote-apply.ps1 index 69be89d..a93bd3d 100644 --- a/scripts/deploy-iis-remote-apply.ps1 +++ b/scripts/deploy-iis-remote-apply.ps1 @@ -17,6 +17,8 @@ param( [Parameter(Mandatory = $true)][string]$SiteName, [Parameter(Mandatory = $true)][string]$AppPool, [Parameter(Mandatory = $true)][string]$DbPath, + [string]$AdminSiteName = '', + [string]$AdminAppPool = '', [switch]$RunMigrations = $true ) @@ -69,6 +71,23 @@ Set-ItemProperty ('IIS:\Sites\' + $SiteName) -Name applicationPool -Value $AppPo # dev machine, which needs the 32-bit one - so the app pool stays 64-bit here. Set-ItemProperty ('IIS:\AppPools\' + $AppPool) -Name enable32BitAppOnWin64 -Value $false +# --- Admin site --- +$adminPublicDir = Join-Path $RemoteDir 'public-admin' +if(![string]::IsNullOrWhiteSpace($AdminSiteName) -and (Test-Path $adminPublicDir)){ + if(!(Get-Website -Name $AdminSiteName -ErrorAction SilentlyContinue)){ + throw "IIS admin site '$AdminSiteName' does not exist yet. Create the site and app pool once manually before running this deploy." + } + Write-Host "Configuring admin site $AdminSiteName" + Stop-Website -Name $AdminSiteName -ErrorAction SilentlyContinue + if(![string]::IsNullOrWhiteSpace($AdminAppPool)){ + Stop-WebAppPool -Name $AdminAppPool -ErrorAction SilentlyContinue + Set-ItemProperty ('IIS:\Sites\' + $AdminSiteName) -Name applicationPool -Value $AdminAppPool + Set-ItemProperty ('IIS:\AppPools\' + $AdminAppPool) -Name enable32BitAppOnWin64 -Value $false + icacls $dataDir /grant ("IIS AppPool\" + $AdminAppPool + ":(OI)(CI)(M)") /T | Out-Null + } + Set-ItemProperty ('IIS:\Sites\' + $AdminSiteName) -Name physicalPath -Value $adminPublicDir +} + if($RunMigrations){ Write-Host 'Running pending migrations' Push-Location $RemoteDir @@ -88,4 +107,15 @@ if((Get-WebAppPoolState -Name $AppPool).Value -eq 'Started'){ } Start-Website $SiteName +if(![string]::IsNullOrWhiteSpace($AdminSiteName)){ + if(![string]::IsNullOrWhiteSpace($AdminAppPool)){ + if((Get-WebAppPoolState -Name $AdminAppPool).Value -eq 'Started'){ + Restart-WebAppPool -Name $AdminAppPool + } else { + Start-WebAppPool -Name $AdminAppPool + } + } + Start-Website $AdminSiteName +} + Write-Host 'Remote apply complete.' diff --git a/scripts/deploy-iis.ps1 b/scripts/deploy-iis.ps1 index 5c009ac..174fab2 100644 --- a/scripts/deploy-iis.ps1 +++ b/scripts/deploy-iis.ps1 @@ -26,8 +26,11 @@ param( [string]$RemoteDir = 'C:\inetpub\wwwroot\Purple_Envelop_Order_Site', [string]$SiteName = 'PurpleEnvelopes', [string]$AppPool = 'PurpleEnvelopes', + [string]$AdminSiteName = '', + [string]$AdminAppPool = '', [string]$DbPath = 'C:\inetpub\data\webdata.accdb', [string]$BaseUrl = 'https://pe.kentcommunications.com/', + [string]$AdminBaseUrl = '', [switch]$RunMigrations = $true, [string]$SshExe = 'ssh', [string]$ScpExe = 'scp' @@ -101,6 +104,12 @@ $remoteCommandParts = @( '-AppPool', ('"' + $AppPool + '"'), '-DbPath', ('"' + $DbPath + '"') ) +if(![string]::IsNullOrWhiteSpace($AdminSiteName)){ + $remoteCommandParts += @('-AdminSiteName', ('"' + $AdminSiteName + '"')) +} +if(![string]::IsNullOrWhiteSpace($AdminAppPool)){ + $remoteCommandParts += @('-AdminAppPool', ('"' + $AdminAppPool + '"')) +} if($RunMigrations){ $remoteCommandParts += '-RunMigrations' } Write-Host "Applying release on $RemoteTarget" @@ -147,4 +156,36 @@ if($failed){ throw 'Smoke test failed - see above' } +# --- 6. Admin site smoke test --- +if(![string]::IsNullOrWhiteSpace($AdminBaseUrl)){ + Write-Host 'Smoke testing admin site...' + $adminChecks = @( + @{ Path = '/'; Expect = 200 } + ) + foreach($check in $adminChecks){ + $url = $AdminBaseUrl.TrimEnd('/') + $check.Path + try { + $response = Invoke-WebRequest -UseBasicParsing -Uri $url -TimeoutSec 30 + $status = [int]$response.StatusCode + } catch { + if($_.Exception.Response){ + $status = [int]$_.Exception.Response.StatusCode + } else { + Write-Host ("FAIL (admin) " + $check.Path + ' -> request failed: ' + $_.Exception.Message) + $failed = $true + continue + } + } + if($status -eq $check.Expect){ + Write-Host ("OK (admin) " + $check.Path + ' -> ' + $status) + } else { + Write-Host ("FAIL (admin) " + $check.Path + ' -> ' + $status + ' (expected ' + $check.Expect + ')') + $failed = $true + } + } + if($failed){ + throw 'Admin smoke test failed - see above' + } +} + Write-Host 'Deploy complete.'