diff --git a/db/comalyr.svg b/db/comalyr.svg new file mode 100644 index 0000000..800796f --- /dev/null +++ b/db/comalyr.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/server/db/db.js b/server/db/db.js index 0b00f6b..ea184f0 100644 --- a/server/db/db.js +++ b/server/db/db.js @@ -3,6 +3,7 @@ import chalk from 'chalk'; import path from 'path'; import Titles from "./model/Titles.js" +import Networks from "./model/Networks.js" import Members from './model/Members.js' import Tokens from './model/Tokens.js' import Payments from "./model/Payments.js" @@ -12,6 +13,7 @@ import Messages from "./model/Messages/Messages.js" export default class Database { titles = new Titles() + networks = new Networks() members = new Members() tokens = new Tokens() payments = new Payments() @@ -20,8 +22,9 @@ export default class Database { messages = new Messages() fromID = { - "HY": this.titles, "MEMBER": this.members, + "NETWORK": this.networks, + "TITLE": this.titles, "TOKEN": this.tokens, "PAYMENT": this.payments, "POST": this.posts, diff --git a/server/db/model/Members.js b/server/db/model/Members.js index a0f0b6b..7de4f51 100644 --- a/server/db/model/Members.js +++ b/server/db/model/Members.js @@ -22,6 +22,7 @@ export default class Members extends OrderedObject { ), joined: z.string(), address: this.addressSchema, + networks: z.array(z.number()) }) isHashed = (s) => {return s.startsWith("$argon2")} diff --git a/server/db/model/Networks.js b/server/db/model/Networks.js new file mode 100644 index 0000000..e544ee8 --- /dev/null +++ b/server/db/model/Networks.js @@ -0,0 +1,39 @@ +import OrderedObject from "./OrderedObject.js" +import { z } from 'zod'; + +export default class Networks extends OrderedObject { + prefix = `NETWORK` + + schema = z.object({ + id: z.number(), + name: z.string(), + }) + + save(n) { + let id = `${this.prefix}-${n.id}` + let result = this.schema.safeParse(n) + if(result.success) { + try { + super.add(id, n) + } catch(e) { + console.error(e) + throw e + } + } else { + console.error(result.error) + throw new global.ServerError(400, "Invalid Member Data!"); + } + } + + add(n) { + let toSave = { + id: this.entries.length+1, + ...n + } + this.save(toSave) + } + + get(id) { + return this.entries[this.ids[`${this.prefix}-${id}`]] + } +} \ No newline at end of file diff --git a/server/index.js b/server/index.js index 18db27c..0557a11 100644 --- a/server/index.js +++ b/server/index.js @@ -24,7 +24,7 @@ class Server { registerRoutes(router) { /* Stripe */ - router.post("/create-checkout-session", PaymentsHandler.danceTicket) + router.post("/create-checkout-session", PaymentsHandler.newSubscription) router.post("/webhook", express.raw({ type: "application/json" }), PaymentsHandler.webhook) /* Auth */ @@ -33,7 +33,6 @@ class Server { router.get('/signout', this.auth.logout) /* Site */ - router.get('/signup', this.verifyToken, this.get) router.post('/signup', this.verifyToken, this.newUserSubmission) router.get('/db/images/*', this.getUserImage) router.get('/*', this.get) diff --git a/server/payments.js b/server/payments.js index ad3ef54..1f71f7a 100644 --- a/server/payments.js +++ b/server/payments.js @@ -6,22 +6,22 @@ const stripe = new Stripe(process.env.STRIPE_SECRET); export default class PaymentsHandler { - static async danceTicket(req, res) { + static async newSubscription(req, res) { try { const session = await stripe.checkout.sessions.create({ mode: "payment", payment_method_types: ["card"], metadata: { - productId: "austin_winter_ball_2025_ticket" + productId: "50_month_sub" }, line_items: [ { price_data: { currency: "usd", product_data: { - name: "Hyperia Winter Ball" + name: "Monthly Subscription" }, - unit_amount: 3500 + unit_amount: 5000 }, quantity: 1 } diff --git a/ui/_/code/shared.css b/ui/_/code/shared.css index c241044..5edf8de 100644 --- a/ui/_/code/shared.css +++ b/ui/_/code/shared.css @@ -5,7 +5,8 @@ --gold: #FEBA7D; --divider: #bb7c36; --green: #0857265c; - --red: #E84343; + --red: #CE0000; + --redorange: #ff2e00; --brown: #812A18; --darkbrown: #3f0808; @@ -14,12 +15,18 @@ @media (prefers-color-scheme: dark) { :root { - --main: var(--brown); - --accent: var(--gold); + --main: #000000; + --accent: #453C33; --accent2: var(--gold); } } +:root.red { + --main: var(--red); + --accent: #6a0000; + --accent2: var(--green); +} + @font-face { font-family: 'Canterbury'; src: url('/_/fonts/Canterbury/Canterbury.ttf') format('truetype'); @@ -84,6 +91,7 @@ a:active { } button { + font-family: inherit; background-color: transparent; color: var(--accent); padding: 0.5em; @@ -102,6 +110,7 @@ input { } input::placeholder { + font-family: "Nanum"; color: var(--accent) } diff --git a/ui/_/icons/forum.svg b/ui/_/icons/forum.svg new file mode 100644 index 0000000..eddf4cb --- /dev/null +++ b/ui/_/icons/forum.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/ui/_/icons/house.svg b/ui/_/icons/house.svg new file mode 100644 index 0000000..f2e6f86 --- /dev/null +++ b/ui/_/icons/house.svg @@ -0,0 +1,4 @@ + + + + diff --git a/ui/_/icons/nodes.svg b/ui/_/icons/nodes.svg new file mode 100644 index 0000000..b04a17a --- /dev/null +++ b/ui/_/icons/nodes.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/ui/_/icons/people.svg b/ui/_/icons/people.svg new file mode 100644 index 0000000..9ec5c7f --- /dev/null +++ b/ui/_/icons/people.svg @@ -0,0 +1,3 @@ + + + diff --git a/ui/_/icons/quill.svg b/ui/_/icons/quill.svg index 065e6a5..1bace50 100644 --- a/ui/_/icons/quill.svg +++ b/ui/_/icons/quill.svg @@ -2,4 +2,4 @@ - + \ No newline at end of file diff --git a/ui/_/icons/quillblack.svg b/ui/_/icons/quillblack.svg new file mode 100644 index 0000000..aa36be4 --- /dev/null +++ b/ui/_/icons/quillblack.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/ui/desktop/apps/Forum/Forum.js b/ui/desktop/apps/Forum/Forum.js index 0a26881..0f7fc26 100644 --- a/ui/desktop/apps/Forum/Forum.js +++ b/ui/desktop/apps/Forum/Forum.js @@ -87,8 +87,8 @@ class Forum extends Shadow { .gap(0.5, em) .width(100, pct) .height(100, vh) - .alignHorizontal("center") - .alignVertical("end") + .horizontalAlign("center") + .verticalAlign("end") }) .width(100, "%") .height(87, vh) diff --git a/ui/desktop/apps/Messages/Messages.js b/ui/desktop/apps/Messages/Messages.js index dd288a6..350c880 100644 --- a/ui/desktop/apps/Messages/Messages.js +++ b/ui/desktop/apps/Messages/Messages.js @@ -72,8 +72,8 @@ class Messages extends Shadow { }) .gap(1, em) .width(100, pct) - .alignHorizontal("center") - .alignVertical("end") + .horizontalAlign("center") + .verticalAlign("end") }) .onAppear(async () => { let res = await Socket.send({app: "MESSAGES", operation: "GET"}) @@ -121,8 +121,8 @@ class Messages extends Shadow { }) .gap(1, em) - .alignVertical("center") - .alignHorizontal("center") + .verticalAlign("center") + .horizontalAlign("center") .backgroundColor("black") .border("1px solid var(--accent)") .position("fixed") diff --git a/ui/desktop/components/AppMenu.js b/ui/desktop/components/AppMenu.js index 96a507e..141f62a 100644 --- a/ui/desktop/components/AppMenu.js +++ b/ui/desktop/components/AppMenu.js @@ -25,9 +25,8 @@ css(` border-radius: 5px; text-underline-offset: 5px; } - app-menu p:hover { - text-decoration: underline; - transform: translateY(-5%) + app-menu img:hover { + border: "1px solid black"; } app-menu p.touched { text-decoration: underline; @@ -56,19 +55,14 @@ class AppMenu extends Shadow { render() { VStack(() => { HStack(() => { - p("Forum") - p("Messages") - p("Market") - p("Jobs") + img("/_/icons/house.svg", "1.5em") + img("/_/icons/nodes.svg", "1.5em") + img("/_/icons/forum.svg", "1.5em") + img("/_/icons/people.svg", "1.5em") }) .justifyContent("center") - .gap(1.5, em) + .gap(3, em) .paddingRight(2, em) - - img("/_/images/divider.svg", "40vw") - .attr({ - "id": "divider", - }) }) .gap(0.5, em) .onNavigate(() => { @@ -81,14 +75,14 @@ class AppMenu extends Shadow { } }) .onAppear(() => { - Array.from(this.querySelectorAll("p")).forEach((el) => { + Array.from(this.querySelectorAll("img")).forEach((el) => { el.addEventListener("mousedown", (e) => { el.classList.add("touched") }) }) window.addEventListener("mouseup", (e) => { let target = e.target - if(!target.matches("app-menu p")) { + if(!target.matches("app-menu img")) { return } diff --git a/ui/desktop/components/Home.js b/ui/desktop/components/Home.js index 54fe664..9ab2f5f 100644 --- a/ui/desktop/components/Home.js +++ b/ui/desktop/components/Home.js @@ -8,23 +8,17 @@ class Home extends Shadow { render() { ZStack(() => { - img("/_/icons/logo.svg", "2.5em") - .position("fixed") - .left(3, em) - .top(3, vh) - .zIndex(3) - .onClick(() => { - window.navigateTo("/") - }) - - div() - .width(100, vw) - .height(100, vh) - .margin(0) - .backgroundImage("/_/images/the_return.webp") - .backgroundSize("cover") - .backgroundPosition("48% 65%") - .backgroundRepeat("no-repeat") + + VStack(() => { + img(document.documentElement.classList.contains("red") ? "/_/icons/quillblack.svg" : "/_/icons/quill.svg", "2.5em") + .position("fixed") + .left(3, em) + .top(3, vh) + .zIndex(3) + .onClick(() => { + window.navigateTo("/") + }) + }) switch(window.location.pathname) { case "/": @@ -82,7 +76,7 @@ class Home extends Shadow { .gap(1, em) .xRight(2, em).y(2.3, em) .position("fixed") - .alignVertical("center") + .verticalAlign("center") }) } diff --git a/ui/desktop/index.html b/ui/desktop/index.html index fe8fbe8..7d7cc78 100644 --- a/ui/desktop/index.html +++ b/ui/desktop/index.html @@ -3,7 +3,7 @@ Parchment - + diff --git a/ui/mobile/apps/Forum/Forum.js b/ui/mobile/apps/Forum/Forum.js index 8ee2f0b..a507efc 100644 --- a/ui/mobile/apps/Forum/Forum.js +++ b/ui/mobile/apps/Forum/Forum.js @@ -52,8 +52,8 @@ class Forum extends Shadow { .gap(0.5, em) .width(100, pct) .height(100, vh) - .alignHorizontal("center") - .alignVertical("end") + .horizontalAlign("center") + .verticalAlign("end") }) .onAppear(() => document.body.style.backgroundColor = "var(--darkbrown)") .width(100, pct) diff --git a/ui/mobile/apps/Messages/Messages.js b/ui/mobile/apps/Messages/Messages.js index dd288a6..350c880 100644 --- a/ui/mobile/apps/Messages/Messages.js +++ b/ui/mobile/apps/Messages/Messages.js @@ -72,8 +72,8 @@ class Messages extends Shadow { }) .gap(1, em) .width(100, pct) - .alignHorizontal("center") - .alignVertical("end") + .horizontalAlign("center") + .verticalAlign("end") }) .onAppear(async () => { let res = await Socket.send({app: "MESSAGES", operation: "GET"}) @@ -121,8 +121,8 @@ class Messages extends Shadow { }) .gap(1, em) - .alignVertical("center") - .alignHorizontal("center") + .verticalAlign("center") + .horizontalAlign("center") .backgroundColor("black") .border("1px solid var(--accent)") .position("fixed") diff --git a/ui/public/pages/Events.js b/ui/public/pages/Events.js deleted file mode 100644 index 15612d5..0000000 --- a/ui/public/pages/Events.js +++ /dev/null @@ -1,325 +0,0 @@ -class Events extends Shadow { - - events = [ - { - date: `January 23, 2025`, - title: `Hyperia Winter Ball`, - description: `Join us in Austin, Texas for a dance. Live music and drinks will be included.
Admission for men is $50, women are free. Open to the public.`, - location: `Austin, TX` - } - ] - - render() { - ZStack(() => { - VStack(() => { - - h1("HYPERIA") - .marginBottom(0, em) - - p("Public Events") - .fontSize(1.2, em) - .marginBottom(2, em) - - const Stack = window.isMobile() ? VStack : HStack - Stack(() => { - - VStack(() => { - p(`January 23, 2025`) - - p(`Hyperia Winter Ball`) - .fontSize(1.2, em) - - p(`Austin, TX`) - - }) - - p(`Join us in Austin, Texas for a great dance, with free drinks and live music.

Admission: $35 for men, women are free.`) - .marginRight(4, em) - - HStack(() => { - img("/_/icons/creditcards/visa.svg") - img("/_/icons/creditcards/mastercard.svg") - img("/_/icons/creditcards/discover.svg") - img("/_/icons/creditcards/amex.svg") - }) - .alignSelf("flex-start") - .height(2, em) - .maxWidth(40, vw) - - button("Buy Ticket") - .color("var(--darkbrown") - .border("1px solid #ab2f007d") - .background('var(--green)') - .marginLeft("auto") - .onClick(async function() { - this.innerText = "Loading..." - const res = await fetch("/create-checkout-session", { method: "POST" }); - const data = await res.json(); - window.location = data.url; - }) - }) - .gap(3, em) - .color("var(--darkbrown)") - .background(`var(--accent)`) - .padding(1, em) - .borderRadius(12, px) - .border("2px solid #ab2f007d") - }) - .marginLeft(window.isMobile() ? 0 : 15, vmax) - .marginRight(window.isMobile() ? 0 : 15, vmax) - .marginTop(10, vmax) - - HStack(() => { - p("Privacy Policy") - .onHover(function (hovering) { - if(hovering) { - this.style.color = "var(--darkbrown)" - } else { - this.style.color = "" - } - }) - .onClick(() => { - this.$("#policyWindow").style.display = "flex" - }) - p("Refund and Return Policy") - .onHover(function (hovering) { - if(hovering) { - this.style.color = "var(--darkbrown)" - } else { - this.style.color = "" - } - }) - .onClick(() => { - this.$("#refundWindow").style.display = "flex" - }) - p("Contact Us") - .onHover(function (hovering) { - if(hovering) { - this.style.color = "var(--darkbrown)" - } else { - this.style.color = "" - } - }) - .onClick(() => { - this.$("#contactWindow").style.display = "flex" - }) - }) - .x(50, vw).yBottom(0, vh) - .center() - .gap(2, em) - .opacity(0.5) - .cursor("default") - }) - - VStack(() => { - - p("Privacy Policy") - .fontSize(2, em) - .fontWeight(600) - .marginBottom(1, em) - - p("We value your privacy. This Privacy Policy explains how we collect, use, store, and protect your information when you use our website or services.") - - p("1. Information We Collect") - .fontWeight(600) - .marginTop(1, em) - - p("• Personal information you provide, such as your name, email address, or other contact details.") - p("• Automatically collected data, including IP address, browser type, device information, and usage statistics.") - p("• Cookies or similar tracking technologies that help us improve the user experience.") - - p("2. How We Use Your Information") - .fontWeight(600) - .marginTop(1, em) - - p("• To operate and improve our website and services.") - p("• To communicate with you about updates, support requests, or account-related matters.") - p("• To maintain security, prevent fraud, and ensure proper functionality.") - - p("3. How We Share Information") - .fontWeight(600) - .marginTop(1, em) - - p("We do not sell your personal information. We may share data only with trusted service providers who help us operate the platform, or when required by law.") - - p("4. Data Storage & Security") - .fontWeight(600) - .marginTop(1, em) - - p("We use reasonable technical and administrative safeguards to protect your information. However, no system is completely secure, and we cannot guarantee absolute protection.") - - p("5. Cookies") - .fontWeight(600) - .marginTop(1, em) - - p("Our site may use cookies to remember preferences, analyze traffic, and enhance usability. You can disable cookies in your browser settings, but some features may stop working.") - - p("6. Your Rights") - .fontWeight(600) - .marginTop(1, em) - - p("Depending on your location, you may have rights to access, update, delete, or request a copy of your personal data. Contact us if you wish to exercise these rights.") - - p("7. Third-Party Links") - .fontWeight(600) - .marginTop(1, em) - - p("Our website may contain links to third-party sites. We are not responsible for their content or privacy practices.") - - p("8. Changes to This Policy") - .fontWeight(600) - .marginTop(1, em) - - p("We may update this Privacy Policy from time to time. Updated versions will be posted on this page with the effective date.") - - p("9. Contact Us") - .fontWeight(600) - .marginTop(1, em) - - p("If you have any questions about this Privacy Policy, feel free to contact us at info@hyperia.so.") - - p("x") - .onClick(function (done) { - if(done) { - this.parentElement.style.display = "none" - } - }) - .color("var(--red)") - .xRight(1, em).y(1, em) - .fontSize(2, em) - .cursor("pointer") - - }) - .x(50, vw).y(50, vh) - .width(70, vw).height(70, vh) - .center() - .backgroundColor("var(--accent)") - .display("none") - .overflow("scroll") - .padding(1, em) - .border("3px solid black") - .color("var(--darkbrown)") - .attr({ id: "policyWindow" }) - - VStack(() => { - - p("Refund & Return Policy") - .fontSize(2, em) - .fontWeight(600) - .marginBottom(1, em) - - p("1. Eligibility for Refunds") - .fontWeight(600) - .marginTop(1, em) - - p("• Refund requests may be considered when submitted within 14 days of purchase.") - p("• To qualify, you must provide proof of purchase and a valid reason for the request.") - p("• Certain digital products or services may be non-refundable once accessed or downloaded.") - - p("2. Non-Refundable Items") - .fontWeight(600) - .marginTop(1, em) - - p("• Products or services that have already been delivered, downloaded, or accessed in full.") - p("• Custom work, personalized items, or one-time service fees.") - p("• Any promotional or discounted items, unless required by law.") - - p("3. Returns (If Applicable)") - .fontWeight(600) - .marginTop(1, em) - - p("• Physical items must be returned in their original condition.") - p("• You are responsible for return shipping costs unless the item was defective or incorrect.") - p("• Items damaged through misuse or neglect cannot be returned.") - - p("4. Processing Refunds") - .fontWeight(600) - .marginTop(1, em) - - p("• Approved refunds are issued to the original payment method.") - p("• Processing times may vary depending on your bank or payment provider.") - p("• We will notify you once your refund has been initiated.") - - p("5. Cancellations") - .fontWeight(600) - .marginTop(1, em) - - p("• Orders or subscriptions may be cancelled before fulfillment or renewal.") - p("• If Hyperia declare a cancellation of any product or event, a refund will be issued to all parties.") - - p("6. Contact for Refund Requests") - .fontWeight(600) - .marginTop(1, em) - - p("If you need to request a refund, return an item, or cancel an order, please contact us at info@hyperia.so. Include your order number and relevant details so we can assist you promptly.") - - p("7. Policy Updates") - .fontWeight(600) - .marginTop(1, em) - - p("We may update this Refund & Return Policy from time to time. Any changes will be posted on this page with the effective date.") - - p("x") - .onClick(function (done) { - if(done) { - this.parentElement.style.display = "none" - } - }) - .color("var(--red)") - .xRight(1, em).y(1, em) - .fontSize(2, em) - .cursor("pointer") - - }) - .x(50, vw).y(50, vh) - .width(70, vw).height(70, vh) - .center() - .backgroundColor("var(--accent)") - .display("none") - .overflow("scroll") - .padding(1, em) - .border("3px solid black") - .color("var(--darkbrown)") - .attr({ id: "refundWindow" }) - - - VStack(() => { - - p("Contact Us") - .fontSize(2, em) - .fontWeight(600) - .marginBottom(1, em) - - p("Email: info@hyperia.so") - p("Phone: 813-373-9100") - p("Address: 2014 E 9th St, Unit A, Austin TX") - - p("x") - .onClick(function (done) { - if(done) { - this.parentElement.style.display = "none" - } - }) - .color("var(--red)") - .xRight(1, em).y(1, em) - .fontSize(2, em) - .cursor("pointer") - - }) - .gap(2, em) - .x(50, vw).y(50, vh) - .width(50, vw).height(50, vh) - .center() - .backgroundColor("var(--accent)") - .display("none") - .overflow("scroll") - .padding(1, em) - .border("3px solid black") - .color("var(--darkbrown)") - .attr({ id: "contactWindow" }) - - - } -} - -register(Events) \ No newline at end of file diff --git a/ui/public/pages/Home.js b/ui/public/pages/Home.js index f7a1b02..b9def48 100644 --- a/ui/public/pages/Home.js +++ b/ui/public/pages/Home.js @@ -1,9 +1,6 @@ -import "../components/NavBar.js" -import "./SignupForm.js" -import "./Why.js" -import "./Events.js" -import "./Join.js" +import "./SignUp.js" import "./SignIn.js" +import "./SignupForm.js" import "./Success.js" class Home extends Shadow { @@ -55,7 +52,6 @@ class Home extends Shadow { .fontSize(1.2, em) .cursor("default") .onHover(function (hovering) { - console.log("hover") if(hovering) { this.style.background = "var(--red)" this.style.color = "black" @@ -75,7 +71,6 @@ class Home extends Shadow { .fontSize(1.2, em) .cursor("default") .onHover(function (hovering) { - console.log("hover") if(hovering) { this.style.background = "var(--red)" this.style.color = "black" @@ -110,7 +105,6 @@ class Home extends Shadow { .marginHorizontal(27, vw) .width(46, vw) .marginTop(10, vh) - .fontFamily("Nanum") } render() { @@ -121,28 +115,22 @@ class Home extends Shadow { case "/": this.MainContent() break; - case "/why": - Why() - break; - case "/events": - Events() - break; - case "/join": - Join() + case "/signup": + SignUp() break; case "/success": Success() break; default: - if(window.location.pathname.startsWith("/signup")) { + if(window.location.pathname.startsWith("/free")) { SignupForm() } else if(window.location.pathname.startsWith("/login")) { SignIn() } } - }) + .fontFamily("Nanum") .onNavigate(() => { this.rerender() }) diff --git a/ui/public/pages/SignUp.js b/ui/public/pages/SignUp.js new file mode 100644 index 0000000..0b243df --- /dev/null +++ b/ui/public/pages/SignUp.js @@ -0,0 +1,31 @@ +class SignUp extends Shadow { + render() { + VStack(() => { + + h2("$50 / Month Subscription") + .color("var(--red)") + + p(" - Access to Parchment Online and Parchment Desktop") + p(" - Ability to Create Networks") + p(" - Up to 5GB Storage Space") + + button("Buy") + .color("var(--red") + .border("1px solid var(--red)") + .marginLeft("auto") + .fontSize(1.1, em) + .onClick(async function() { + this.innerText = "Loading..." + const res = await fetch("/create-checkout-session", { method: "POST" }); + const data = await res.json(); + window.location = data.url; + }) + .marginTop(2, em) + }) + .x(50, vw).y(50, vh).center() + .border("1px solid var(--red)") + .padding(1, em) + } +} + +register(SignUp) \ No newline at end of file diff --git a/ui/public/pages/SignupForm.js b/ui/public/pages/SignupForm.js index 0e466b2..56a9bc3 100644 --- a/ui/public/pages/SignupForm.js +++ b/ui/public/pages/SignupForm.js @@ -12,7 +12,7 @@ class SignupForm extends Shadow { render() { ZStack(() => { form(() => { - + VStack(() => { p() @@ -23,20 +23,20 @@ class SignupForm extends Shadow { .background("var(--accent)") HStack(() => { - + VStack(() => { - input("First Name*") - .attr({name: "firstName", type: "name", required: "true"}) - .styles(this.inputStyles) - - input("Last Name*") - .attr({name: "lastName", type: "name", required: "true"}) - .styles(this.inputStyles) - input("Email*") .attr({name: "email", type: "email", required: "true"}) .styles(this.inputStyles) - + + input("First Name*") + .attr({name: "firstName", type: "name", required: "true"}) + .styles(this.inputStyles) + + input("Last Name*") + .attr({name: "lastName", type: "name", required: "true"}) + .styles(this.inputStyles) + input("Password*") .attr({name: "password", type: "password", required: "true"}) .styles(this.inputStyles)