Its been a while

This commit is contained in:
Jason Jordan
2026-07-23 10:38:20 -04:00
parent 77f294c972
commit 8b3bb209b3
50 changed files with 4147 additions and 1452 deletions
+6 -1
View File
@@ -7,8 +7,13 @@ WORKDIR /usr/src/app
# Copy package.json and package-lock.json (or yarn.lock/pnpm-lock.yaml)
COPY package*.json ./
ARG NEXT_SERVER_ACTIONS_ENCRYPTION_KEY
ENV NEXT_SERVER_ACTIONS_ENCRYPTION_KEY=$NEXT_SERVER_ACTIONS_ENCRYPTION_KEY
# Install dependencies
RUN npm install --legacy-peer-deps
RUN npm install
# RUN build
# Copy the rest of your application code
COPY . .
+47
View File
@@ -0,0 +1,47 @@
<div
class="min-h-full flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8"
>
<div class="max-w-md w-full space-y-8">
<div>
<div class="flex flex-col text-8xl font-bold text-[#2A4B6F] space-y-30">
<div>
<span class="[text-shadow:0_0_20px_#fff,0_0_50px_#0090D9,0_0_80px_#0ff] text-[#04153E]">B</span>acl<span class="relative inline-block">i<span class="absolute top-0 right-0 h-5 w-5 rounded-full bg-[#CD7F32] translate-y-[12px] -translate-x-[4px]"></span></span>ght
</div>
</div>
<h2 class="mt-10 text-center text-2xl font-extrabold text-[#CD7F32]">
Britton Ancillary Center
</h2>
</div>
<%= form_for(resource, as: resource_name, url: session_path(resource_name), html: { data: { turbo: false} }) do |f| %>
<div class="mt-8 space-y-6">
<input type="hidden" name="remember" value="true" />
<div class="rounded-md shadow-sm -space-y-px">
<div>
<%= f.label :email, class: "sr-only", for: "email-address" %>
<%= f.email_field :email, id: "email-address", autofocus: true, autocomplete: "email", placeholder: "Email address", class: "appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-bluetang text-deepcove rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm" %>
</div>
<div>
<%= f.label :password, for: "password", class: "sr-only" %>
<%= f.password_field :password, id: "password", autocomplete: "current-password", placeholder: "Password", class: "appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-bluetang text-deepcove rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm" %>
</div>
</div>
<% if devise_mapping.rememberable? %>
<div class="flex items-center justify-between">
<div class="field flex items-center">
<%= f.check_box :remember_me, class: "h-4 w-4 text-[#CD7F32] focus:ring-indigo-500 border-[#CD7F32] rounded" %>
<%= f.label :remember_me, class: "ml-2 block font-semibold text-md text-[#CD7F32]" %>
</div>
<div class="text-md">
<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %>
<%= link_to "Forgot your password?", new_password_path(resource_name), class: "font-semibold text-atmosphere hover:text-bluemana" %>
<% end %>
</div>
</div>
<% end %>
<div>
<%= f.submit "Sign in", { class: "relative w-full flex justify-center py-2 px-4 border-4 font-bold rounded-lg text-platinum" } %>
</div>
</div>
<% end %>
</div>
</div>
+10 -1
View File
@@ -5,6 +5,15 @@ services:
dockerfile: Dockerfile.dev
ports:
- "3001:3001"
environment:
- BE_API_SERVER_HOST=web_api
- BE_API_SERVER_PORT=3005
volumes:
- .:/usr/src/app # Mount the current directory into the container for live reloading
- /usr/src/app/node_modules # Isolate node_modules to prevent host-container conflicts
- /usr/src/app/node_modules # Isolate node_modules to prevent host-container conflicts
networks:
- britton_network
networks:
britton_network:
external: true
+13 -1
View File
@@ -1,7 +1,19 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
// async rewrites() {
// return [
// {
// source: '/api-backend/:path*',
// destination: `http://web_api:3005`, // The external api URL
// },
// ];
// },
outputFileTracingIncludes: {
'/api/**/*': ['./node_modules/jose/**/*'],
},
allowedDevOrigins: [`${process.env.BE_API_SERVER_HOST}:${process.env.BE_API_SERVER_PORT}`]
};
export default nextConfig;
+1277 -1134
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -12,9 +12,11 @@
"@apollo/client": "^3.13.9",
"@apollo/experimental-nextjs-app-support": "^0.12.3",
"graphql": "^16.11.0",
"jose": "^6.2.3",
"next": "15.3.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-idle-timer": "^5.7.3",
"react-markdown": "^10.1.0",
"react-places-autocomplete": "^7.3.0"
},
+76
View File
@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 184.69333 172.64"
height="172.64"
width="184.69333"
xml:space="preserve"
id="svg2"
version="1.1"
sodipodi:docname="Cigna_deepcove.svg"
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/"><sodipodi:namedview
id="namedview27"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="3.6376274"
inkscape:cx="48.520637"
inkscape:cy="86.32"
inkscape:window-width="1512"
inkscape:window-height="873"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="g12" />
<metadata
id="metadata8">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>
image/svg+xml
</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage">
</dc:type>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs6">
</defs>
<g
transform="matrix(1.3333333,0,0,-1.3333333,0,172.64)"
id="g10">
<g
transform="scale(0.1)"
id="g12">
<path
id="path14"
style="fill:#04153e;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 973.246,741.891 c 0,-8.696 -11.594,-20.286 -23.183,-37.672 -46.372,-57.961 -150.7,-159.391 -150.7,-414.418 0,-130.41 20.289,-185.469 20.289,-246.3322 C 819.652,14.4883 813.855,0 799.363,0 c -31.875,0 -89.836,139.109 -89.836,304.293 0,350.656 220.246,446.293 255.024,446.293 5.797,0 8.695,-2.898 8.695,-8.695 m -553.519,8.695 c 34.777,0 252.125,-95.637 252.125,-443.395 C 671.852,142 616.793,2.89844 582.016,2.89844 c -14.493,0 -20.286,14.49216 -20.286,43.47266 0,60.8589 20.286,115.9179 20.286,246.3279 0,252.129 -104.328,356.457 -150.696,414.414 -14.492,17.391 -23.183,28.981 -23.183,37.676 2.894,2.899 5.793,5.797 11.59,5.797">
</path>
<path
id="path16"
style="fill:#04153e;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 611.109,681.008 c 0,-46.367 34.774,-81.145 81.145,-81.145 43.469,0 78.242,34.778 78.242,81.145 0,46.367 -34.773,81.144 -81.141,81.144 -40.574,0 -78.246,-34.777 -78.246,-81.144">
</path>
<path
id="path18"
style="fill:#04153e;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 692.621,1060.09 c -31.875,14.49 -63.754,43.47 -63.754,101.43 0,55.07 26.082,110.12 63.754,133.31 37.676,-23.19 63.758,-78.24 63.758,-133.31 0,-57.96 -31.879,-84.04 -63.758,-101.43 M 1376.55,164.609 c -113.02,69.551 -286.9,142 -539.027,165.188 0,-2.899 -2.898,-20.289 -2.898,-40.578 0,-8.688 0,-20.278 2.898,-20.278 246.327,-14.492 420.207,-66.66 539.027,-110.132 2.9,0 5.8,-2.887 5.8,-2.887 0,0 2.9,0 2.9,2.887 -2.9,2.902 -5.8,2.902 -8.7,5.8 M 823.031,1100.66 c -20.285,17.39 -34.773,43.47 -23.183,84.05 11.593,37.67 40.574,69.55 72.453,78.24 20.285,-26.08 28.976,-66.65 17.387,-107.22 -14.493,-37.68 -43.469,-49.27 -66.657,-55.07 M 1005.61,947.07 c -5.797,23.184 -5.797,52.164 26.08,81.14 28.98,26.09 69.55,34.78 98.53,26.09 5.8,-31.88 -11.59,-69.558 -40.57,-92.741 -31.88,-28.977 -60.86,-23.184 -84.04,-14.489 m 5.79,-231.84 c 8.7,23.184 23.19,46.368 63.76,52.165 37.67,2.898 75.35,-8.696 95.63,-34.778 -14.49,-28.98 -46.37,-49.265 -84.04,-52.164 -40.57,-5.797 -60.86,14.488 -75.35,34.777 m -66.65,-84.042 c 14.488,17.386 34.773,34.777 69.55,26.082 31.88,-5.797 60.86,-28.981 72.45,-55.063 -17.39,-20.285 -52.16,-28.98 -86.937,-23.184 -34.778,8.696 -49.266,31.879 -55.063,52.165 m -199.965,220.25 c -23.183,20.285 -43.469,52.164 -28.976,101.429 14.488,46.367 49.265,84.043 86.937,92.733 26.082,-28.98 34.777,-81.143 20.285,-127.51 -14.488,-46.367 -49.265,-63.754 -78.246,-66.652 m 86.942,-52.165 c -8.696,26.082 -5.797,60.856 28.98,89.836 31.879,28.981 78.246,40.575 113.023,28.981 5.793,-34.777 -14.492,-78.246 -46.371,-104.328 -37.671,-28.981 -69.55,-26.082 -95.632,-14.489 m 57.961,188.368 c -14.493,26.079 -26.083,63.759 2.898,107.229 26.082,40.57 72.449,69.55 113.024,66.65 17.38,-34.77 11.59,-89.84 -17.391,-130.41 -28.981,-43.469 -66.653,-49.262 -98.531,-43.469 M 1008.5,825.355 c 0,28.981 11.6,63.754 55.07,84.04 43.47,17.39 89.83,14.492 121.71,-5.793 -5.79,-34.778 -34.77,-72.454 -78.24,-92.739 -43.47,-20.285 -75.35,-5.797 -98.54,14.492 M 0,158.809 v 0 c 2.89844,-2.887 5.79688,0 8.69531,0 118.81669,43.472 292.69569,95.64 539.02769,110.132 0,2.899 2.898,14.489 2.898,20.278 0,17.39 -2.898,37.679 -2.898,40.578 C 295.598,306.609 124.613,234.16 8.69531,164.609 2.89844,161.711 0,161.711 0,158.809 m 495.559,999.821 c -11.594,37.67 -5.797,81.14 17.386,107.22 31.879,-8.69 60.86,-40.57 72.45,-78.24 11.593,-40.58 -2.895,-66.66 -23.184,-84.05 -26.082,2.9 -55.063,14.49 -66.652,55.07 M 292.699,958.664 c -28.98,26.078 -46.371,63.756 -40.574,92.736 28.98,8.69 69.555,0 98.531,-26.08 31.879,-26.086 31.879,-55.066 26.082,-81.148 -23.183,-5.797 -52.164,-11.59 -84.039,14.492 m 5.793,-278.211 c -37.672,2.899 -72.449,26.082 -86.937,52.164 20.285,23.184 57.961,37.676 95.633,34.778 40.574,-2.899 55.062,-28.981 63.757,-52.165 -11.593,-20.289 -34.777,-40.574 -72.453,-34.777 m 86.942,-98.531 c -31.879,-5.797 -66.653,2.898 -86.942,23.183 8.696,26.083 37.676,46.368 72.453,52.165 34.774,8.695 55.063,-8.696 66.653,-26.082 -5.797,-20.286 -17.387,-43.469 -52.164,-49.266 M 559.313,918.09 c -14.489,46.367 -5.793,98.53 20.289,127.51 37.671,-11.59 72.449,-49.264 86.941,-92.733 14.488,-49.265 -5.797,-81.144 -26.082,-101.429 -31.879,2.898 -66.656,20.285 -81.148,66.652 M 457.883,813.762 c -31.875,28.98 -52.164,69.551 -46.367,104.328 34.777,11.594 78.246,0 113.023,-28.981 34.774,-28.98 34.774,-63.754 28.981,-89.836 -28.981,-11.593 -60.86,-14.492 -95.637,14.489 m -60.856,217.348 c -26.082,40.57 -31.879,92.74 -17.39,130.41 40.574,0 84.043,-26.08 113.023,-66.65 28.981,-43.47 17.387,-78.25 0,-107.229 -31.879,-5.793 -69.551,0 -95.633,43.469 M 275.309,810.863 c -40.571,20.285 -72.45,57.961 -78.246,92.739 31.878,20.285 81.144,23.183 121.718,2.898 43.469,-20.289 55.063,-52.164 55.063,-84.043 -23.188,-17.391 -55.063,-31.879 -98.535,-11.594">
</path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.3 KiB

