The Publish tab is where you deploy your screener to a publicly
diff --git a/builder-frontend/src/components/project/manageBenefits/ManageBenefits.tsx b/builder-frontend/src/components/project/manageBenefits/ManageBenefits.tsx
index 2bde5cdf..aa1c16a6 100644
--- a/builder-frontend/src/components/project/manageBenefits/ManageBenefits.tsx
+++ b/builder-frontend/src/components/project/manageBenefits/ManageBenefits.tsx
@@ -4,24 +4,28 @@ import { useParams } from "@solidjs/router";
import BenefitList from "./benefitList/BenefitList";
import ConfigureBenefit from "./configureBenefit/ConfigureBenefit";
-
const ManageBenefits = () => {
const params = useParams();
- const [benefitIdToConfigure, setBenefitIdToConfigure] = createSignal(null);
+ const [benefitIdToConfigure, setBenefitIdToConfigure] = createSignal<
+ null | string
+ >(null);
return (
-
- {
- benefitIdToConfigure() === null && (
-
params.projectId} setBenefitIdToConfigure={setBenefitIdToConfigure}/>
- )
- }
- {
- benefitIdToConfigure() !== null && (
- params.projectId} benefitId={benefitIdToConfigure} setBenefitId={setBenefitIdToConfigure}/>
- )
- }
+
+ {benefitIdToConfigure() === null && (
+ params.projectId}
+ setBenefitIdToConfigure={setBenefitIdToConfigure}
+ />
+ )}
+ {benefitIdToConfigure() !== null && (
+ params.projectId}
+ benefitId={benefitIdToConfigure}
+ setBenefitId={setBenefitIdToConfigure}
+ />
+ )}
);
-}
+};
export default ManageBenefits;
diff --git a/builder-frontend/src/components/shared/ANavbar.css b/builder-frontend/src/components/shared/ANavbar.css
new file mode 100644
index 00000000..17197ec4
--- /dev/null
+++ b/builder-frontend/src/components/shared/ANavbar.css
@@ -0,0 +1,11 @@
+@reference "tailwindcss";
+
+.navbarlink {
+ @apply px-4 py-2 no-underline border-b-2 hover:bg-gray-200 transition-colors;
+}
+.navbarlink.inactive {
+ @apply border-transparent text-gray-500 hover:text-gray-700;
+}
+.navbarlink.active {
+ @apply border-b border-gray-700 text-gray-700;
+}
diff --git a/builder-frontend/src/components/shared/ANavbar.tsx b/builder-frontend/src/components/shared/ANavbar.tsx
new file mode 100644
index 00000000..9a22eb7a
--- /dev/null
+++ b/builder-frontend/src/components/shared/ANavbar.tsx
@@ -0,0 +1,21 @@
+import type { Component } from "solid-js";
+import { A } from "@solidjs/router";
+
+import "./ANavbar.css";
+
+interface Props {
+ items: { label: string; href: string }[];
+}
+const ANavBar: Component = (props) => {
+ return (
+
+ {props.items.map(({ label, href }) => (
+
+ {label}
+
+ ))}
+
+ );
+};
+
+export default ANavBar;
diff --git a/builder-frontend/src/components/shared/Button.module.css b/builder-frontend/src/components/shared/Button.module.css
new file mode 100644
index 00000000..699d9488
--- /dev/null
+++ b/builder-frontend/src/components/shared/Button.module.css
@@ -0,0 +1,48 @@
+@reference "tailwindcss";
+
+.button {
+ @apply inline-flex py-1.5 px-4 rounded border-2 disabled:opacity-50;
+}
+.button:hover {
+ @apply cursor-pointer;
+}
+
+.primary {
+ @apply border-(--brand) bg-(--brand) text-white;
+}
+.primary:not([disabled]):hover {
+ @apply brightness-110;
+}
+.outline-primary {
+ @apply border-(--brand) bg-white text-(--brand);
+}
+.outline-primary:not([disabled]):hover {
+ @apply border-(--brand)/70 bg-(--brand)/30;
+}
+
+.secondary {
+ @apply border-(--text-strong) bg-(--text-strong) text-white;
+}
+.secondary:not([disabled]):hover {
+ @apply brightness-200;
+}
+.outline-secondary {
+ @apply border-(--text-strong) bg-white text-(--text-strong);
+}
+.outline-secondary:not([disabled]):hover {
+ @apply border-(--text-strong)/70 bg-(--text-strong)/30;
+}
+
+.tertiary {
+ @apply border-(--text-strong) bg-(--text-strong) text-white;
+}
+
+.danger {
+ @apply border-red-400 bg-red-400 text-white;
+}
+.outline-danger {
+ @apply border-red-400 bg-white text-red-400;
+}
+.outline-danger:not([disabled]):hover {
+ @apply bg-red-100;
+}
diff --git a/builder-frontend/src/components/shared/Button.tsx b/builder-frontend/src/components/shared/Button.tsx
new file mode 100644
index 00000000..e7324c44
--- /dev/null
+++ b/builder-frontend/src/components/shared/Button.tsx
@@ -0,0 +1,28 @@
+import { Component, JSX, ParentProps } from "solid-js";
+
+import styles from "./Button.module.css";
+
+interface Props extends JSX.ButtonHTMLAttributes {
+ variant?:
+ | "primary"
+ | "secondary"
+ | "tertiary"
+ | "danger"
+ | "outline-primary"
+ | "outline-secondary"
+ | "outline-tertiary"
+ | "outline-danger";
+}
+
+export const Button: Component> = (props) => {
+ const { type, children, ...rest } = props;
+ return (
+
+ {props.children}
+
+ );
+};
diff --git a/builder-frontend/src/components/shared/ComponentLibrary.css b/builder-frontend/src/components/shared/ComponentLibrary.css
new file mode 100644
index 00000000..5a3859c2
--- /dev/null
+++ b/builder-frontend/src/components/shared/ComponentLibrary.css
@@ -0,0 +1,12 @@
+@reference 'tailwindcss';
+
+.library-wrapper > * {
+ @apply p-5;
+}
+.library-wrapper > *:nth-child(even) {
+ @apply bg-amber-100;
+}
+
+.my-button {
+ @apply border-1 border-red-500;
+}
diff --git a/builder-frontend/src/components/shared/ComponentLibrary.tsx b/builder-frontend/src/components/shared/ComponentLibrary.tsx
new file mode 100644
index 00000000..b61205c0
--- /dev/null
+++ b/builder-frontend/src/components/shared/ComponentLibrary.tsx
@@ -0,0 +1,124 @@
+import { createSignal, For, JSX } from "solid-js";
+
+import { Button } from "@/components/shared/Button";
+import Form from "@/components/shared/Form";
+import { Modal } from "@/components/shared/Modal";
+
+import "@/components/shared/ComponentLibrary.css";
+
+export const ComponentLibrary = () => {
+ const handleSubmitForm: JSX.EventHandler = (
+ e,
+ ) => {
+ e.preventDefault();
+ alert("Form submitted!");
+ };
+ return (
+
+
Component Library
+
+ Button styled using CSS
+
+
+ Primary button component
+ Secondary button component
+ Tertiary button component
+ Danger button component
+
+
+ Primary button component
+ Secondary button component
+ Tertiary button component
+ Danger button component
+
+
+
+ Form with placeholder text that moves above text input when focused or
+ has value.
+
+
+
+
+
+
+
+
+
+
+
+ Submit
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const ModalForm = () => {
+ const [showModal, setShowModal] = createSignal(false);
+ const handleSubmitForm: JSX.EventHandler = (
+ e,
+ ) => {
+ e.preventDefault();
+ alert("Submitted the form from the modal!");
+ setShowModal(false);
+ };
+ return (
+ <>
+ setShowModal(true)}>
+ Open modal with form
+
+ setShowModal(false)}>
+ Enter the required information in the form
+
+
+
+
+
+ Submit
+
+
+
+ >
+ );
+};
+
+const SelectComponent = () => {
+ const dataTypes: { label: string; value: string }[] = [
+ { label: "String", value: "string" },
+ { label: "Number", value: "number" },
+ { label: "Boolean", value: "boolean" },
+ ];
+
+ return (
+
+
+ {(dataType) => {
+ return {dataType.label} ;
+ }}
+
+
+
+ );
+};
diff --git a/builder-frontend/src/components/shared/Form/Context.tsx b/builder-frontend/src/components/shared/Form/Context.tsx
new file mode 100644
index 00000000..4ae1093c
--- /dev/null
+++ b/builder-frontend/src/components/shared/Form/Context.tsx
@@ -0,0 +1,18 @@
+import { Accessor, createContext, Setter, useContext } from "solid-js";
+
+type FormContextValue = {
+ value: Accessor;
+ setValue: Setter;
+ htmlFor?: string;
+};
+export const FormContext = createContext();
+
+export const useFormContext = () => {
+ const context = useContext(FormContext);
+ if (!context) {
+ throw new Error(
+ "useFormContext must be used within a FormContext provider",
+ );
+ }
+ return context;
+};
diff --git a/builder-frontend/src/components/shared/Form/Form.module.css b/builder-frontend/src/components/shared/Form/Form.module.css
new file mode 100644
index 00000000..0036a5f2
--- /dev/null
+++ b/builder-frontend/src/components/shared/Form/Form.module.css
@@ -0,0 +1,64 @@
+@reference "tailwindcss";
+
+.textfield,
+.input-wrapper {
+ @apply relative;
+}
+.textfield .textfield-input,
+.textfield-above .textfield-input,
+.input-wrapper .select {
+ @apply border-2 border-slate-400 rounded-sm;
+}
+.textfield:hover .textfield-input,
+.textfield-above:hover .textfield-input,
+.input-wrapper:hover .select {
+ @apply border-slate-500;
+}
+.textfield-above .textfield-input:disabled {
+ @apply bg-slate-200 border-slate-300;
+}
+.textfield-above:has(.textfield-input:disabled) > .textfield-label {
+ @apply text-slate-400;
+}
+.textfield-above {
+ @apply relative;
+ flex-direction: column;
+ margin-bottom: 1rem;
+}
+.textfield-label {
+ @apply py-2;
+}
+.textfield-above > .textfield-label,
+.input-wrapper > .textfield-label {
+ @apply absolute top-0 left-0 p-4 text-slate-500 pointer-events-none;
+}
+.textfield-above:has(.textfield-input:focus) {
+ @apply border-slate-800;
+}
+.textfield-above > .textfield-input:focus ~ .textfield-label,
+.textfield-above > .textfield-input:not(:placeholder-shown) ~ .textfield-label,
+.input-wrapper > .select ~ .textfield-label {
+ transform: scale(0.8) translateY(-1.5rem);
+ transform-origin: 1rem;
+}
+.textfield-input {
+ @apply w-full p-4 focus:outline-0 text-(--text-strong);
+}
+
+.select {
+ @apply w-full p-4;
+}
+
+.radio-wrapper {
+ @apply flex items-center;
+}
+.radio-button {
+ @apply mr-1 w-4 h-4;
+}
+.radio-button:checked {
+ @apply accent-(--brand);
+}
+
+.form-error {
+ @apply text-red-500 pb-4;
+}
diff --git a/builder-frontend/src/components/shared/Form/Form.tsx b/builder-frontend/src/components/shared/Form/Form.tsx
new file mode 100644
index 00000000..bb8e227e
--- /dev/null
+++ b/builder-frontend/src/components/shared/Form/Form.tsx
@@ -0,0 +1,118 @@
+import { Component, createSignal, JSX, splitProps, useContext } from "solid-js";
+
+import styles from "./Form.module.css";
+import { Select } from "./Select";
+import { Radio } from "@/components/shared/Form/Radio";
+import { FormContext, useFormContext } from "@/components/shared/Form/Context";
+
+interface FormProps extends JSX.FormHTMLAttributes {}
+interface FormWrapperProps extends JSX.HTMLAttributes {
+ htmlFor: string;
+ value?: string;
+}
+interface LabelWrapperProps extends JSX.HTMLAttributes {
+ htmlFor: string;
+ placeholder: string;
+}
+interface TextInputProps extends JSX.InputHTMLAttributes {
+ value?: string;
+ placeholder?: string;
+}
+
+interface LabelProps extends JSX.LabelHTMLAttributes {}
+interface ErrorProps extends JSX.HTMLAttributes {}
+
+const mergeClasses = (...classes: (string | undefined)[]): string => {
+ return classes.filter((c) => c !== undefined && c.length > 0).join(" ");
+};
+
+const TextInput = (props: TextInputProps) => {
+ // const [value, setValue] = createSignal(props.value || "");
+ const [local, rest] = splitProps(props, ["class", "placeholder"]);
+ const ctx = useContext(FormContext);
+ if (!ctx) {
+ throw new Error(" must be used with a FormContext.");
+ }
+ const showPlaceholder =
+ ctx.value().length < 1 && (local.placeholder || "").length > 0;
+
+ return (
+ ctx.setValue(e.target.value)}
+ placeholder={showPlaceholder ? local.placeholder : ""}
+ {...rest}
+ />
+ );
+};
+
+const Label = (props: LabelProps) => {
+ const [local, rest] = splitProps(props, ["class"]);
+ const ctx = useFormContext();
+
+ return (
+
+ );
+};
+
+const FormError = (props: ErrorProps) => {
+ const [local, rest] = splitProps(props, ["class"]);
+ return (
+
+ );
+};
+
+/**
+ * `` element with integrated ``.
+ * Accepts a `children` prop that contains ``
+ */
+
+const LabelAbove = (props: LabelWrapperProps) => {
+ const [local, rest] = splitProps(props, [
+ "children",
+ "htmlFor",
+ "placeholder",
+ ]);
+ return (
+
+ {local.children}
+ {local.placeholder}
+
+ );
+};
+const FormWrapper = (props: FormWrapperProps) => {
+ const [local, rest] = splitProps(props, ["htmlFor", "value"]);
+ const [value, setValue] = createSignal(local.value ?? "");
+
+ return (
+
+
+
+ );
+};
+
+const Form: Component = (props) => {
+ return ;
+};
+
+export default Object.assign(Form, {
+ Label,
+ TextInput,
+ FormWrapper,
+ LabelAbove,
+ FormError,
+ Select,
+ Radio,
+});
diff --git a/builder-frontend/src/components/shared/Form/Radio.tsx b/builder-frontend/src/components/shared/Form/Radio.tsx
new file mode 100644
index 00000000..be30ee03
--- /dev/null
+++ b/builder-frontend/src/components/shared/Form/Radio.tsx
@@ -0,0 +1,26 @@
+import { useFormContext } from "@/components/shared/Form/Context";
+import { JSX, splitProps } from "solid-js";
+
+import styles from "./Form.module.css";
+
+interface RadioProps extends JSX.InputHTMLAttributes {}
+
+export const Radio = (props: RadioProps) => {
+ const ctx = useFormContext();
+ const [local, rest] = splitProps(props, ["class", "children", "value"]);
+ return (
+
+ ctx.setValue(e.currentTarget.value)}
+ {...rest}
+ />
+ {local.children}
+
+ );
+};
diff --git a/builder-frontend/src/components/shared/Form/Select.tsx b/builder-frontend/src/components/shared/Form/Select.tsx
new file mode 100644
index 00000000..08924fc0
--- /dev/null
+++ b/builder-frontend/src/components/shared/Form/Select.tsx
@@ -0,0 +1,37 @@
+import { For, JSX, splitProps } from "solid-js";
+
+import styles from "./Form.module.css";
+
+interface SelectProps extends JSX.SelectHTMLAttributes {
+ id: string;
+ label: string;
+ options: { label: string; value: string }[];
+}
+
+export const Select = (props: SelectProps) => {
+ const [local, rest] = splitProps(props, [
+ "children",
+ "id",
+ "label",
+ "options",
+ "value",
+ ]);
+
+ return (
+
+
+ {local.children}
+
+ {(opt) => (
+
+ {opt.label}
+
+ )}
+
+
+
+ {local.label}
+
+
+ );
+};
diff --git a/builder-frontend/src/components/shared/Form/index.tsx b/builder-frontend/src/components/shared/Form/index.tsx
new file mode 100644
index 00000000..5e2f96ac
--- /dev/null
+++ b/builder-frontend/src/components/shared/Form/index.tsx
@@ -0,0 +1,3 @@
+import Form from "./Form";
+
+export default Form;
diff --git a/builder-frontend/src/components/shared/Modal.module.css b/builder-frontend/src/components/shared/Modal.module.css
new file mode 100644
index 00000000..ca286ec9
--- /dev/null
+++ b/builder-frontend/src/components/shared/Modal.module.css
@@ -0,0 +1,28 @@
+@reference "tailwindcss";
+
+.modal-wrapper {
+ @apply fixed top-0 left-0 h-full w-full bg-black/10 backdrop-blur-sm;
+ z-index: 10;
+ overflow-y: auto;
+}
+.modal-content {
+ @apply bg-white rounded-xl mx-auto my-24 w-1/2;
+ position: relative;
+ padding: 2rem;
+ width: fit-content;
+ min-width: 50%;
+}
+.modal-header {
+ display: flex;
+ justify-content: flex-end;
+}
+.modal-close {
+ @apply w-fit text-gray-500;
+}
+.modal-close:hover,
+.modal-close:hover * {
+ @apply text-gray-800 cursor-pointer;
+}
+body:has(.modal-wrapper) {
+ overflow-y: hidden;
+}
diff --git a/builder-frontend/src/components/shared/Modal.tsx b/builder-frontend/src/components/shared/Modal.tsx
new file mode 100644
index 00000000..7d0db47f
--- /dev/null
+++ b/builder-frontend/src/components/shared/Modal.tsx
@@ -0,0 +1,36 @@
+import { Accessor, Component, createEffect, ParentProps, Show } from "solid-js";
+import { Portal } from "solid-js/web";
+import { CircleX } from "lucide-solid";
+
+import styles from "./Modal.module.css";
+
+interface Props {
+ show: boolean;
+ onClose: () => void;
+}
+export const Modal: Component> = (props) => {
+ return (
+
+
+ props.onClose()}>
+
e.stopPropagation()}
+ >
+
+
{props.children}
+
+
+
+
+ );
+};
diff --git a/builder-frontend/src/index.css b/builder-frontend/src/index.css
index 677d6e36..a8c2a7b9 100644
--- a/builder-frontend/src/index.css
+++ b/builder-frontend/src/index.css
@@ -3,15 +3,22 @@
strategy: "class"; /* only generate form-* classes */
}
+:root {
+ --brand: var(--color-sky-600);
+ --text-strong: #2d2d2d;
+ --text-weak: #6d6d6d;
+ --bg-green: #f5fef6;
+ --bg-yellow: #fff1c2;
+ --bg-red: #fff6f5;
+}
+
body {
- @apply
- text-gray-800
+ @apply text-gray-800;
}
@layer components {
.btn-default {
- @apply
- inline-block px-5 py-1
- cursor-pointer select-none !rounded-md text-sm font-semibold
+ @apply inline-block px-5 py-1
+ cursor-pointer select-none !rounded-md text-sm font-semibold;
}
.btn-default.btn-gray {
@apply border-gray-300 border-2 bg-white hover:bg-gray-200 text-gray-800;
@@ -24,7 +31,7 @@ body {
@apply bg-yellow-700 hover:bg-yellow-800 text-white;
}
.btn-default.btn-blue {
- @apply bg-sky-600 hover:bg-sky-700 text-white
+ @apply bg-sky-600 hover:bg-sky-700 text-white;
}
.eligibility-check-table-header {
diff --git a/builder-frontend/src/index.tsx b/builder-frontend/src/index.tsx
index 2b5e2265..2267d220 100644
--- a/builder-frontend/src/index.tsx
+++ b/builder-frontend/src/index.tsx
@@ -2,23 +2,25 @@
import { render } from "solid-js/web";
import { Router } from "@solidjs/router";
-import { Toaster } from 'solid-toast';
+import { Toaster } from "solid-toast";
import { AuthProvider } from "./context/AuthContext.jsx";
import App from "./App";
import "./index.css";
+import { MetaProvider, Title } from "@solidjs/meta";
const root = document.getElementById("root");
render(
() => (
-
-
-
+
+ Benefit Decision Toolkit - Home
+
+
-
-
+
+
),
- root
+ root,
);
diff --git a/builder-frontend/src/types.ts b/builder-frontend/src/types.ts
index 5a7c95d1..c23cc491 100644
--- a/builder-frontend/src/types.ts
+++ b/builder-frontend/src/types.ts
@@ -1,4 +1,4 @@
-import type { JSONSchema7 } from 'json-schema';
+import type { JSONSchema7 } from "json-schema";
/* Types for managing benefits in a project */
export interface ScreenerBenefits {
@@ -82,32 +82,12 @@ export interface UpdateCheckParametersRequest {
// Parameter Types
export type ParameterType = "string" | "number" | "boolean" | "date" | "array";
-export type ParameterDefinition =
- | StringParameter
- | StringMultiInputParameter
- | NumberParameter
- | BooleanParameter
- | DateParameter;
-interface BaseParameter {
+export type ParameterDefinition = {
key: string;
label: string;
+ type: "string" | "number" | "boolean" | "date" | "array";
required: boolean;
-}
-export interface StringMultiInputParameter extends BaseParameter {
- type: "array";
-}
-export interface StringParameter extends BaseParameter {
- type: "string";
-}
-export interface NumberParameter extends BaseParameter {
- type: "number";
-}
-export interface BooleanParameter extends BaseParameter {
- type: "boolean";
-}
-export interface DateParameter extends BaseParameter {
- type: "date";
-}
+};
/* Screener Evaluation Results */
export interface ScreenerResult {
@@ -144,4 +124,4 @@ export interface PublishedScreener {
export interface FormPath {
path: string;
type: string;
-}
\ No newline at end of file
+}
diff --git a/builder-frontend/vite.config.js b/builder-frontend/vite.config.js
index 1abff710..726e585c 100644
--- a/builder-frontend/vite.config.js
+++ b/builder-frontend/vite.config.js
@@ -1,34 +1,35 @@
import { defineConfig } from "vite";
import solid from "vite-plugin-solid";
-import tsconfigPaths from 'vite-tsconfig-paths'
+import tsconfigPaths from "vite-tsconfig-paths";
+import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
- plugins: [solid(), tsconfigPaths()],
+ plugins: [tailwindcss(), solid(), tsconfigPaths()],
server: {
port: process.env.DEV_SERVER_PORT || 5173,
proxy: {
- '/identitytoolkit.googleapis.com': {
- target: 'http://localhost:9099',
+ "/identitytoolkit.googleapis.com": {
+ target: "http://localhost:9099",
},
- '/securetoken.googleapis.com': {
- target: 'http://localhost:9099',
+ "/securetoken.googleapis.com": {
+ target: "http://localhost:9099",
},
- '/emulator': {
- target: 'http://localhost:9099',
+ "/emulator": {
+ target: "http://localhost:9099",
},
- '/__/auth': {
- target: 'http://localhost:9099',
+ "/__/auth": {
+ target: "http://localhost:9099",
},
- '/api': {
- target: 'http://localhost:8081',
+ "/api": {
+ target: "http://localhost:8081",
},
},
},
optimizeDeps: {
// Ensure Preact is not pre-bundled separately, avoiding duplicate instances
- include: ['preact', 'preact/hooks', 'preact/compat']
+ include: ["preact", "preact/hooks", "preact/compat"],
},
resolve: {
- dedupe: ['preact', 'preact/hooks', 'preact/compat']
- }
+ dedupe: ["preact", "preact/hooks", "preact/compat"],
+ },
});