+62
View File
@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 474 159"
width="474"
height="159"
version="1.1"
id="svg4971"
sodipodi:docname="Fairosrx.svg"
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs4975" />
<sodipodi:namedview
id="namedview4973"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="1.4842767"
inkscape:cx="238.16314"
inkscape:cy="79.500002"
inkscape:window-width="1512"
inkscape:window-height="873"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="svg4971" />
<path
d="M444.4 15.25C441.68 23.37 431.19 17.11 426.32 15.4C413.29 10.83 390.72 13.95 390.41 31.25C390.21 42.22 399.98 44.81 408.65 47.16C425.62 51.77 450.1 53.21 450.22 76.25C450.24 79.45 449.92 82.58 448.78 85.58C441.34 105.13 410.99 104.68 394.86 98.85C388.59 96.59 373.77 88.72 381.76 80.25C384.76 80.59 390.94 87.06 394.49 88.73C404.72 93.56 416.25 94.72 427.27 92.09C432.87 90.76 439.09 87.04 439.95 80.75C441.63 68.4 434.34 64.02 423.92 60.78C410.29 56.54 383.56 55.46 381.03 37.25C377.34 10.64 402.77 2.26 424.43 5.76C430.08 6.68 440.84 10.49 444.4 15.25ZM64.75 5.56C65.88 12.91 57.87 14.55 52.25 14.55C42.58 14.55 32.92 14.55 23.25 14.55C19.81 14.55 14.24 13.51 11.27 15.25C11.27 26.37 11.27 37.49 11.27 48.61C25.1 48.61 38.92 48.61 52.75 48.61C53.75 54.08 48.57 57.09 43.75 57.51C36.2 58.16 28.33 57.55 20.75 57.55C17.94 57.55 13.62 56.72 11.26 58.25C11.26 72.69 11.26 87.13 11.26 101.57C8.1 101.57 4.95 101.57 1.79 101.57C1.79 69.57 1.79 37.56 1.79 5.56C22.78 5.56 43.76 5.56 64.75 5.56ZM106.25 5.63C119.66 37.6 133.08 69.57 146.49 101.54C143.24 101.54 140 101.54 136.75 101.54C125.42 74.3 114.08 47.06 102.75 19.81C96.79 33.79 90.83 47.77 84.88 61.75C91.72 63.14 99.76 61.98 106.75 61.97C109.48 61.97 113.38 61.27 115.75 62.59C115.57 67.59 111.49 70.64 106.75 71.02C98.53 71.68 89.42 69.49 81.38 71.25C77 81.35 72.63 91.46 68.25 101.56C65.07 101.56 61.89 101.56 58.71 101.56C72.05 69.58 85.4 37.6 98.75 5.63C101.25 5.63 103.75 5.63 106.25 5.63ZM167.05 5.86C167.05 37.76 167.05 69.66 167.05 101.55C163.93 101.55 160.8 101.55 157.68 101.55C155.91 93.75 157.27 84.27 157.27 76.25C157.27 59.58 157.27 42.92 157.27 26.25C157.27 18.99 155.56 4.43 167.05 5.86ZM236.54 65.25C243.95 76.65 251.35 88.05 258.75 99.44C262.92 93.09 267.08 86.75 271.25 80.4C273.13 83.01 275.01 85.63 276.89 88.25C272.66 95.08 268.43 101.92 264.21 108.75C273.54 124.08 282.88 139.42 292.22 154.75C285.64 158.84 280.93 153.78 277.65 148.6C273.2 141.56 268.71 134.52 264.32 127.43C262.53 124.53 260.84 120.55 258.25 118.39C252.08 127.99 245.92 137.59 239.75 147.18C236.25 147.18 232.75 147.18 229.25 147.18C237.04 134.37 244.84 121.56 252.63 108.75C243.69 94.92 234.76 81.08 225.82 67.25C218.58 66.86 203.83 69.73 204.25 58.14C207.43 56.75 212.28 57.61 215.75 57.61C228.29 57.61 241.7 58.43 246 43.76C246.79 41.05 247.37 38.38 247.25 35.51C246.88 26.95 241.78 17.87 233.12 15.21C227.81 13.58 221.23 14.55 215.75 14.55C209.93 14.55 203.85 14.06 198.08 14.75C198.08 43.69 198.08 72.63 198.08 101.56C194.82 101.56 191.56 101.56 188.3 101.56C188.3 69.66 188.3 37.76 188.3 5.86C197.09 4.21 207.3 5.55 216.25 5.55C223.37 5.55 231.27 4.61 238.02 7.26C256.29 14.45 261.98 38.12 251.68 53.95C249.29 57.62 245.59 60.98 241.67 62.92C239.97 63.76 238.07 64.1 236.54 65.25ZM467.75 101.23C462.71 93.59 457.97 94.1 453.25 101.61C451.72 92.91 469.15 92.93 467.75 101.23ZM457.75 99.12C460.39 98.54 463.26 98.28 464.15 101.25C463.9 102.29 462.85 103.26 462.87 104.25C462.89 105.35 463.8 105.84 463.75 106.97C461.63 106.35 461.64 104.18 459.25 104.14C458.92 105.05 458.58 105.95 458.25 106.85C456.94 105.41 457.43 101.06 457.75 99.12ZM462.77 101.25C461.85 99.77 460.66 99.94 459.07 100.25C459.07 101.09 459.07 101.93 459.07 102.77C460.92 102.91 461.96 103.04 462.77 101.25ZM468.25 101.29C469.96 104.83 466.62 109.62 463.13 110.8C457.15 112.8 452.46 107.74 452.51 102.25C454.09 104.16 454.1 108.24 457.44 109.78C463.97 112.78 467.37 106.36 468.25 101.29Z"
fill="#3469a5"
fill-rule="evenodd"
stroke="#3469a5"
stroke-width="0.25"
stroke-linejoin="round"
id="path4965"
style="fill:#0047ab;fill-opacity:1" />
<path
d="M337.99 5.75C334.75 6.31 327.37 4.96 323.25 5.1C314.9 5.38 306.42 8.73 299.5 13.27C271.82 31.42 270.21 73.63 296.37 93.86C303.08 99.05 311.75 103.27 320.25 104C325.22 104.42 330.6 103.37 335.44 104.25C328.2 108.53 312.04 105.99 304.44 103.31C268.94 90.76 258.61 45.98 284.33 18.59C294.3 7.97 308.35 2.73 322.75 2.84C326.45 2.87 335.35 3.45 337.99 5.75ZM359.48 27.87C359.28 28 359.08 28.12 358.89 28.25C359.02 28.92 359.16 29.58 359.3 30.25C358.78 30.28 358.27 30.31 357.75 30.34C357.45 30.65 357.15 30.95 356.84 31.25C356.83 31.68 356.83 32.1 356.82 32.53C354.88 30.66 353.41 28.22 351.5 26.25C346.08 20.65 338.54 16.88 331 15.26C326.74 14.34 319.72 16.05 316.25 14.68C328.38 8.03 351.75 16.98 359.48 27.87Z"
fill="#c45d3e"
fill-rule="evenodd"
stroke="#c45d3e"
stroke-width="0.25"
stroke-linejoin="round"
id="path4967"
style="fill:#cd7f32;fill-opacity:1" />
<path
d="M359.48 27.87C363.36 31.37 365.79 37.08 367.3 41.95C373.66 62.5 363.46 85.12 343.43 93.23C337.09 95.79 321.06 98.57 315.48 94.25C321.15 93.66 326.66 95.28 332.48 93.72C351.38 88.65 364.29 71.46 363.07 51.75C362.79 47.17 361.62 42.61 359.77 38.43C358.89 36.44 357.44 34.57 356.82 32.53C356.83 32.1 356.83 31.68 356.84 31.25C357.15 30.95 357.45 30.65 357.75 30.34C358.27 30.31 358.78 30.28 359.3 30.25C359.16 29.58 359.02 28.92 358.89 28.25C359.08 28.12 359.28 28 359.48 27.87Z"
fill="#d68551"
fill-rule="evenodd"
stroke="#d68551"
stroke-width="0.25"
stroke-linejoin="round"
id="path4969"
style="fill:#cd7f32;fill-opacity:1" />
</svg>

After

Width:  |  Height:  |  Size: 6.1 KiB

+62
View File
@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 474 159"
width="474"
height="159"
version="1.1"
id="svg4971"
sodipodi:docname="fairosrx_inverted.svg"
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs4975" />
<sodipodi:namedview
id="namedview4973"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="1.4842767"
inkscape:cx="237.48941"
inkscape:cy="79.5"
inkscape:window-width="1512"
inkscape:window-height="873"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="svg4971" />
<path
d="M444.4 15.25C441.68 23.37 431.19 17.11 426.32 15.4C413.29 10.83 390.72 13.95 390.41 31.25C390.21 42.22 399.98 44.81 408.65 47.16C425.62 51.77 450.1 53.21 450.22 76.25C450.24 79.45 449.92 82.58 448.78 85.58C441.34 105.13 410.99 104.68 394.86 98.85C388.59 96.59 373.77 88.72 381.76 80.25C384.76 80.59 390.94 87.06 394.49 88.73C404.72 93.56 416.25 94.72 427.27 92.09C432.87 90.76 439.09 87.04 439.95 80.75C441.63 68.4 434.34 64.02 423.92 60.78C410.29 56.54 383.56 55.46 381.03 37.25C377.34 10.64 402.77 2.26 424.43 5.76C430.08 6.68 440.84 10.49 444.4 15.25ZM64.75 5.56C65.88 12.91 57.87 14.55 52.25 14.55C42.58 14.55 32.92 14.55 23.25 14.55C19.81 14.55 14.24 13.51 11.27 15.25C11.27 26.37 11.27 37.49 11.27 48.61C25.1 48.61 38.92 48.61 52.75 48.61C53.75 54.08 48.57 57.09 43.75 57.51C36.2 58.16 28.33 57.55 20.75 57.55C17.94 57.55 13.62 56.72 11.26 58.25C11.26 72.69 11.26 87.13 11.26 101.57C8.1 101.57 4.95 101.57 1.79 101.57C1.79 69.57 1.79 37.56 1.79 5.56C22.78 5.56 43.76 5.56 64.75 5.56ZM106.25 5.63C119.66 37.6 133.08 69.57 146.49 101.54C143.24 101.54 140 101.54 136.75 101.54C125.42 74.3 114.08 47.06 102.75 19.81C96.79 33.79 90.83 47.77 84.88 61.75C91.72 63.14 99.76 61.98 106.75 61.97C109.48 61.97 113.38 61.27 115.75 62.59C115.57 67.59 111.49 70.64 106.75 71.02C98.53 71.68 89.42 69.49 81.38 71.25C77 81.35 72.63 91.46 68.25 101.56C65.07 101.56 61.89 101.56 58.71 101.56C72.05 69.58 85.4 37.6 98.75 5.63C101.25 5.63 103.75 5.63 106.25 5.63ZM167.05 5.86C167.05 37.76 167.05 69.66 167.05 101.55C163.93 101.55 160.8 101.55 157.68 101.55C155.91 93.75 157.27 84.27 157.27 76.25C157.27 59.58 157.27 42.92 157.27 26.25C157.27 18.99 155.56 4.43 167.05 5.86ZM236.54 65.25C243.95 76.65 251.35 88.05 258.75 99.44C262.92 93.09 267.08 86.75 271.25 80.4C273.13 83.01 275.01 85.63 276.89 88.25C272.66 95.08 268.43 101.92 264.21 108.75C273.54 124.08 282.88 139.42 292.22 154.75C285.64 158.84 280.93 153.78 277.65 148.6C273.2 141.56 268.71 134.52 264.32 127.43C262.53 124.53 260.84 120.55 258.25 118.39C252.08 127.99 245.92 137.59 239.75 147.18C236.25 147.18 232.75 147.18 229.25 147.18C237.04 134.37 244.84 121.56 252.63 108.75C243.69 94.92 234.76 81.08 225.82 67.25C218.58 66.86 203.83 69.73 204.25 58.14C207.43 56.75 212.28 57.61 215.75 57.61C228.29 57.61 241.7 58.43 246 43.76C246.79 41.05 247.37 38.38 247.25 35.51C246.88 26.95 241.78 17.87 233.12 15.21C227.81 13.58 221.23 14.55 215.75 14.55C209.93 14.55 203.85 14.06 198.08 14.75C198.08 43.69 198.08 72.63 198.08 101.56C194.82 101.56 191.56 101.56 188.3 101.56C188.3 69.66 188.3 37.76 188.3 5.86C197.09 4.21 207.3 5.55 216.25 5.55C223.37 5.55 231.27 4.61 238.02 7.26C256.29 14.45 261.98 38.12 251.68 53.95C249.29 57.62 245.59 60.98 241.67 62.92C239.97 63.76 238.07 64.1 236.54 65.25ZM467.75 101.23C462.71 93.59 457.97 94.1 453.25 101.61C451.72 92.91 469.15 92.93 467.75 101.23ZM457.75 99.12C460.39 98.54 463.26 98.28 464.15 101.25C463.9 102.29 462.85 103.26 462.87 104.25C462.89 105.35 463.8 105.84 463.75 106.97C461.63 106.35 461.64 104.18 459.25 104.14C458.92 105.05 458.58 105.95 458.25 106.85C456.94 105.41 457.43 101.06 457.75 99.12ZM462.77 101.25C461.85 99.77 460.66 99.94 459.07 100.25C459.07 101.09 459.07 101.93 459.07 102.77C460.92 102.91 461.96 103.04 462.77 101.25ZM468.25 101.29C469.96 104.83 466.62 109.62 463.13 110.8C457.15 112.8 452.46 107.74 452.51 102.25C454.09 104.16 454.1 108.24 457.44 109.78C463.97 112.78 467.37 106.36 468.25 101.29Z"
fill="#3469a5"
fill-rule="evenodd"
stroke="#3469a5"
stroke-width="0.25"
stroke-linejoin="round"
id="path4965"
style="fill:#cd7f32;fill-opacity:1" />
<path
d="M337.99 5.75C334.75 6.31 327.37 4.96 323.25 5.1C314.9 5.38 306.42 8.73 299.5 13.27C271.82 31.42 270.21 73.63 296.37 93.86C303.08 99.05 311.75 103.27 320.25 104C325.22 104.42 330.6 103.37 335.44 104.25C328.2 108.53 312.04 105.99 304.44 103.31C268.94 90.76 258.61 45.98 284.33 18.59C294.3 7.97 308.35 2.73 322.75 2.84C326.45 2.87 335.35 3.45 337.99 5.75ZM359.48 27.87C359.28 28 359.08 28.12 358.89 28.25C359.02 28.92 359.16 29.58 359.3 30.25C358.78 30.28 358.27 30.31 357.75 30.34C357.45 30.65 357.15 30.95 356.84 31.25C356.83 31.68 356.83 32.1 356.82 32.53C354.88 30.66 353.41 28.22 351.5 26.25C346.08 20.65 338.54 16.88 331 15.26C326.74 14.34 319.72 16.05 316.25 14.68C328.38 8.03 351.75 16.98 359.48 27.87Z"
fill="#c45d3e"
fill-rule="evenodd"
stroke="#c45d3e"
stroke-width="0.25"
stroke-linejoin="round"
id="path4967"
style="fill:#0047ab;fill-opacity:1" />
<path
d="M359.48 27.87C363.36 31.37 365.79 37.08 367.3 41.95C373.66 62.5 363.46 85.12 343.43 93.23C337.09 95.79 321.06 98.57 315.48 94.25C321.15 93.66 326.66 95.28 332.48 93.72C351.38 88.65 364.29 71.46 363.07 51.75C362.79 47.17 361.62 42.61 359.77 38.43C358.89 36.44 357.44 34.57 356.82 32.53C356.83 32.1 356.83 31.68 356.84 31.25C357.15 30.95 357.45 30.65 357.75 30.34C358.27 30.31 358.78 30.28 359.3 30.25C359.16 29.58 359.02 28.92 358.89 28.25C359.08 28.12 359.28 28 359.48 27.87Z"
fill="#d68551"
fill-rule="evenodd"
stroke="#d68551"
stroke-width="0.25"
stroke-linejoin="round"
id="path4969"
style="fill:#0047ab;fill-opacity:1" />
</svg>

After

Width:  |  Height:  |  Size: 6.1 KiB

+119
View File
@@ -0,0 +1,119 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
version="1.1"
id="Layer_1"
x="0px"
y="0px"
viewBox="0 0 43.213475 44.200001"
xml:space="preserve"
sodipodi:docname="MedCost_deepcove.svg"
width="43.213474"
height="44.200001"
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs2819" /><sodipodi:namedview
id="namedview2817"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="5.0021322"
inkscape:cx="85.66347"
inkscape:cy="22.590367"
inkscape:window-width="1512"
inkscape:window-height="873"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="g2806" />
<style
type="text/css"
id="style2778">
.st0{fill:#235937;}
.st1{fill:#55A51C;}
</style>
<g
id="g2810"
transform="translate(0.1)">
<g
id="g2808"
transform="matrix(1.0026328,0,0,1,2.6327667e-4,0)">
<g
id="g2794">
<path
class="st0"
d="m 73.7,10.7 v 26.9 h -3 V 25.5 c 0,-4.1 0.2,-9.5 0.1,-9.5 v 0 c 0,0 -1.4,2.5 -2.3,4.1 L 62.3,31 H 60.7 L 54.6,20.1 C 53.7,18.5 52.3,16 52.3,16 v 0 c 0,0 0.2,5.3 0.2,9.5 v 12.1 h -3 V 10.7 h 2.9 l 6.5,11.4 c 2.3,3.9 2.7,5 2.7,5 v 0 c 0,0 0.5,-1.1 2.7,-5 l 6.5,-11.4 z"
id="path2780" />
<path
class="st0"
d="m 95.7,34.6 v 3 H 77.9 V 10.7 h 17.5 v 3 H 81 v 9.1 h 11.6 v 2.8 H 81 v 9.1 h 14.7 z"
id="path2782" />
<path
class="st0"
d="m 111,10.7 c 7.5,0 13.6,5.5 13.6,13.6 0,8 -6,13.3 -13.4,13.3 H 99.9 V 10.7 Z m 0.2,23.9 c 5.5,0 10.2,-3.9 10.2,-10.3 0,-6.5 -4.7,-10.6 -10.4,-10.6 h -8 v 21 h 8.2 z"
id="path2784" />
<path
class="st0"
d="m 150.1,18 c -1.8,-3 -5.1,-4.8 -8.8,-4.8 -6,0 -10.5,4.4 -10.5,10.9 0,6.5 4.6,10.9 10.5,10.9 4,0 7.5,-2.2 9.3,-5.6 l 3,0.8 c -2.2,4.8 -6.8,7.8 -12.3,7.8 -7.5,0 -13.6,-5.9 -13.6,-14 0,-8.1 6.1,-14 13.6,-14 5.1,0 9.6,2.8 11.9,7.1 z"
id="path2786" />
<path
class="st0"
d="m 170,10.2 c 7.5,0 13.7,5.9 13.7,14 0,8.1 -6.1,14 -13.7,14 -7.5,0 -13.6,-5.9 -13.6,-14 0,-8.2 6.1,-14 13.6,-14 z m 0,24.9 c 5.9,0 10.5,-4.5 10.5,-10.9 0,-6.5 -4.5,-10.9 -10.5,-10.9 -6,0 -10.5,4.4 -10.5,10.9 0.1,6.4 4.6,10.9 10.5,10.9 z"
id="path2788" />
<path
class="st0"
d="m 202.1,18.1 c -0.6,-3.3 -3.3,-5.2 -6.6,-5.2 -3.5,0 -5.9,1.7 -5.9,4.4 0,3 3.6,3.9 7.1,5 3.7,1.1 8.7,2.8 8.7,8 0,4.5 -3.8,7.8 -9.6,7.8 -5.6,0 -9.2,-3.4 -10,-7.9 l 3,-0.8 c 0.4,3.1 2.9,5.9 7.2,5.9 3.9,0 6.3,-2.1 6.3,-4.9 0,-3.9 -4.5,-4.7 -8.3,-5.9 -3,-1 -7.4,-2.3 -7.4,-7 0,-4.2 3.9,-7.3 9.1,-7.3 4.8,0 8.5,2.9 9.4,7.2 z"
id="path2790" />
<path
class="st0"
d="m 227.7,10.7 v 3 h -8.8 v 23.9 h -3 v -24 h -8.8 v -3 h 20.6 z"
id="path2792" />
</g>
<g
id="g2806">
<path
class="st1"
d="M 29.6,13.7 C 31.4,9.5 32.5,4.9 32.5,0 h -4.2 c 0,7.9 -3.1,15.1 -8.1,20.4 -0.1,0.1 -0.3,0.3 -0.4,0.4 C 14.7,26 7.7,29.2 0,29.2 v 4.3 c 4.8,0 9.3,-1.1 13.3,-3 -1.8,4.2 -2.9,8.8 -2.9,13.7 h 4.2 c 0,-7.9 3.1,-15.1 8.1,-20.4 0.1,-0.1 0.3,-0.3 0.4,-0.4 C 28.2,18.2 35.2,15 42.9,15 v -4.3 c -4.7,0 -9.2,1.1 -13.3,3 z"
id="path2796"
style="fill:#04153e;fill-opacity:1" />
<path
class="st1"
d="m 16.6,44.2 h 4.2 c 0,-12.6 9.9,-22.8 22.2,-23 V 17 C 28.4,17.1 16.6,29.3 16.6,44.2 Z"
id="path2798"
style="fill:#04153e;fill-opacity:1" />
<path
class="st1"
d="m 22.7,44.2 h 4.2 c 0,-9.1 7.2,-16.5 16,-16.7 v -4.3 c -11.1,0.2 -20.2,9.5 -20.2,21 z"
id="path2800"
style="fill:#04153e;fill-opacity:1" />
<path
class="st1"
d="m 26.3,0 h -4.2 c 0,12.6 -9.9,22.8 -22.2,23 v 4.3 C 14.6,27.1 26.3,14.9 26.3,0 Z"
id="path2802"
style="fill:#04153e;fill-opacity:1" />
<path
class="st1"
d="M 20.2,0 H 16 C 16,9.1 8.8,16.5 0,16.7 V 21 C 11.2,20.8 20.2,11.5 20.2,0 Z"
id="path2804"
style="fill:#04153e;fill-opacity:1" />
</g>
</g>
</g>
<g
id="g2814"
transform="translate(0.1)">
<path
class="st0"
d="m 234.5,8.9 c 0,1.7 -1.3,3 -3,3 -1.7,0 -3,-1.3 -3,-3 0,-1.6 1.4,-3 3,-3 1.7,0.1 3,1.4 3,3 z m -5.3,0 c 0,1.3 1,2.4 2.3,2.4 1.3,0 2.3,-1 2.3,-2.3 0,-1.3 -1,-2.4 -2.3,-2.4 -1.3,0 -2.3,1 -2.3,2.3 z m 1.8,1.6 h -0.7 v -3 c 0.3,-0.1 0.6,-0.1 1.1,-0.1 0.6,0 0.8,0.1 1,0.2 0.2,0.1 0.3,0.4 0.3,0.6 0,0.3 -0.3,0.6 -0.6,0.7 v 0 c 0.3,0.1 0.4,0.3 0.5,0.7 0.1,0.4 0.1,0.6 0.2,0.7 h -0.7 C 232,10.2 232,9.9 231.9,9.6 231.8,9.3 231.7,9.1 231.3,9.1 H 231 Z m 0,-1.7 h 0.3 c 0.4,0 0.7,-0.1 0.7,-0.4 0,-0.3 -0.2,-0.5 -0.6,-0.5 -0.2,0 -0.3,0 -0.4,0 z"
id="path2812" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 KiB

+169
View File
@@ -0,0 +1,169 @@
belongs_to = member.joins(employer: { broker: :carrier }).select(
'brokers.id AS broker_id',
'carriers.id AS carrier_id'
)
keys = member.attributes.with_indifferent_access.slice(
:name, :family_id, :mb_member_key, :pb_entity_key, :pl_plan_key
)
enum :role, { member: 0, employer: 1, broker: 2, provider: 3, carrier: 4, admin: 5 }
http://localhost:3005/api/v1/claims/member_claims/343872
<div class="w-full flex items-center pt-6">
<h3 class="flex-none font-bold text-2xl text-brightlava">Admin</h3>
<div class="grow h-[1px] mt-2 ml-1 bg-brightlava"></div>
</div>
{
"network_provider": "Cigna",
"plan_benefits": [
{
"id": 1295,
"title": "CLASSIC 3.5K",
"sequence": 1,
"benefit": "$25",
"benefit_desc": "Primary Visit"
},
{
"id": 1296,
"title": "CLASSIC 3.5K",
"sequence": 2,
"benefit": "$50",
"benefit_desc": "Specialist Visit"
},
{
"id": 1297,
"title": "CLASSIC 3.5K",
"sequence": 3,
"benefit": "$50",
"benefit_desc": "Urgent Care"
},
{
"id": 1298,
"title": "CLASSIC 3.5K",
"sequence": 4,
"benefit": "$3,500",
"benefit_desc": "INN-Ind Ded"
},
{
"id": 1299,
"title": "CLASSIC 3.5K",
"sequence": 5,
"benefit": "$7,000",
"benefit_desc": "INN-Family Ded"
},
{
"id": 1300,
"title": "CLASSIC 3.5K",
"sequence": 6,
"benefit": "$7,000",
"benefit_desc": "OON-Ind Ded"
},
{
"id": 1301,
"title": "CLASSIC 3.5K",
"sequence": 7,
"benefit": "$14,000",
"benefit_desc": "OON-Family Ded"
},
{
"id": 1302,
"title": "CLASSIC 3.5K",
"sequence": 8,
"benefit": "70%/30%",
"benefit_desc": "Co-Insurance"
},
{
"id": 1303,
"title": "CLASSIC 3.5K",
"sequence": 9,
"benefit": "$7,000",
"benefit_desc": "INN-Ind OOP"
},
{
"id": 1304,
"title": "CLASSIC 3.5K",
"sequence": 10,
"benefit": "$14,000",
"benefit_desc": "INN-Family OOP"
},
{
"id": 1305,
"title": "CLASSIC 3.5K",
"sequence": 11,
"benefit": "$14,000",
"benefit_desc": "OON-Ind OOP"
},
{
"id": 1306,
"title": "CLASSIC 3.5K",
"sequence": 12,
"benefit": "$28,000",
"benefit_desc": "OON-Family OOP"
},
{
"id": 1307,
"title": "CLASSIC 3.5K",
"sequence": 13,
"benefit": "Sub to Ded",
"benefit_desc": "Emergency Room"
},
{
"id": 1308,
"title": "CLASSIC 3.5K",
"sequence": 14,
"benefit": "100%",
"benefit_desc": "Preventive Care"
}
],
"recent_claims": [
{
"claim_number": 1950638,
"visit_date": "04-28-2026",
"claim_status": "Approved Claim",
"patient_name": "LAWHORN, ALMON",
"provider_name": "COLE, CHRISTOPHER"
},
{
"claim_number": 1949703,
"visit_date": "02-12-2026",
"claim_status": "Approved Claim",
"patient_name": "LAWHORN, ALMON",
"provider_name": "CIGNA HEALTH AND LIFE INSURANCE COMPANY"
},
{
"claim_number": 1942866,
"visit_date": "02-12-2026",
"claim_status": "Denied Claim",
"patient_name": "LAWHORN, ALMON",
"provider_name": "NELSON, JOHN C"
},
{
"claim_number": 1942522,
"visit_date": "02-12-2026",
"claim_status": "Denied Claim",
"patient_name": "LAWHORN, ALMON",
"provider_name": "MALOY, JENNIFER R"
},
{
"claim_number": 1941594,
"visit_date": "02-12-2026",
"claim_status": "Denied Claim",
"patient_name": "LAWHORN, ALMON",
"provider_name": "JON K MARTI, DO"
},
{
"claim_number": 1940479,
"visit_date": "02-12-2026",
"claim_status": "Approved Claim",
"patient_name": "LAWHORN, ALMON",
"provider_name": "GROUNDS, DAVID S"
}
]
}
+1 -1
View File
@@ -46,7 +46,7 @@ export default async function LandingPageLayout({ children }: { children: React.
</div>
</div>
</div> */}
<div className="w-full">
<div className="w-full text-platinum">
{children}
</div>
<Footer />
+54
View File
@@ -0,0 +1,54 @@
"user client"
import { sideNavItems } from '@/lib/dashboard';
import { headers } from "next/headers";
import Script from 'next/script';
import AccountDropdown from '../../components/AccountDropdown';
import SideNav from "@/components/SideNav";
import Image from 'next/image';
import classnames from 'classnames';
export default async function DashboardLayout({ children }: { children: React.ReactNode }) {
const headersList = await headers();
const pathname = headersList.get("x-current-path");
const dashboardType = pathname?.split('/').pop() || "";
const pageNavItems = sideNavItems[dashboardType];
return (
<div className="relative w-full h-full">
<div className="absolute bottom-0 right-0 flex flex-col justify-end items-end w-full h-full">
<div className="w-full -mb-[2px] flex justify-start">
<div className="w-[87vw] sm:w-[91vw] md:w-[93vw] lg:w-[94.5vw] xl:w-[95.5vw] h-20 border-b-2 border-light-bronze z-2" />
</div>
<div className="w-[60px] h-full flex justify-start">
<div className="w-[32.5px] flex flex-col">
<div className={classnames(
"relative h-full border-r-6 rounded-out-tl-4xl rounded-tr-4xl bg-light-bronze border-atmosphere"
)} />
</div>
</div>
</div>
<div className="w-full h-full flex items-start">
<div className="w-full min-w-[87vw] max-w-[94vw] lg:min-w-[89vw] lg:max-w-[95vw] flex flex-col items-end">
<div className="w-[90.15vw] h-20 flex justify-between items-center">
<div className={classnames('flex items-center')}>
<div className="-ml-8.5 z-2">
<Image
src="/images/bbstpa-forsite.png"
alt="Britton Logo"
width={205}
height={60}
/>
</div>
</div>
</div>
<div className="w-full h-full flex justify-center ml-[60px]">
{children}
</div>
</div>
</div>
</div>
)
}
+94
View File
@@ -0,0 +1,94 @@
// /app/login/page.tsx
'use test' // Marks this component as a client component
'use client'
import classnames from 'classnames';
import { useActionState } from 'react'
import { signInAndFetchUser } from '@/app/actions/signin'
import { Schibsted_Grotesk } from 'next/font/google'
import Link from 'next/link';
import Button from '@/components/Button';
const font = Schibsted_Grotesk({ subsets: ['latin'] })
export default function SignInPage() {
// useActionState manages the server state (errors, loading) and form binding
const [state, formAction, isPending] = useActionState(signInAndFetchUser, null)
return (
<div
className="h-full flex flex-col items-center justify-center py-6 mt-10 px-4 sm:px-6 lg:px-8"
>
<div className="flex w-full items-center justify-center text-7xl font-bold text-atmosphere">
Britton Benefits
{/* <p>Britton</p>
<p>Benefits</p> */}
</div>
<div className={classnames("w-md space-y-4", font.className)}>
<div className="mt-10 text-center text-3xl font-extrabold text-light-bronze border-b-2 border-light-bronze">
Sign In
</div>
<form action={formAction}>
{/* Error Message Display */}
{state?.error && (
<div className="rounded-lg bg-red-50 p-3 text-sm text-red-600">
{state.error}
</div>
)}
<div className="mt-2">
<div className="rounded-md shadow-sm -space-y-px">
<label className="block text-md font-bold text-platinum">Email</label>
<input
type="email"
name="email"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 bg-platinum border border-gray-300 placeholder-bluetang text-deepcove rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="you@example.com"
/>
</div>
</div>
<div className="mt-1">
<div className="rounded-md shadow-sm -space-y-px">
<label className="block text-md font-bold text-platinum">Password</label>
<input
type="password"
name="password"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 bg-platinum border border-gray-300 placeholder-bluetang text-deepcove rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder=""
/>
</div>
</div>
<button
type="submit"
disabled={isPending}
className="relative w-full flex justify-center py-2 px-4 mt-6 border-4 border-cobalt font-bold rounded-lg text-lg text-platinum hover:text-light-bronze bg-cobalt hover:bg-deepcove hover:cursor-pointer"
>
{isPending ? 'Signing In...' : 'Submit'}
</button>
</form>
{/* <div className="text-bluemana">
Not signed up?
<Link
href="/account/signin"
>
<span className="hover:text-light-bronze">Click here.</span>
</Link>
</div> */}
</div>
<div className="text-bluemana mt-4 z-20">
Not signed up?{' '}
<Link
href="/account/signup"
className="underline text-atmosphere hover:text-light-bronze z-10"
>
Click here.
</Link>
</div>
</div>
)
}
+188
View File
@@ -0,0 +1,188 @@
// /app/login/page.tsx
'use test' // Marks this component as a client component
'use client'
import classnames from 'classnames';
import { useActionState, useState } from 'react'
import { registerUser } from '@/app/actions/signup'
import { Schibsted_Grotesk } from 'next/font/google'
const font = Schibsted_Grotesk({ subsets: ['latin'] })
export default function LoginPage() {
// useActionState manages the server state (errors, loading) and form binding
const [state, formAction, isPending] = useActionState(registerUser, null)
const [role, setRole] = useState("");
const [date, setDate] = useState("");
const handleDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
let input = e.target.value.replace(/\D/g, "");
if (input.length > 2 && input.length <= 4) {
input = `${input.slice(0, 2)}/${input.slice(2)}`;
} else if (input.length > 4) {
input = `${input.slice(0, 2)}/${input.slice(2, 4)}/${input.slice(4, 8)}`;
}
setDate(input);
};
return (
<div
className="h-full flex flex-col items-center justify-center py-6 px-4 sm:px-6 lg:px-8"
>
<div className="flex w-full items-center justify-center text-7xl font-bold text-atmosphere">
Britton Benefits
{/* <p>Britton</p>
<p>Benefits</p> */}
</div>
<div className={classnames("w-md space-y-4", font.className)}>
<div className="mt-10 text-center text-3xl font-extrabold text-light-bronze border-b-2 border-light-bronze">
Sign Up
</div>
<div className="mb-4 text-center text-md font-extrabold text-bluemana">
(Employer and Broker users must be invited)
</div>
<form action={formAction}>
{/* Error Message Display */}
{state?.error && (
<div className="rounded-lg bg-red-50 p-3 text-sm text-red-600">
{state.error}
</div>
)}
<div>
<label htmlFor="two-item-dropdown" className="block text-md font-bold text-platinum">
I Am A...
</label>
<select
id="role"
name="role"
value={role}
onChange={(e) => setRole(e.target.value)}
required
className="rounded-none relative block w-full px-3 py-2 bg-platinum border border-gray-300 placeholder-bluetang text-deepcove rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
>
{/* Placeholder option */}
<option value="" disabled>Select User Role</option>
{/* The two dropdown items */}
<option value="member">Member</option>
<option value="provider">Provider</option>
</select>
</div>
<div className="mt-2">
<div className="rounded-md shadow-sm -space-y-px">
<label className="block text-md font-bold text-platinum">Email</label>
<input
type="email"
name="email"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 bg-platinum border border-gray-300 placeholder-bluetang text-deepcove rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="you@example.com"
/>
</div>
</div>
{role === 'member' && (
<>
{/* <div className="mt-2">
<div className="rounded-md shadow-sm -space-y-px">
<label className="block text-md font-bold text-platinum">Member ID</label>
<input
type="roleId"
name="roleId"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 bg-platinum border border-gray-300 placeholder-bluetang text-deepcove rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="######"
/>
</div>
</div> */}
<div className="mt-2">
<div className="rounded-md shadow-sm -space-y-px">
<label className="block text-md font-bold text-platinum">Last 4 Digits of SSN</label>
<input
type="lastFourSsn"
name="lastFourSsn"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 bg-platinum border border-gray-300 placeholder-bluetang text-deepcove rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="####"
/>
</div>
</div>
<div className="mt-2">
<div className="rounded-md shadow-sm -space-y-px">
<label className="block text-md font-bold text-platinum">Date of Birth</label>
<input
type="dateOfBirth"
name="dateOfBirth"
value={date}
onChange={handleDateChange}
required
className="appearance-none rounded-none relative block w-full px-3 py-2 bg-platinum border border-gray-300 placeholder-bluetang text-deepcove rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="MM/DD/YYYY"
/>
</div>
</div>
</>
)}
{role === 'provider' && (
<div className="mt-2">
<div className="rounded-md shadow-sm -space-y-px">
<label className="block text-md font-bold text-platinum">Tax ID</label>
<input
type="roleId"
name="roleId"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 bg-platinum border border-gray-300 placeholder-bluetang text-deepcove rounded-t-md rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="######"
/>
</div>
</div>
)}
<div className="mt-1">
<div className="rounded-md shadow-sm -space-y-px">
<label className="block text-md font-bold text-platinum">Password</label>
<input
type="password"
name="password"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 bg-platinum border border-gray-300 placeholder-bluetang text-deepcove rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="••••••••"
/>
</div>
</div>
<div className="mt-1">
<div className="rounded-md shadow-sm -space-y-px">
<label className="block text-md font-bold text-platinum">Confirm Password</label>
<input
type="confirmPassword"
name="confirmPassword"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 bg-platinum border border-gray-300 placeholder-bluetang text-deepcove rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="••••••••"
/>
</div>
</div>
<button
type="submit"
disabled={isPending}
className="relative w-full flex justify-center py-2 px-4 mt-6 border-4 border-cobalt font-bold rounded-lg text-lg text-platinum hover:text-light-bronze bg-cobalt hover:bg-deepcove hover:cursor-pointer"
>
{isPending ? 'Signing In...' : 'Submit'}
</button>
</form>
</div>
</div>
)
}
+100
View File
@@ -0,0 +1,100 @@
// /app/actions/auth.ts
'use server'
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
import { SignJWT } from 'jose/jwt/sign';
const SECRET_KEY = new TextEncoder().encode(process.env.JWT_SECRET);
function roleMapping(role_number: number) {
const map = [
"member",
"employer",
"broker",
"provider",
"carrier",
"admin"
]
return map[role_number]
}
export async function loginUser(prevState: any, formData: FormData) {
const email = formData.get('email') as string
const password = formData.get('password') as string
// Simple validation check
if (!email || !password) {
throw new Error('Please fill in all fields.');
}
const response = await fetch(`${process.env.BE_API_SERVER_HOST}:${process.env.BE_API_SERVER_PORT}/api/v1/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email, password }),
});
if (!response.ok) {
throw new Error('Invalid credentials');
}
const data = await response.json();
const role = roleMapping(data.keychain.role)
const sessionPayload = {
accessToken: data.token,
user: {
id: data.id,
name: data.name,
email: data.email,
keychain: data.keychain,
role: role,
role_id: data.keychain.role_id
}
};
const encryptedSession = await new SignJWT(sessionPayload)
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('2h')
.sign(SECRET_KEY);
const cookieStore = await cookies();
cookieStore.set('session', encryptedSession, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/',
});
const initUserResponse = await fetch('https://yourdomain.com', {
headers: { Authorization: `Bearer ${token}` },
});
const userData = await initUserResponse.json();
// return { success: true };
if (role == "member") {
redirect('/dashboard/member')
} else if (role == "employer") {
redirect('/dashboard/employer')
} else if (role == "broker") {
redirect('/dashboard/broker')
} else if (role == "provider") {
redirect('/dashboard/provider')
} else if (role == "carrier") {
redirect('/dashboard/carrier')
} else if (role == "admin") {
redirect('/dashboard/admin')
} else {
return { error: 'Invalid email or password.' }
}
}
+91
View File
@@ -0,0 +1,91 @@
'use server'
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
import { SignJWT } from 'jose';
const SECRET_KEY = new TextEncoder().encode(process.env.JWT_SECRET_KEY);
function roleMapping(role_number: number) {
const map = [
"member",
"employer",
"broker",
"provider",
"carrier",
"admin"
]
return map[role_number]
}
export async function signInAndFetchUser(prevState: any, formData: FormData) {
const email = formData.get('email') as string
const password = formData.get('password') as string
if (!email || !password) {
throw new Error('Please fill in all fields.');
}
const authResponse = await fetch(`http://${process.env.BE_API_SERVER_HOST}:${process.env.BE_API_SERVER_PORT}/signin`, {
method: 'POST',
body: JSON.stringify({ user: {email, password} }),
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
});
if (!authResponse.ok) {
throw new Error('Invalid credentials');
}
const { user } = await authResponse.json();
const token = authResponse.headers.get('authorization')?.split(' ')[1];
console.log("token: ", token)
// const token = user.jti
const role = user.role
const sessionPayload = {
token: token,
user: {
id: user.id,
name: user.name,
email: user.email,
role: role,
roleId: user.role_id,
keychain: user.keychain
}
};
console.log("sessionPayload: ", sessionPayload)
const encryptedSession = await new SignJWT(sessionPayload)
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('2h')
.sign(SECRET_KEY);
const cookieStore = await cookies();
cookieStore.set('session', encryptedSession, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/',
});
if (role == "member") {
redirect('/dashboard/member')
} else if (role == "employer") {
redirect('/dashboard/employer')
} else if (role == "broker") {
redirect('/dashboard/broker')
} else if (role == "provider") {
redirect('/dashboard/provider')
} else if (role == "carrier") {
redirect('/dashboard/carrier')
} else if (role == "admin") {
redirect('/dashboard/admin')
} else {
return { error: 'Invalid email or password.' }
}
}
+98
View File
@@ -0,0 +1,98 @@
// app/login/actions.ts
'use server'
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
import { SignJWT } from 'jose';
const SECRET_KEY = new TextEncoder().encode(process.env.JWT_SECRET_KEY);
function roleMapping(role_number: number) {
const map = [
"member",
"employer",
"broker",
"provider",
"carrier",
"admin"
]
return map[role_number]
}
export async function registerUser(prevState: any, formData: FormData) {
const email = formData.get('email') as string
const password = formData.get('password') as string
const confirm_password = formData.get('confirmPassword') as string
const role = formData.get('role') as string
const role_id = formData.get('roleId') as string
const last_four_ssn = formData.get('lastFourSsn') as string
const date_of_birth = formData.get('dateOfBirth') as string
if (!email || !password || !confirm_password || !role) {
throw new Error('Please fill in all fields.');
}
if (role === "provider" && !role_id) {
throw new Error('Please fill in all fields.');
}
if (role === "member" && (!date_of_birth || !last_four_ssn)) {
throw new Error('Please fill in all fields.');
}
const authResponse = await fetch(`${process.env.BE_API_SERVER_HOST}:${process.env.BE_API_SERVER_PORT}/api/v1/login`, {
method: 'POST',
body: JSON.stringify({ user: {email, password, confirm_password, role, role_id, last_four_ssn, date_of_birth} }),
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
});
if (!authResponse.ok) {
throw new Error('Invalid credentials');
}
const data = await authResponse.json();
const token = data.jti
const sessionPayload = {
token: token,
user: {
id: data.id,
name: data.name,
email: data.email,
role: data.role,
roleId: data.role_id,
keychain: data.keychain
}
};
const encryptedSession = await new SignJWT(sessionPayload)
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('2h')
.sign(SECRET_KEY);
const cookieStore = await cookies();
cookieStore.set('session', encryptedSession, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/',
});
if (role == "member") {
redirect('/dashboard/member')
} else if (role == "employer") {
redirect('/dashboard/employer')
} else if (role == "broker") {
redirect('/dashboard/broker')
} else if (role == "provider") {
redirect('/dashboard/provider')
} else if (role == "carrier") {
redirect('/dashboard/carrier')
} else if (role == "admin") {
redirect('/dashboard/admin')
} else {
return { error: 'Invalid email or password.' }
}
}
+18
View File
@@ -0,0 +1,18 @@
import { isIPv4 } from 'net';
export async function GET() {
// Read the runtime env variable safely on the server
let apiUrl = "";
const backendApiHost = process.env.BE_API_SERVER_HOST || "localhost";
if (isIPv4(backendApiHost)) {
apiUrl = apiUrl.concat(backendApiHost)
} else {
apiUrl = apiUrl.concat("127.0.0.1")
}
if (process.env.BE_API_SERVER_PORT) {
apiUrl = apiUrl.concat(":").concat(process.env.BE_API_SERVER_PORT)
}
return Response.json({ apiUrl });
}
+148
View File
@@ -0,0 +1,148 @@
import classnames from 'classnames';
import { cookies } from "next/headers";
import { JWTPayload, jwtVerify } from "jose";
import DashboardHeader from "@/components/DashboardHeader";
import { Schibsted_Grotesk } from 'next/font/google'
import { redirect } from 'next/navigation';
import { isRedirectError } from 'next/dist/client/components/redirect-error';
const font = Schibsted_Grotesk({ subsets: ['latin'] })
const SECRET_KEY = new TextEncoder().encode(process.env.JWT_SECRET_KEY);
interface ClaimType {
claim_number: number;
visit_date: string;
claim_status: string;
patient_name: string;
provider_name: string;
}
interface UserJwtPayload extends JWTPayload {
user: UserData;
}
interface UserData {
id: string;
email: string;
name: string;
role: string;
roleId: string;
keychain: UserKeychain;
}
interface UserKeychain {
family_id: string;
mb_member_key: number;
pb_entity_key: number;
pl_plan_key: number;
};
export default async function EmployerIdCards() {
const cookieStore = await cookies();
const sessionCookie = cookieStore.get("session");
if (!sessionCookie) {
redirect('/account/signin')
}
try {
// 2. Extract the raw string token from the cookie object
const token = sessionCookie.value;
// 3. Verify and decode the JWT using jose
const { payload } = await jwtVerify(token, SECRET_KEY)
// 3. Cast the payload to your custom interface
const data = payload as UserJwtPayload;
// const userDataTest = {
// pb_entity_key: 377527,
// pl_plan_key: '61',
// name: "John McMember"
// }
const userData = {
pb_entity_key: data.user.keychain.pb_entity_key,
pl_plan_key: data.user.keychain.pl_plan_key,
name: data.user.name
}
const response = await fetch(`http://${process.env.BE_API_SERVER_HOST}:${process.env.BE_API_SERVER_PORT}/api/v1/members/${userData.pb_entity_key}/claims`, {
method: 'GET',
credentials: 'include',
headers: {
'Content-Type': 'application/json' ,
'Authorization': `Bearer ${data.token}`
},
});
if (!response.ok) throw new Error("Unauthenticated");
const claimsData: ClaimType[] = await response.json()
return (
<>
{claimsData &&
<div className="flex flex-col w-full pl-8 pt-5 2xl:pt-7">
<DashboardHeader
userDisplayName={userData.name}
/>
<div className="flex w-full pr-23">
<div className={classnames("w-full flex flex-col pt-4 z-2 space-x-4 space-y-4 xl:space-y-0 text-bluemana")}>
<div className="w-full flex text-3xl ml-2 mb-4 font-semibold">
<p>Claims</p>
</div>
<div className={classnames("w-full flex flex-col px-2 text-lg", font.className)}>
<div className='flex w-full mb-4 border-b border-bluemana'>
<div className="flex w-2/20">
Claim #
</div>
<div className="flex w-4/20 pl-3">
Patient Name
</div>
<div className="flex w-6/20 pl-3">
Provider
</div>
<div className="flex w-3/20 pl-3">
Visit Date
</div>
<div className="flex w-4/20 pl-8.5">
Claim Status
</div>
</div>
{(claimsData).map((mc) => (
<div key={mc.claim_number} className="flex w-full text-platinum">
<div className="flex w-2/20">
{mc.claim_number}
</div>
<div className="flex w-4/20 pl-3">
{mc.patient_name}
</div>
<div className="flex w-6/20 pl-3">
{mc.provider_name}
</div>
<div className="flex w-3/20 pl-3">
{mc.visit_date}
</div>
<div className="flex w-4/20 pl-8.5">
{mc.claim_status}
</div>
</div>
))}
</div>
</div>
</div>
</div>
}
</>
);
} catch (error: any) {
if (error.message === 'NEXT_REDIRECT') throw error;
if (error.message === 'Unauthenticated') {
redirect('/account/signin');
}
}
}
+149
View File
@@ -0,0 +1,149 @@
import classnames from 'classnames';
import DashboardHeader from "@/components/DashboardHeader";
import FindProviderWidget from '@/components/FindProvider';
import InsuranceCardWidget from '@/components/InsuranceCard';
import FindRxInformationWidget from '@/components/FindRxInformation';
import RecentClaimsWidget from '@/components/RecentClaimsWidget';
import BenefitsAAGWidget from '@/components/BenefitsAAGWidget';
import { cookies } from "next/headers";
import { JWTPayload, jwtVerify } from "jose";
import { redirect } from 'next/navigation';
import IdCardViewer from '@/components/IdCardViewer';
// Ensure your secret key matches exactly what you used to sign the token
const SECRET_KEY = new TextEncoder().encode(process.env.JWT_SECRET_KEY);
interface PlanBenefitType {
id: number;
title: string;
sequence: number;
benefit: string;
benefit_desc: string;
}
interface RecentClaimType {
claim_number: number;
visit_date: string;
claim_status: string;
patient_name: string;
provider_name: string;
}
interface DashboardDataType {
network_provider: string;
plan_benefits: PlanBenefitType[];
recent_claims: RecentClaimType[];
}
interface UserJwtPayload extends JWTPayload {
user: UserData;
}
interface UserData {
id: string;
email: string;
name: string;
role: string;
roleId: string;
keychain: UserKeychain;
}
interface UserKeychain {
family_id: string;
mb_member_key: number;
pb_entity_key: number;
pl_plan_key: number;
};
export default async function Member() {
const cookieStore = await cookies();
const sessionCookie = cookieStore.get("session"); // Replace with your cookie name
if (!sessionCookie) {
redirect('/account/signin')
}
try {
// 2. Extract the raw string token from the cookie object
const token = sessionCookie.value;
// 3. Verify and decode the JWT using jose
const { payload } = await jwtVerify(token, SECRET_KEY)
// 3. Cast the payload to your custom interface
const data = payload as UserJwtPayload;
// const userDataTest = {
// pb_entity_key: 377527,
// pl_plan_key: '61',
// name: "John McMember"
// }
const userData = {
pb_entity_key: data.user.keychain.pb_entity_key,
pl_plan_key: data.user.keychain.pl_plan_key,
name: data.user.name
}
const response = await fetch(`http://${process.env.BE_API_SERVER_HOST}:${process.env.BE_API_SERVER_PORT}/api/v1/members/${userData.pb_entity_key}/initialize_dashboard`, {
method: 'GET',
credentials: 'include',
headers: {
'Content-Type': 'application/json' ,
'Authorization': `Bearer ${data.token}`
},
});
if (!response.ok) throw new Error("Unauthenticated");
const dashboardData: DashboardDataType = await response.json()
return (
<>
{dashboardData &&
<div className="flex flex-col w-full pl-8 pt-5 2xl:pt-7">
<DashboardHeader
userDisplayName={userData.name}
/>
<div className="flex w-full pr-23">
<div className={classnames("w-full flex flex-col xl:flex-row pt-4 z-2 space-x-4 space-y-4 xl:space-y-0")}>
<div className="flex flex-col gap-y-4">
<div className="flex flex-col w-full h-full xl:h-auto lg:flex-row gap-y-4 lg:gap-y-0 lg:gap-x-4">
{/* <DeductableProgressWidget /> */}
<IdCardViewer
pbEntityKey={userData.pb_entity_key}
cardLaout="FullPageCard"
/>
<FindProviderWidget
// pl_plan_key='57'
networkProvider={dashboardData.network_provider}
/>
</div>
<FindRxInformationWidget />
<RecentClaimsWidget
recentClaims={dashboardData.recent_claims}
/>
</div>
<BenefitsAAGWidget
planBenefits={dashboardData.plan_benefits}
/>
</div>
</div>
</div>
}
</>
);
} catch (error: any) {
if (error.message === 'NEXT_REDIRECT') throw error;
if (error.message === 'Unauthenticated') {
redirect('/account/signin');
}
}
// } catch (error) {
// // Fails if token is expired, tampered with, or invalid
// console.error("JWT verification failed:", error);
// return <div>Session invalid or expired.</div>;
// }
}
+82 -45
View File
@@ -1,63 +1,100 @@
"user client"
import { sideNavItems } from '@/lib/dashboard';
import { headers } from "next/headers";
import Script from 'next/script';
import { cookies } from "next/headers";
import { JWTPayload, jwtVerify } from "jose";
import AccountDropdown from '../../components/AccountDropdown';
import SideNav from "@/components/SideNav";
import Image from 'next/image';
import classnames from 'classnames';
const SECRET_KEY = new TextEncoder().encode(process.env.JWT_SECRET_KEY);
interface UserJwtPayload extends JWTPayload {
user: UserData;
}
interface UserData {
id: string;
email: string;
name: string;
role: string;
roleId: string;
}
export default async function DashboardLayout({ children }: { children: React.ReactNode }) {
const headersList = await headers();
const pathname = headersList.get("x-current-path");
const dashboardType = pathname?.split('/').pop() || "";
const pageNavItems = sideNavItems[dashboardType];
const cookieStore = await cookies();
const sessionCookie = cookieStore.get("session"); // Replace with your cookie name
return (
<div className="relative w-full h-full">
<Script src={`https://maps.googleapis.com/maps/api/js?key=${process.env.GOOGLE_PLACES_API_KEY}&libraries=places`} strategy="beforeInteractive" />
<div className="absolute bottom-0 right-0 flex flex-col justify-end items-end w-full h-full">
<div className="w-full -mb-[2px] flex justify-start">
<div className="w-[87vw] sm:w-[91vw] md:w-[93vw] lg:w-[94.5vw] xl:w-[95.5vw] h-20 border-b-2 border-copper z-2" />
</div>
<div className="w-[60px] h-full flex justify-start">
<div className="w-[32.5px] flex flex-col">
<div className={classnames(
"relative h-full border-r-6 rounded-out-tl-4xl rounded-tr-4xl bg-copper border-atmosphere"
)} />
</div>
</div>
</div>
<div className="w-full h-full flex items-start">
<SideNav
maxWidth="w-[16vw]"
bgColor="bg-atmosphere"
navItems={pageNavItems}
/>
<div className="w-full min-w-[87vw] max-w-[94vw] lg:min-w-[89vw] lg:max-w-[95vw] flex flex-col items-end">
<div className="w-[90.15vw] h-20 flex justify-between items-center">
<div className={classnames('flex items-center')}>
<div className="-ml-8.5 z-2">
<Image
src="/images/bbstpa-forsite.png"
alt="Britton Logo"
width={205}
height={60}
/>
if (!sessionCookie) {
return <div className='flex w-full justify-center'>No active session. Please log in.</div>;
}
try {
// 2. Extract the raw string token from the cookie object
const token = sessionCookie.value;
// 3. Verify and decode the JWT using jose
const { payload } = await jwtVerify(token, SECRET_KEY)
// 3. Cast the payload to your custom interface
const data = payload as UserJwtPayload;
const userRole = data.user.role
// const headersList = await headers();
// const pathname = headersList.get("x-current-path");
// const dashboardType = pathname?.split('/').pop() || "";
const pageNavItems = sideNavItems[userRole];
return (
<div className="relative w-full h-full">
<div className="absolute bottom-0 right-0 flex flex-col justify-end items-end w-full h-full">
<div className="w-full -mb-[2px] flex justify-start">
<div className="w-[87vw] sm:w-[91vw] md:w-[93vw] lg:w-[94.5vw] xl:w-[95.5vw] h-20 border-b-2 border-copper z-2" />
</div>
<div className="w-[60px] h-full flex justify-start">
<div className="w-[32.5px] flex flex-col">
<div className={classnames(
"relative h-full border-r-6 rounded-out-tl-4xl rounded-tr-4xl bg-copper border-atmosphere"
)} />
</div>
</div>
<div className="pr-20 z-2">
<AccountDropdown userRole="member" />
</div>
<div className="w-full h-full flex items-start">
<SideNav
maxWidth="w-[16vw]"
bgColor="bg-atmosphere"
navItems={pageNavItems}
/>
<div className="w-full min-w-[87vw] max-w-[94vw] lg:min-w-[89vw] lg:max-w-[95vw] flex flex-col items-end">
<div className="w-[90.15vw] h-20 flex justify-between items-center">
<div className={classnames('flex items-center')}>
<div className="-ml-4.5 z-2">
<Image
src="/images/bbstpa-forsite.png"
alt="Britton Logo"
width={205}
height={60}
/>
</div>
</div>
<div className="pr-20 z-2">
<AccountDropdown userRole="member" />
</div>
</div>
<div className="w-full h-full flex justify-start">
{children}
</div>
</div>
</div>
<div className="w-full h-full flex justify-start">
{children}
</div>
</div>
</div>
</div>
)
)
} catch (error) {
// Fails if token is expired, tampered with, or invalid
console.error("JWT verification failed:", error);
return <div className='flex w-full justify-center'>Session invalid or expired.</div>;
}
}
+76
View File
@@ -0,0 +1,76 @@
// /app/login/page.tsx
'use test' // Marks this component as a client component
'use client'
import classnames from 'classnames';
import { useActionState } from 'react'
import { loginAndFetchUser } from '@/app/actions/signin'
import { Schibsted_Grotesk } from 'next/font/google'
const font = Schibsted_Grotesk({ subsets: ['latin'] })
export default function LoginPage() {
// useActionState manages the server state (errors, loading) and form binding
const [state, formAction, isPending] = useActionState(loginAndFetchUser, null)
return (
<div
className="min-h-full flex flex-col items-center justify-center py-6 px-4 sm:px-6 lg:px-8"
>
<div className="flex w-full items-center justify-center text-7xl font-bold text-atmosphere">
Britton Benefits
{/* <p>Britton</p>
<p>Benefits</p> */}
</div>
<div className={classnames("w-sm space-y-4", font.className)}>
<div className="mt-10 text-center text-3xl font-extrabold text-light-bronze border-b-1 border-light-bronze">
Sign In
</div>
<form action={formAction}>
{/* Error Message Display */}
{state?.error && (
<div className="rounded-lg bg-red-50 p-3 text-sm text-red-600">
{state.error}
</div>
)}
<div className="mt-2">
<div className="rounded-md shadow-sm -space-y-px">
<label className="block text-md font-bold text-platinum">Email</label>
<input
type="email"
name="email"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 bg-platinum border border-gray-300 placeholder-bluetang text-deepcove rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="you@example.com"
/>
</div>
</div>
<div className="mt-1">
<div className="rounded-md shadow-sm -space-y-px">
<label className="block text-md font-bold text-platinum">Password</label>
<input
type="password"
name="password"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 bg-platinum border border-gray-300 placeholder-bluetang text-deepcove rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder=""
/>
</div>
</div>
<button
type="submit"
disabled={isPending}
className="relative w-full flex justify-center py-2 px-4 mt-6 border-4 border-cobalt font-bold rounded-lg text-lg text-platinum hover:text-light-bronze bg-cobalt hover:bg-deepcove hover:cursor-pointer"
>
{isPending ? 'Signing In...' : 'Submit'}
</button>
</form>
</div>
</div>
)
}
+148
View File
@@ -0,0 +1,148 @@
import classnames from 'classnames';
import { cookies } from "next/headers";
import { JWTPayload, jwtVerify } from "jose";
import DashboardHeader from "@/components/DashboardHeader";
import { Schibsted_Grotesk } from 'next/font/google'
import { redirect } from 'next/navigation';
import { isRedirectError } from 'next/dist/client/components/redirect-error';
const font = Schibsted_Grotesk({ subsets: ['latin'] })
const SECRET_KEY = new TextEncoder().encode(process.env.JWT_SECRET_KEY);
interface ClaimType {
claim_number: number;
visit_date: string;
claim_status: string;
patient_name: string;
provider_name: string;
}
interface UserJwtPayload extends JWTPayload {
user: UserData;
}
interface UserData {
id: string;
email: string;
name: string;
role: string;
roleId: string;
keychain: UserKeychain;
}
interface UserKeychain {
family_id: string;
mb_member_key: number;
pb_entity_key: number;
pl_plan_key: number;
};
export default async function MemberClaims() {
const cookieStore = await cookies();
const sessionCookie = cookieStore.get("session");
if (!sessionCookie) {
redirect('/account/signin')
}
try {
// 2. Extract the raw string token from the cookie object
const token = sessionCookie.value;
// 3. Verify and decode the JWT using jose
const { payload } = await jwtVerify(token, SECRET_KEY)
// 3. Cast the payload to your custom interface
const data = payload as UserJwtPayload;
// const userDataTest = {
// pb_entity_key: 377527,
// pl_plan_key: '61',
// name: "John McMember"
// }
const userData = {
pb_entity_key: data.user.keychain.pb_entity_key,
pl_plan_key: data.user.keychain.pl_plan_key,
name: data.user.name
}
const response = await fetch(`http://${process.env.BE_API_SERVER_HOST}:${process.env.BE_API_SERVER_PORT}/api/v1/members/${userData.pb_entity_key}/claims`, {
method: 'GET',
credentials: 'include',
headers: {
'Content-Type': 'application/json' ,
'Authorization': `Bearer ${data.token}`
},
});
if (!response.ok) throw new Error("Unauthenticated");
const claimsData: ClaimType[] = await response.json()
return (
<>
{claimsData &&
<div className="flex flex-col w-full pl-8 pt-5 2xl:pt-7">
<DashboardHeader
userDisplayName={userData.name}
/>
<div className="flex w-full pr-23">
<div className={classnames("w-full flex flex-col pt-4 z-2 space-x-4 space-y-4 xl:space-y-0 text-bluemana")}>
<div className="w-full flex text-3xl ml-2 mb-4 font-semibold">
<p>Claims</p>
</div>
<div className={classnames("w-full flex flex-col px-2 text-lg", font.className)}>
<div className='flex w-full mb-4 border-b border-bluemana'>
<div className="flex w-2/20">
Claim #
</div>
<div className="flex w-4/20 pl-3">
Patient Name
</div>
<div className="flex w-6/20 pl-3">
Provider
</div>
<div className="flex w-3/20 pl-3">
Visit Date
</div>
<div className="flex w-4/20 pl-8.5">
Claim Status
</div>
</div>
{(claimsData).map((mc) => (
<div key={mc.claim_number} className="flex w-full text-platinum">
<div className="flex w-2/20">
{mc.claim_number}
</div>
<div className="flex w-4/20 pl-3">
{mc.patient_name}
</div>
<div className="flex w-6/20 pl-3">
{mc.provider_name}
</div>
<div className="flex w-3/20 pl-3">
{mc.visit_date}
</div>
<div className="flex w-4/20 pl-8.5">
{mc.claim_status}
</div>
</div>
))}
</div>
</div>
</div>
</div>
}
</>
);
} catch (error: any) {
if (error.message === 'NEXT_REDIRECT') throw error;
if (error.message === 'Unauthenticated') {
redirect('/account/signin');
}
}
}
+137 -19
View File
@@ -1,31 +1,149 @@
import classnames from 'classnames';
import DashboardHeader from "@/components/DashboardHeader";
import FindProvider from '@/components/FindProvider';
import FindProviderWidget from '@/components/FindProvider';
import InsuranceCardWidget from '@/components/InsuranceCard';
import DeductableProgressWidget from '@/components/DeductableProgressWidget';
import FindRxInformationWidget from '@/components/FindRxInformation';
import RecentClaimsWidget from '@/components/RecentClaimsWidget';
import BenefitsAAGWidget from '@/components/BenefitsAAGWidget';
import { cookies } from "next/headers";
import { JWTPayload, jwtVerify } from "jose";
import { redirect } from 'next/navigation';
import IdCardViewer from '@/components/IdCardViewer';
export default function Member() {
return (
<div className="flex flex-col w-full pl-8 pt-5 2xl:pt-7">
<DashboardHeader
userDisplayName="Robert La'Blah"
/>
<div className="flex w-full pr-23">
<div className={classnames("w-full flex flex-col xl:flex-row pt-4 z-2 space-x-4 space-y-4 xl:space-y-0")}>
<div className="flex flex-col space-y-4">
<div className="flex flex-col w-full h-full xl:h-auto lg:flex-row space-y-4 lg:space-y-0 lg:space-x-4">
<DeductableProgressWidget />
<InsuranceCardWidget />
// Ensure your secret key matches exactly what you used to sign the token
const SECRET_KEY = new TextEncoder().encode(process.env.JWT_SECRET_KEY);
interface PlanBenefitType {
id: number;
title: string;
sequence: number;
benefit: string;
benefit_desc: string;
}
interface RecentClaimType {
claim_number: number;
visit_date: string;
claim_status: string;
patient_name: string;
provider_name: string;
}
interface DashboardDataType {
network_provider: string;
plan_benefits: PlanBenefitType[];
recent_claims: RecentClaimType[];
}
interface UserJwtPayload extends JWTPayload {
user: UserData;
}
interface UserData {
id: string;
email: string;
name: string;
role: string;
roleId: string;
keychain: UserKeychain;
}
interface UserKeychain {
family_id: string;
mb_member_key: number;
pb_entity_key: number;
pl_plan_key: number;
};
export default async function Member() {
const cookieStore = await cookies();
const sessionCookie = cookieStore.get("session"); // Replace with your cookie name
if (!sessionCookie) {
redirect('/account/signin')
}
try {
// 2. Extract the raw string token from the cookie object
const token = sessionCookie.value;
// 3. Verify and decode the JWT using jose
const { payload } = await jwtVerify(token, SECRET_KEY)
// 3. Cast the payload to your custom interface
const data = payload as UserJwtPayload;
// const userDataTest = {
// pb_entity_key: 377527,
// pl_plan_key: '61',
// name: "John McMember"
// }
const userData = {
pb_entity_key: data.user.keychain.pb_entity_key,
pl_plan_key: data.user.keychain.pl_plan_key,
name: data.user.name
}
const response = await fetch(`http://${process.env.BE_API_SERVER_HOST}:${process.env.BE_API_SERVER_PORT}/api/v1/members/${userData.pb_entity_key}/initialize_dashboard`, {
method: 'GET',
credentials: 'include',
headers: {
'Content-Type': 'application/json' ,
'Authorization': `Bearer ${data.token}`
},
});
if (!response.ok) throw new Error("Unauthenticated");
const dashboardData: DashboardDataType = await response.json()
return (
<>
{dashboardData &&
<div className="flex flex-col w-full pl-8 pt-5 2xl:pt-7">
<DashboardHeader
userDisplayName={userData.name}
/>
<div className="flex w-full pr-23">
<div className={classnames("w-full flex flex-col xl:flex-row pt-4 z-2 space-x-4 space-y-4 xl:space-y-0")}>
<div className="flex flex-col gap-y-4">
<div className="flex flex-col w-full h-full xl:h-auto lg:flex-row gap-y-4 lg:gap-y-0 lg:gap-x-4">
{/* <DeductableProgressWidget /> */}
<IdCardViewer
pbEntityKey={userData.pb_entity_key}
cardLaout="FullPageCard"
/>
<FindProviderWidget
// pl_plan_key='57'
networkProvider={dashboardData.network_provider}
/>
</div>
<FindRxInformationWidget />
<RecentClaimsWidget
recentClaims={dashboardData.recent_claims}
/>
</div>
<FindProvider />
<RecentClaimsWidget />
<BenefitsAAGWidget
planBenefits={dashboardData.plan_benefits}
/>
</div>
<BenefitsAAGWidget />
</div>
</div>
</div>
)
}
</>
);
} catch (error: any) {
if (error.message === 'NEXT_REDIRECT') throw error;
if (error.message === 'Unauthenticated') {
redirect('/account/signin');
}
}
// } catch (error) {
// // Fails if token is expired, tampered with, or invalid
// console.error("JWT verification failed:", error);
// return <div>Session invalid or expired.</div>;
// }
}
+1 -1
View File
@@ -19,7 +19,7 @@ export default function RootLayout({
return (
<html lang="en">
<body>
<main className={classNames("bg-deepcove min-h-screen flex justify-center", font.className)}>
<main className={classNames("bg-deepcove h-full min-h-screen flex justify-center", font.className)}>
<div className="w-full max-w-[1920px]">
{children}
</div>
+26 -46
View File
@@ -4,33 +4,11 @@ import InlineLink from "../components/InlineLink";
import Navbar from '@/components/Navbar';
import Footer from '@/components/Footer';
import { getClient } from "@/lib/graphqlClient";
import { gql } from "@apollo/client";
const HOME_GET_DATA_QUERY = gql`
query {
homePage {
headline
section1Header
section1Body
section1Cta
section2Header
section2Body
section2Cta
section3Header
section3Body
section3Cta
section4Header
section4Body
section4Cta
}
}
`;
export default async function Home() {
const { data } = await getClient().query({
query: HOME_GET_DATA_QUERY,
});
// const { data } = await getClient().query({
// query: HOME_GET_DATA_QUERY,
// });
return (
<>
@@ -41,40 +19,42 @@ export default async function Home() {
textTop="Healthcare that's"
textBottom="truly affordable"
/>
<div className="pt-4">
<div className="pt-10">
<InfoCard
title={data.homePage.section1Header}
learnMoreUrl={data.homePage.section1Cta}
imageUrl="/images/members-stock.jpg"
title="Members"
learnMoreUrl="/members"
imageUrl="/images/member-image.jpg"
>
{data.homePage.section1Body}
What we do for members
</InfoCard>
</div>
<div className='bg-atmosphere rounded-3xl'>
<InfoCard
title={data.homePage.section2Header}
learnMoreUrl={data.homePage.section2Cta}
imageUrl='/images/employers-stock.jpg'
title="Employers"
learnMoreUrl="/employers"
imageUrl='/images/employer-image.jpg'
invert
>
{data.homePage.section2Body}
What we do for employers
</InfoCard>
</div>
<InfoCard
title={data.homePage.section3Header}
learnMoreUrl={data.homePage.section3Cta}
imageUrl="/images/providers-stock.jpg"
>
{data.homePage.section3Body}
</InfoCard>
<div className='bg-platinum rounded-3xl'>
<div className="w-full">
<InfoCard
title={data.homePage.section4Header}
learnMoreUrl={data.homePage.section4Cta}
imageUrl="/images/brokers-stock.jpg"
title="Providers"
learnMoreUrl="/providers"
imageUrl="/images/provider-image.png"
>
What we do for providers
</InfoCard>
</div>
<div className='bg-light-bronze rounded-3xl'>
<InfoCard
title="Brokers"
learnMoreUrl="/brokers"
imageUrl="/images/broker-image.jpg"
invert
>
{data.homePage.section4Body}
What we do for brokers
</InfoCard>
</div>
<Footer />
+2 -2
View File
@@ -28,8 +28,8 @@ const AccountDropdown: React.FC<DropdownProps> = ({userRole}) => {
</MenuButton>
<MenuItems anchor="bottom end" className={"py-1 bg-deepcove border-2 border-copper focus:outline-hidden text-platinum text-md text-right font-semibold rounded-lg z-2"}>
<MenuItem as="div" className={"px-3 py-1.5"}>
<a className="block data-focus:text-bluemana hover:text-bluemana" href="/">
Change Password
<a className="block data-focus:text-bluemana hover:text-bluemana" href="/dashboard/member">
Dashboard
</a>
</MenuItem>
<MenuItem as="div" className={"px-3 py-1.5"}>
+28 -36
View File
@@ -3,50 +3,42 @@
import classnames from 'classnames';
import { Schibsted_Grotesk } from 'next/font/google'
import { useEffect, useState } from 'react';
const font = Schibsted_Grotesk({ subsets: ['latin'] })
interface BenefitsAAGWidgetProps {
planBenefits: PlanBenefitType[];
}
const BenefitsAAGWidget = () => {
interface PlanBenefitType {
id: number;
title: string;
sequence: number;
benefit: string;
benefit_desc: string;
}
const BenefitsAAGWidget: React.FC<BenefitsAAGWidgetProps> = ({ planBenefits }) => {
const planTitle = planBenefits[0]?.title || "Unknown Plan Benefits"
return (
<div className="2xl:min-w-[32vw] xl:min-w-[30vw] w-full flex flex-col justify-start items-center px-4 py-3 text-xl border border-6 border-platinum rounded-4xl">
<div className="2xl:min-w-[32vw] xl:min-w-[25vw] w-full flex flex-col justify-start items-center px-4 py-3 text-xl text-platinum border border-6 border-platinum rounded-4xl">
<p className="text-3xl underline py-2">Benefits At A Glance</p>
<p className={classnames("text-2xl font-semibold py-2", font.className)}>CLASSIC 5K 100/5000</p>
<div className={classnames("w-full flex justify-between", font.className)}>
<div className="flex flex-col">
<p>Physician Visit</p>
<p>Specialist Visit</p>
<p>Urgent Care</p>
<p>INN-Ind Ded</p>
<p>INN-Family Ded</p>
<p>OON-Ind Ded</p>
<p>OON-Family Ded</p>
<p>Co-Insurance</p>
<p>INN-Ind OOP</p>
<p>INN-Family OOP</p>
<p>OON-Ind OOP</p>
<p>OON-Family OOP</p>
<p>Emergency Room</p>
<p>Preventive Care</p>
</div>
<div className="flex flex-col">
<p>$25.00</p>
<p>$50.00</p>
<p>$50.00</p>
<p>$5,000</p>
<p>$10,000</p>
<p>$10,000</p>
<p>$20,000</p>
<p>100%/0%</p>
<p>$5,000</p>
<p>$10,000</p>
<p>$10,000</p>
<p>$20,000</p>
<p>Ded & Coin</p>
<p>100%</p>
</div>
<p className={classnames("text-2xl font-semibold py-2", font.className)}>{planTitle}</p>
<div className={classnames("w-full flex-col items-between px-4", font.className)}>
{(planBenefits).map((mb) => (
<div key={mb.sequence} className="flex items-center w-full">
<div className="flex-none">
{mb.benefit_desc}
</div>
<div className="flex-grow h-[1px] mx-1 mt-1 bg-verdigris"></div>
<div className="flex-none justify-end">
{mb.benefit}
</div>
</div>
))}
</div>
</div>
);
+3 -1
View File
@@ -5,16 +5,18 @@ import classnames from 'classnames';
interface ButtonProps {
onClick?: () => void;
disabled?: boolean;
children: React.ReactNode;
className?: string;
}
const Button: React.FC<ButtonProps> = ({ onClick, children, className }) => {
const Button: React.FC<ButtonProps> = ({ onClick, disabled, children, className }) => {
const [isHovered, setIsHovered] = useState(false);
return (
<button
onClick={onClick}
disabled={disabled}
className={classnames(
className, {
'cursor-pointer': isHovered,
+5 -1
View File
@@ -1,4 +1,7 @@
import classnames from 'classnames';
import { Schibsted_Grotesk } from 'next/font/google'
const font = Schibsted_Grotesk({ subsets: ['latin'] })
interface DashboardHeaderProps {
userDisplayName: string;
@@ -9,7 +12,8 @@ const DashboardHeader: React.FC<DashboardHeaderProps> = ({ userDisplayName }) =>
return (
<div className="w-full flex flex-col justify-start">
<div className={classnames(
"-mb-[34px] text-4xl text-left z-2 text-bluemana font-semibold text-shadow-[1px_1px_0,-1px_1px_0,-1px_-1px_0,1px_-1px_0] text-shadow-deepcove"
"-mb-[34px] ml-1 text-4xl text-left z-2 text-bluemana font-semibold text-shadow-[1px_1px_0,-1px_1px_0,-1px_-1px_0,1px_-1px_0] text-shadow-deepcove",
font.className
)}>
{userDisplayName}
</div>
+177
View File
@@ -0,0 +1,177 @@
'use client';
import { useState, useEffect, ChangeEvent } from 'react';
import Button from './Button';
interface SelectOption {
entity_key: string;
name: string;
}
interface EmployerMemberSelectProps {
onSelectionCompleteEmployer: (employerValue: string) => void;
onSelectionCompleteMember: (memberValue: string) => void;
userRole: 'employer' | 'broker' | 'carrier';
employerAllButtonText: string;
plPlanKey?: string;
employerName?: string;
brokerId?: number;
carrierId?: number;
}
const EmployerMemberSelect: React.FC<EmployerMemberSelectProps> = ({ onSelectionCompleteEmployer, onSelectionCompleteMember, userRole, employerAllButtonText, plPlanKey, employerName, brokerId, carrierId }) => {
const [optionsEmployer, setOptionsEmployer] = useState<SelectOption[]>([]);
const [selectedEmployer, setSelectedEmployer] = useState<string>('');
const [loadingEmployer, setLoadingEmployer] = useState<boolean>(false);
const [optionsMember, setOptionsMember] = useState<SelectOption[]>([]);
const [selectedMember, setSelectedMember] = useState<string>('');
const [loadingMember, setLoadingMember] = useState<boolean>(false);
const [apiUrl, setapiUrl] = useState("");
useEffect(() => {
fetch("/api/backendApiUrl")
.then((res) => res.json())
.then((data) => setapiUrl(data.apiUrl));
}, []);
useEffect(() => {
async function fetchOptionsEmployer(role: string, roleId: number) {
setLoadingEmployer(true);
try {
const response = await fetch(`http://${apiUrl}/api/v1/${role}/${roleId}/employers_list`, {
method: "GET",
});
const data = await response.json();
setOptionsEmployer(data);
} catch (error) {
console.error('Error fetching employer options:', error);
} finally {
setLoadingEmployer(false);
}
}
if (plPlanKey && employerName) {
setSelectedEmployer(plPlanKey)
setOptionsEmployer([{entity_key: plPlanKey, name: employerName}]);
} else if (brokerId) {
fetchOptionsEmployer("brokers", brokerId);
} else if (carrierId) {
fetchOptionsEmployer("carriers", carrierId);
}
}, [apiUrl]);
useEffect(() => {
if (!selectedEmployer) {
setOptionsMember([]);
setSelectedMember('');
return;
}
async function fetchOptionsMember() {
setLoadingMember(true);
try {
const response = await fetch(`http://${apiUrl}/api/v1/employers/${selectedEmployer}/members_list`, {
method: "GET",
});
const data = await response.json();
setOptionsMember(data);
setSelectedMember('');
} catch (error) {
console.error('Error fetching second options:', error);
} finally {
setLoadingMember(false);
}
}
fetchOptionsMember();
}, [selectedEmployer]);
const handleFirstChange = (e: ChangeEvent<HTMLSelectElement>) => {
const val = e.target.value;
setSelectedEmployer(val);
};
const handleAllEmployerSelect = () => {
if (selectedEmployer) {
onSelectionCompleteEmployer(selectedEmployer);
}
};
const handleSecondChange = (e: ChangeEvent<HTMLSelectElement>) => {
const val = e.target.value;
setSelectedMember(val);
// if (val) {
// onSelectionCompleteMember(val);
// }
};
const handleMemberSelect = () => {
if (selectedMember) {
onSelectionCompleteMember(selectedMember);
}
};
return (
<div className="flex flex-col gap-4 max-w-xs">
{/* First Select Dropdown */}
{!plPlanKey &&
<div className="flex flex-col gap-1">
<label className="text-sm font-medium">Employer</label>
<div className="flex">
<select
value={selectedEmployer}
onChange={handleFirstChange}
disabled={loadingEmployer}
className="border p-2 rounded"
>
<option value="">{loadingEmployer ? 'Loading...' : '-- Select Employer --'}</option>
{optionsEmployer.map((opt) => (
<option key={opt.entity_key} value={opt.entity_key}>
{opt.name}
</option>
))}
</select>
<Button
onClick={handleAllEmployerSelect}
>
<div className="flex justify-center items-center rounded-r-2">
{employerAllButtonText}
</div>
</Button>
</div>
</div>
}
{/* Second Select Dropdown */}
<div className="flex flex-col gap-1">
<label className="text-sm font-medium">Member</label>
<div className="flex">
<select
value={selectedMember}
onChange={handleSecondChange}
disabled={!selectedEmployer || loadingMember}
className="border p-2 rounded"
>
<option value="">
{!selectedEmployer
? '-- Select a category first --'
: loadingMember ? 'Loading...' : '-- Select Subcategory --'}
</option>
{optionsMember.map((opt) => (
<option key={opt.entity_key} value={opt.entity_key}>
{opt.name}
</option>
))}
</select>
<Button
onClick={handleAllEmployerSelect}
>
<div className="flex justify-center items-center rounded-r-2">
Select Member
</div>
</Button>
</div>
</div>
</div>
);
}
+43 -40
View File
@@ -1,57 +1,60 @@
"use client";
import classnames from 'classnames';
import Button from '@/components/Button';
import { MdLocationOn } from "react-icons/md";
import LocationSearchInput from '@/components/LocationSearchInput';
import Image from 'next/image';
import { Schibsted_Grotesk } from 'next/font/google'
const font = Schibsted_Grotesk({ subsets: ['latin'] })
interface FindProviderProps {
zipOnly?: boolean;
import Link from 'next/link';
interface FindProviderWidgetProps {
networkProvider: string;
}
const FindProvider: React.FC<FindProviderProps> = ({ zipOnly }) => {
interface NetworkProvider {
network_provider: string;
}
const FindProviderWidget: React.FC<FindProviderWidgetProps> = ({ networkProvider }) => {
return (
<div className="2xl:min-w-[48vw] xl:min-w-[45vw] flex z-1">
<div className="w-full flex flex-col px-4 py-3 border border-6 border-bluemana rounded-4xl">
<div className="w-full pb-2 flex justify-center text-2xl text-bronze font-semibold">
<p>Find A Provider By...</p>
<div className="2xl:min-w-[16vw] xl:min-w-[15vw] w-full h-full flex lg:flex-col justify-center items-center text-xl space-x-10 lg:space-x-0 border border-6 border-atmosphere rounded-4xl">
<div className="flex flex-col justify-end items-center mb-4">
{networkProvider &&
<div className="flex items-end mb-3">
<div className="text-platinum font-semibold">Find A {networkProvider} Provider</div>
</div>
<div className="w-full flex px-2">
<div className="w-full flex border-x-2 border-t-2 rounded-t-xl font-semibold divide-x-2 divide-bronze">
<a href="#" className="w-full h-9 flex justify-center items-center rounded-tl-lg bg-bluetang text-platinum">
Location
</a>
<a href="#" className="w-full h-9 flex justify-center items-center text-platinum hover:text-bronze">
Procedure
</a>
<a href="#" className="w-full h-9 flex justify-center items-center rounded-tr-lg text-platinum hover:text-bronze">
Provider/Facility
</a>
</div>
</div>
<div className={classnames("w-full flex items-start px-2 pb-2 text-xl", font.className)}>
{/* <p className='pr-3'><GrMapLocation size={30} /></p> */}
<Button className="h-10 text-deepcove hover:text-atmosphere bg-platinum rounded-bl-xl">
<MdLocationOn size={30} />
}
{networkProvider == "Cigna" &&
<Link href="http://cigna.com" target="_blank" rel="noopener noreferrer">
<Button className="flex items-center justify-center bg-cobalt rounded hover:bg-copper rounded-xl">
<Image
src="/images/Cigna_deepcove.svg"
alt="Cigna Logo"
width={150}
height={150}
priority
/>
</Button>
<LocationSearchInput className="h-10 pl-3 text-bluemana border-y-2 border-platinum" />
<Button className="h-10 pr-3 pl-2 bg-platinum hover:bg-bluemana text-center text-deepcove font-semibold rounded-br-xl border-y-2 border-r-2 border-platinum">
Search
</Link>
}
{networkProvider == "MedCost" &&
<Link href="http://medcost.com" target="_blank" rel="noopener noreferrer">
<Button className="flex items-center justify-center bg-cobalt rounded hover:bg-copper rounded-xl">
<Image
src="/images/MedCost_deepcove.svg"
alt="MedCost Logo"
width={150}
height={150}
priority
/>
</Button>
</div>
<div className="w-full flex justify-end text-platinum font-semibold">
<Button className="mr-2 underline hover:text-bronze">
View Full Provider List
</Button>
</div>
</div>
</Link>
}
</div>
</div>
);
};
export default FindProvider;
export default FindProviderWidget;
+57
View File
@@ -0,0 +1,57 @@
"use client";
import classnames from 'classnames';
import Button from '@/components/Button';
import { MdLocationOn } from "react-icons/md";
import LocationSearchInput from '@/components/LocationSearchInput';
import { Schibsted_Grotesk } from 'next/font/google'
const font = Schibsted_Grotesk({ subsets: ['latin'] })
interface FindProviderProps {
zipOnly?: boolean;
}
const FindProvider: React.FC<FindProviderProps> = ({ zipOnly }) => {
return (
<div className="2xl:min-w-[48vw] xl:min-w-[45vw] flex z-1">
<div className="w-full flex flex-col px-4 py-3 border border-6 border-bluemana rounded-4xl">
<div className="w-full pb-2 flex justify-center text-2xl text-bronze font-semibold">
<p>Find A Provider By...</p>
</div>
<div className="w-full flex px-2">
<div className="w-full flex border-x-2 border-t-2 rounded-t-xl font-semibold divide-x-2 divide-bronze">
<a href="#" className="w-full h-9 flex justify-center items-center rounded-tl-lg bg-bluetang text-platinum">
Location
</a>
<a href="#" className="w-full h-9 flex justify-center items-center text-platinum hover:text-bronze">
Procedure
</a>
<a href="#" className="w-full h-9 flex justify-center items-center rounded-tr-lg text-platinum hover:text-bronze">
Provider/Facility
</a>
</div>
</div>
<div className={classnames("w-full flex items-start px-2 pb-2 text-xl", font.className)}>
{/* <p className='pr-3'><GrMapLocation size={30} /></p> */}
<Button className="h-10 text-deepcove hover:text-atmosphere bg-platinum rounded-bl-xl">
<MdLocationOn size={30} />
</Button>
<LocationSearchInput className="h-10 pl-3 text-bluemana border-y-2 border-platinum" />
<Button className="h-10 pr-3 pl-2 bg-platinum hover:bg-bluemana text-center text-deepcove font-semibold rounded-br-xl border-y-2 border-r-2 border-platinum">
Search
</Button>
</div>
<div className="w-full flex justify-end text-platinum font-semibold">
<Button className="mr-2 underline hover:text-bronze">
View Full Provider List
</Button>
</div>
</div>
</div>
);
};
export default FindProvider;
+48
View File
@@ -0,0 +1,48 @@
"use client";
import classnames from 'classnames';
import Button from '@/components/Button';
import { FaRegIdCard } from "react-icons/fa";
import Image from 'next/image';
import { Schibsted_Grotesk } from 'next/font/google'
import { useEffect, useState } from 'react';
const font = Schibsted_Grotesk({ subsets: ['latin'] })
import Link from 'next/link';
const FindRxInformationWidget = () => {
return (
<div className="2xl:min-w-[32vw] xl:min-w-30vw] w-full h-full flex lg:flex-col justify-center items-center text-xl space-x-10 lg:space-x-0 border border-6 border-atmosphere rounded-4xl">
<div className="flex flex-col justify-end items-center">
<div className="flex items-end mb-3">
<div className="text-platinum font-semibold">Find Rx Information</div>
</div>
<Link href="https://www.fairosrx.com/" target="_blank" rel="noopener noreferrer" className='group mb-3'>
<Button className="block group-hover:hidden items-center justify-center bg-platinum rounded rounded-xl p-1">
<Image
src="/images/Fairosrx.svg"
alt="FairosRx Logo"
width={300}
height={150}
priority
/>
</Button>
<Button className="hidden group-hover:block items-center justify-center bg-platinum rounded rounded-xl p-1">
<Image
src="/images/Fairosrx_inverted.svg"
alt="Inverted FairosRx Logo"
width={300}
height={150}
priority
/>
</Button>
</Link>
</div>
</div>
);
};
export default FindRxInformationWidget;
+125
View File
@@ -0,0 +1,125 @@
"use client";
import { useState, useEffect } from "react";
interface IdCardDownloaderProps {
plPlanKey: number;
}
const IdCardDownloader: React.FC<IdCardDownloaderProps> = ({ plPlanKey }) => {
const [loading, setLoading] = useState(false);
const [apiUrl, setapiUrl] = useState("");
useEffect(() => {
fetch("/api/backendApiUrl")
.then((res) => res.json())
.then((data) => setapiUrl(data.apiUrl));
}, []);
const handleDownload = async () => {
setLoading(true);
try {
const response = await fetch(`http://${apiUrl}/api/v1/employers/${plPlanKey}/id_cards`, {
method: 'GET',
headers: {
'Accept': 'application/zip',
},
});
if (!response.ok) throw new Error('Failed to download');
const contentDisposition = response.headers.get('Content-Disposition');
console.log("contentDisposition: " + contentDisposition);
let fileName = 'employer_id_cards.zip'; // Default fallback name
// 2. Parse the filename using a regular expression
if (contentDisposition) {
const filenameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
if (filenameMatch && filenameMatch[1]) {
// Remove surrounding quotes if they exist
fileName = filenameMatch[1].replace(/['"]/g, '');
}
}
console.log(fileName)
// Convert the response to a Blob
const blob = await response.blob();
// Create a temporary object URL
const url = window.URL.createObjectURL(blob);
// Create an invisible link and click it
const link = document.createElement('a');
link.href = url;
link.download = fileName;
document.body.appendChild(link);
link.click();
// Clean up memory
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
} catch (error) {
console.error('Download error:', error);
} finally {
setLoading(false);
}
};
return (
<div className="flex flex-col items-center justify-center">
<button
onClick={handleDownload}
disabled={loading}
className="text-platinum underline hover:text-bluemana focus:outline-none"
>
Download Employer Cards
</button>
{(loading) && (
<div className="fixed inset-x-1/6 inset-y-1/12 flex items-center justify-center w-2/3 h-[80vh] bg-atmosphere border-2 border-platinum shadow-md rounded-lg z-2">
{loading && (
<div className="flex flex-col items-center">
{/* Spinner */}
<svg
className="animate-spin h-20 w-20 text-deepcove mb-2"
xmlns="http://w3.org"
fill="none"
viewBox="0 0 24 24"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-45" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<p className="text-deepcove text-lg font-medium">Generating ID Cards...</p>
</div>
)}
</div>
)}
{/* Loading Animation */}
{/* {loading && (
<div className="flex h-[70vh] w-screen items-center gap-2 text-gray-500 animate-pulse z-2">
<svg className="animate-spin h-10 w-10 text-blue-600" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
Fetching ID Card, Please Wait...
</div>
)} */}
{/* Display PDF */}
{/* {pdfUrl && (
<div className="w-screen h-[70vh] border border-gray-300 rounded shadow-lg overflow-hidden z-2">
<iframe
src={pdfUrl}
title="PDF Document"
className="w-full h-full"
/>
</div>
)} */}
</div>
);
}
export default IdCardDownloader;
+150
View File
@@ -0,0 +1,150 @@
"use client";
import { useState, useEffect } from "react";
import Button from "./Button";
import { FaRegIdCard } from "react-icons/fa";
interface IdCardViewerProps {
cardLaout: 'FullPageCard' | 'MobileDisplayCard';
pbEntityKey: number;
}
const IdCardViewer: React.FC<IdCardViewerProps> = ({ cardLaout, pbEntityKey }) => {
const [loading, setLoading] = useState(false);
const [pdfUrl, setPdfUrl] = useState<string | null>(null);
const [apiUrl, setapiUrl] = useState("");
useEffect(() => {
fetch("/api/backendApiUrl")
.then((res) => res.json())
.then((data) => setapiUrl(data.apiUrl));
}, []);
const handleFetchPdf = async () => {
setLoading(true);
setPdfUrl(null);
try {
const response = await fetch(`http://${apiUrl}/api/v1/members/${pbEntityKey}/id_card/${cardLaout}`, {
method: "GET",
});
if (!response.ok) throw new Error("Failed to fetch PDF");
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
setPdfUrl(url);
} catch (error) {
console.error("Error fetching PDF:", error);
} finally {
setLoading(false);
}
};
const handleClose = () => {
if (pdfUrl) {
URL.revokeObjectURL(pdfUrl);
}
setPdfUrl(null);
};
return (
<div className="2xl:min-w-[16vw] xl:min-w-[15vw] w-full h-full flex lg:flex-col justify-center items-center text-xl space-x-10 lg:space-x-0 border border-6 border-atmosphere rounded-4xl">
<div className="flex flex-col justify-end items-center mb-4">
<div className="flex items-end mb-3">
<div className="text-platinum font-semibold">View Temporary ID Card</div>
</div>
<div className="flex items-end">
<div className="text-deepcove -mb-11 font-semibold z-1">My Card</div>
</div>
<Button
onClick={handleFetchPdf}
disabled={loading}
className="focus:outline-none"
>
<FaRegIdCard size={150} className="text-bluemana hover:text-light-bronze" />
</Button>
</div>
{/* <div className="flex flex-col items-center justify-center">
<Button
onClick={handleFetchPdf}
disabled={loading}
className="font-semibold text-platinum underline hover:text-light-bronze focus:outline-none"
>
View ID Card
</Button> */}
{(loading || pdfUrl) && (
<div className="fixed inset-x-1/6 inset-y-1/12 flex items-center justify-center w-2/3 h-[80vh] bg-atmosphere border-2 border-platinum shadow-md rounded-lg z-2">
{loading && (
<div className="flex flex-col items-center">
{/* Spinner */}
<svg
className="animate-spin h-20 w-20 text-deepcove mb-2"
xmlns="http://w3.org"
fill="none"
viewBox="0 0 24 24"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-45" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<p className="text-deepcove text-lg font-medium">Generating ID Card...</p>
</div>
)}
{pdfUrl && (
<div className="flex flex-col w-full h-full bg-platinum rounded-lg shadow-xl overflow-hidden flex flex-col">
{/* Top Bar with Close Button */}
<div className="w-full h-14 bg-atmosphere text-deepcove flex justify-between items-center px-6">
<span className="font-bold text-sm truncate">ID Card Preview</span>
<Button className="px-3 py-2 bg-bluetang text-platinum text-sm font-semibold rounded hover:bg-atmosphere hover:text-bluetang border-2 border-bluetang" onClick={handleClose}>
Close
</Button>
{/* <button
onClick={handleClose}
className="px-3 py-2 bg-bluetang text-platinum text-sm font-semibold rounded hover:bg-atmosphere hover:text-bluetang border-2 border-bluetang"
>
Close
</button> */}
</div>
<iframe
src={pdfUrl}
className="w-full h-full"
title="ID Card"
/>
</div>
)}
</div>
)}
{/* Loading Animation */}
{/* {loading && (
<div className="flex h-[70vh] w-screen items-center gap-2 text-gray-500 animate-pulse z-2">
<svg className="animate-spin h-10 w-10 text-blue-600" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
Fetching ID Card, Please Wait...
</div>
)} */}
{/* Display PDF */}
{/* {pdfUrl && (
<div className="w-screen h-[70vh] border border-gray-300 rounded shadow-lg overflow-hidden z-2">
<iframe
src={pdfUrl}
title="PDF Document"
className="w-full h-full"
/>
</div>
)} */}
</div>
);
}
export default IdCardViewer;
+5 -5
View File
@@ -41,14 +41,14 @@ const InfoCard: React.FC<InfoCardProps> = (
<div className='flex m-h-150 justify-center py-10'>
<div className='flex flex-col w-11/12'>
<div className='flex flex-col w-full items-center'>
<div className={classnames('flex rounded-2xl shadow-copper/90',
<div className={classnames('flex w-full rounded-2xl shadow-platinum/50',
{
"flex-row-reverse relative" : invert,
"shadow-[8px_8px_6px,2px_2px_2px_-1px]" : !invert
})}>
<div className={classnames({
"hidden" : !invert,
"after:absolute after:bottom-0 after:right-0 after:w-full after:h-[calc(100%-(var(--spacing)*23.5))] after:rounded-2xl after:shadow-[8px_8px_6px,2px_2px_2px_-1px] after:shadow-copper/90" : invert,
"after:absolute after:bottom-0 after:right-0 after:w-full after:h-[calc(100%-(var(--spacing)*23.5))] after:rounded-2xl after:shadow-[8px_8px_6px,2px_2px_2px_-1px] after:shadow-platinum/50" : invert,
})}/>
<div className={classnames("flex flex-col w-7/12")}>
<div className={classnames(
@@ -61,7 +61,7 @@ const InfoCard: React.FC<InfoCardProps> = (
"relative w-full pt-8 mb-2 ml-8 mr-100 border-b-3",
{
"border-atmosphere" : !invert,
"border-bronze" : invert
"border-platinum" : invert
}
)}>
<div className={classnames(
@@ -105,7 +105,7 @@ const InfoCard: React.FC<InfoCardProps> = (
</div>
</div>
<div className={classnames(
"w-2/9 relative flex justify-center pb-4 text-center rounded-b-3xl rounded-out-t-2xl shadow-[8px_8px_6px,2px_2px_2px_-1px] shadow-copper/90",
"w-2/9 relative flex justify-center pb-4 text-center rounded-b-3xl rounded-out-t-2xl shadow-[8px_8px_6px,2px_2px_2px_-1px] shadow-platinum/50",
cardBGColor,
{
"ml-[calc(2/9*100%)]" : !invert,
@@ -114,7 +114,7 @@ const InfoCard: React.FC<InfoCardProps> = (
)}>
<Link href={learnMoreUrl ? learnMoreUrl : '/'}
className={classnames(
"py-1 px-4 rounded text-xl text-semibold border-2 text-bronze hover:text-copper border-bronze hover:border-copper",
"py-1 px-4 rounded text-xl text-semibold border-2 hover:text-platinum text-bluemana hover:border-platinum border-bluemana",
)}
>
Learn More
+49 -11
View File
@@ -4,29 +4,67 @@ import classnames from 'classnames';
import Button from '@/components/Button';
import { FaRegIdCard } from "react-icons/fa";
import IdCardViewer from '@/components/IdCardViewer';
import { Schibsted_Grotesk } from 'next/font/google'
import IdCardDownloader from './IdCardDownloader';
const font = Schibsted_Grotesk({ subsets: ['latin'] })
const InsuranceCardWidget = () => {
interface InsuranceCardWidgettProps {
pbEntityKey?: number;
plPlanKey?: number;
}
const InsuranceCardWidget: React.FC<InsuranceCardWidgettProps> = ({ pbEntityKey, plPlanKey }) => {
return (
<div className="2xl:min-w-[16vw] xl:min-w-[15vw] w-full h-full flex lg:flex-col justify-center items-center text-xl space-x-10 lg:space-x-0 border border-6 border-atmosphere rounded-4xl z-1">
<div className="2xl:min-w-[16vw] xl:min-w-[15vw] w-full h-full flex lg:flex-col justify-center items-center text-md space-x-10 lg:space-x-0 border border-6 border-atmosphere rounded-4xl">
<div className="flex flex-col justify-end items-center">
<div className="z-3 flex items-end">
<div className="text-deepcove -mb-11 font-semibold">My Card</div>
<div className="flex items-end">
<div className="text-deepcove -mb-11 font-semibold z-1">My Card</div>
</div>
<FaRegIdCard size={150} className="text-bluemana" />
</div>
<div className={classnames("flex justify-between items-center space-x-1", font.className)}>
<Button className="hover:text-bronze">
<div className={classnames("flex w-full items-center space-x-1", font.className)}>
{/* <Button className="hover:text-bronze">
View
</Button>
<p>|</p>
<Button className="hover:text-bronze">
</Button> */}
{/* <div className={classnames("flex w-[45%] justify-end")}>
<IdCardViewer
pbEntityKey={pbEntityKey}
cardLaout="FullPageCard"
/>
</div> */}
{/* <div className={classnames("flex w-[10%] justify-center")}>
|
</div> */}
{/* <p>|</p> */}
{/* <div className={classnames("flex w-[45%] justify-start")}>
<IdCardViewer
pbEntityKey={pbEntityKey}
cardLaout="MobileDisplayCard"
/>
</div> */}
{/* <Button className="hover:text-bronze">
Download
</Button>
</Button> */}
</div>
</div>
{pbEntityKey &&
<div className={classnames("flex w-[45%] justify-center")}>
<IdCardViewer
pbEntityKey={pbEntityKey}
cardLaout="FullPageCard"
/>
</div>
}
{plPlanKey &&
<div className={classnames("flex w-full justify-center mb-4")}>
<IdCardDownloader
plPlanKey={plPlanKey}
/>
</div>
}
</div>
);
};
+10 -5
View File
@@ -145,11 +145,11 @@ const Navbar: React.FC<NavbarProps> = ({ collapseHeight }) => {
Brokers
</Link>
</li>
<li>
{/* <li>
<Link href="/" className="hover:text-bluemana">
Services
</Link>
</li>
</li> */}
</ul>
{/* <div className={classnames("flex space-x-4 font-semibold text-lg", {"pl-2" : isScrolled})}>
{menuItems.map((item) => {
@@ -164,10 +164,15 @@ const Navbar: React.FC<NavbarProps> = ({ collapseHeight }) => {
})}
</div> */}
<div className={classnames("flex space-x-2 font-semibold", {"pt-1" : isScrolled})}>
<Link href="/dashboard/member">
<button className="bg-platinum text-copper hover:text-bronze py-1 px-4 rounded border-1 border-copper hover:border-bronze">
{/* <Link href="/account/signup">
<Button className="bg-platinum text-copper hover:text-bronze py-1 px-4 rounded border-1 border-copper hover:border-bronze">
Sign Up
</Button>
</Link> */}
<Link href="/account/signin">
<Button className="bg-platinum text-copper hover:text-bronze py-1 px-4 rounded border-1 border-copper hover:border-bronze">
Sign In
</button>
</Button>
</Link>
{/* <Button href="" className="bg-platinum text-copper hover:text-bronze py-1 px-4 rounded border-1 border-copper hover:border-bronze">
Sign In
+52 -65
View File
@@ -4,88 +4,75 @@ import classnames from 'classnames';
import Button from '@/components/Button';
import { Schibsted_Grotesk } from 'next/font/google'
import { useEffect, useState } from 'react';
import Link from 'next/link';
const font = Schibsted_Grotesk({ subsets: ['latin'] })
interface RecentClaimsWidgetProps {
recentClaims: RecentClaimType[];
}
const RecentClaimsWidget = () => {
interface RecentClaimType {
claim_number: number;
visit_date: string;
claim_status: string;
patient_name: string;
provider_name: string;
}
const RecentClaimsWidget: React.FC<RecentClaimsWidgetProps> = ({ recentClaims }) => {
return (
<div className="2xl:min-w-[48vw] xl:min-w-[45vw] flex z-1">
<div className="w-full flex flex-col px-4 py-3 border border-6 border-bronze rounded-4xl space-y-2">
<div className="2xl:min-w-[48vw] xl:min-w-[50vw] flex z-1">
<div className="w-full flex flex-col px-4 py-3 text-platinum border border-6 border-bronze rounded-4xl space-y-2">
<div className="w-full flex justify-center text-2xl text-atmosphere font-semibold">
<p>Recent Claims</p>
</div>
<div className={classnames("w-full flex flex-col px-2 text-lg", font.className)}>
<div className="flex">
<div className="w-1/6">
<Button className="hover:text-bronze text-bluemana">
2907457
</Button>
<div className={classnames("w-full flex flex-col px-2 text-sm", font.className)}>
<div className='flex w-full mb-4 border-b border-bluemana'>
<div className="flex w-2/20">
Claim #
</div>
<div className="w-2/6">
LA'BLAH, ROBERT
<div className="flex w-4/20 pl-3">
Patient Name
</div>
<div className="w-2/6">
HOUSE, GREGORY
<div className="flex w-6/20 pl-3">
Provider
</div>
<div className="w-1/6">
07/21/2025
<div className="flex w-3/20 pl-3">
Visit Date
</div>
<div className="flex w-4/20 pl-8.5">
Claim Status
</div>
</div>
<div className="flex">
<div className="w-1/6">
<Button className="hover:text-bronze text-bluemana">
2907201
</Button>
{(recentClaims ?? []).map((mc) => (
<div key={mc.claim_number} className="flex w-full">
<div className="flex w-2/20">
{mc.claim_number}
</div>
<div className="flex w-4/20 pl-3">
{mc.patient_name}
</div>
<div className="flex w-6/20 pl-3">
{mc.provider_name}
</div>
<div className="flex w-3/20 pl-3">
{mc.visit_date}
</div>
<div className="flex justify-end w-4/20">
{mc.claim_status}
</div>
</div>
<div className="w-2/6">
LA'BLAH, ROBERT
</div>
<div className="w-2/6">
QUINN, MICHAELA
</div>
<div className="w-1/6">
07/02/2025
</div>
</div>
<div className="flex">
<div className="w-1/6">
<Button className="hover:text-bronze text-bluemana">
2905891
</Button>
</div>
<div className="w-2/6">
LA'BLAH, JENNY
</div>
<div className="w-2/6">
HOWSER, DOGGIE
</div>
<div className="w-1/6">
05/15/2025
</div>
</div>
<div className="flex">
<div className="w-1/6">
<Button className="hover:text-bronze text-bluemana">
2905547
</Button>
</div>
<div className="w-2/6">
LA'BLAH, ROBERT
</div>
<div className="w-2/6">
HOUSE, GREGORY
</div>
<div className="w-1/6">
04/29/2025
</div>
</div>
))}
</div>
<div className="w-full flex justify-end text-platinum font-semibold">
<Button className="mr-2 underline hover:text-bronze">
View All Claims
<Button className="mr-2 underline hover:text-light-bronze">
<Link
href="/dashboard/member/claims"
>
View All Claims
</Link>
</Button>
</div>
</div>
+6 -5
View File
@@ -11,6 +11,7 @@ import { TbCircleArrowLeft, TbCircleArrowRight } from "react-icons/tb";
import { PiBreadFill } from "react-icons/pi";
import Button from './Button';
import { usePathname } from 'next/navigation';
interface SideNavProps {
@@ -39,18 +40,18 @@ const SideNav: React.FC<SideNavProps> = (
}
)}>
<div className="h-25" />
<Button
<div className="h-20" />
{/* <Button
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
className="h-10 flex justify-end items-center pr-2"
>
{isSidebarOpen ? <FaArrowCircleLeft size={30} /> : <FaArrowCircleRight size={30} />}
</Button>
</Button> */}
<div className="flex flex-col divide-y divide-bluetang">
{navItems.map((navItem, index) => (
<a href={navItem.url} className={classnames(
<a href={navItem.url} key={index} className={classnames(
"w-full h-20 flex justify-center items-center px-1 text-xs md:text-sm lg:text-base font-semibold hover:text-platinum",
{"bg-bluetang text-platinum" : index == 0}
{"bg-bluetang text-platinum" : usePathname() == navItem.url}
)}>
{isSidebarOpen ? (
<>{navItem.title}</>
+82
View File
@@ -0,0 +1,82 @@
"use client";
import { useState, useEffect, useRef } from "react";
import { useIdleTimer } from "react-idle-timer";
import { signOut, useSession } from "next-auth/react"; // Or use your custom auth hook
const TIMEOUT_MINUTES = 15; // Log out after 15 minutes of inactivity
const WARNING_SECONDS = 60; // Show warning 1 minute before logout
export default function IdleTimerContainer() {
const { data: session } = useSession();
const [showWarning, setShowWarning] = useState(false);
const [remainingTime, setRemainingTime] = useState(0);
const idleTimerRef = useRef(null);
// Triggered when user becomes idle but before logging out (for a warning modal)
const onPrompt = () => {
// getRemainingTime is provided by react-idle-timer
const time = Math.floor(idleTimerRef.current.getRemainingTime() / 1000);
setRemainingTime(time);
setShowWarning(true);
};
// Triggered when the user is completely idle and timer runs out
const onIdle = () => {
setShowWarning(false);
signOut({ callbackUrl: "/login" }); // Redirects to login after logging out
};
const onActive = () => {
setShowWarning(false);
};
const { getRemainingTime, getLastActiveTime } = useIdleTimer({
timeout: 1000 * 60 * TIMEOUT_MINUTES,
promptTimeout: 1000 * 60 * WARNING_SECONDS,
onPrompt,
onIdle,
onActive,
debounce: 500,
});
useEffect(() => {
let interval;
if (showWarning) {
interval = setInterval(() => {
setRemainingTime((prev) => {
if (prev <= 1) {
clearInterval(interval);
return 0;
}
return prev - 1;
});
}, 1000);
}
return () => clearInterval(interval);
}, [showWarning]);
// Don't render anything if the user isn't logged in or no warning is active
if (!session || !showWarning) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
<div className="bg-white p-6 rounded-lg shadow-xl text-center max-w-sm w-full mx-4">
<h2 className="text-lg font-semibold text-gray-800">Session Timeout Warning</h2>
<p className="mt-2 text-sm text-gray-600">
You will be automatically logged out in <b>{remainingTime} seconds</b> due to inactivity.
</p>
<p className="mt-1 text-xs text-gray-500">
Click anywhere or press any key to stay logged in.
</p>
<button
onClick={() => idleTimerRef.current?.activate()}
className="mt-4 px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded hover:bg-blue-700 w-full"
>
Stay Logged In
</button>
</div>
</div>
);
}
+28 -28
View File
@@ -13,38 +13,38 @@ export const sideNavItems: sideNavGroups = {
"member": [
{
title: "Dashboard",
url: "/",
url: "/dashboard/member",
iconName: "LuLayoutDashboard"
},
// {
// title: "My Plan & Benefits",
// url: "/",
// iconName: "LuClipboardList"
// },
// {
// title: "Plan Utilization",
// url: "/",
// iconName: "TbProgress"
// },
{
title: "My Plan & Benefits",
url: "/",
iconName: "LuClipboardList"
},
{
title: "Plan Utilization",
url: "/",
iconName: "TbProgress"
},
{
title: "Claims & Authorizations",
url: "/",
title: "Claims",
url: "/dashboard/member/claims",
iconName: "BsClipboardCheck"
},
{
title: "Providers & Services",
url: "/",
iconName: "FaStethoscope"
},
{
title: "Insurance Card",
url: "/",
iconName: "FaRegIdCard"
},
{
title: "Benefits Documents",
url: "/",
iconName: "IoDocumentsOutline"
}
// {
// title: "Providers & Services",
// url: "/",
// iconName: "FaStethoscope"
// },
// {
// title: "Insurance Card",
// url: "/",
// iconName: "FaRegIdCard"
// },
// {
// title: "Benefits Documents",
// url: "/",
// iconName: "IoDocumentsOutline"
// }
]
}
+6
View File
@@ -21,6 +21,12 @@
--color-copper: #B06E30;
/* bronze */
--color-bronze: #D38F4A;
/* bronze */
--color-light-bronze: #CD7F32;
/* cobalt blue */
--color-cobalt: #0047AB;
/* oxidized copper/bronze green-blue */
--color-verdigris: #618D94;
}
@theme inline {
+17 -4
View File
@@ -1,7 +1,11 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@@ -19,9 +23,18 @@
}
],
"paths": {
"@/*": ["./src/*"]
"@/*": [
"./src/*"
]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}