Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions Dart/htmx/dart-htmx/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
firebase-debug.log*
firebase-debug.*.log*

# Firebase cache
.firebase/

# Firebase config

# Uncomment this if you'd like others to create their own Firebase project.
# For a team working on the same Firebase project(s), it is recommended to leave
# it commented so all members can deploy to the same project(s) in .firebaserc.
# .firebaserc

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# nyc test coverage
.nyc_output

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env

# dataconnect generated files
.dataconnect
29 changes: 29 additions & 0 deletions Dart/htmx/dart-htmx/firebase.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"functions": [
{
"source": "functions",
"codebase": "default",
"disallowLegacyRuntimeConfig": true,
"ignore": [
".dart_tool",
".git",
"firebase-debug.log",
"firebase-debug.*.log",
"*.local"
],
"runtime": "dart3"
}
],
"emulators": {
"functions": {
"port": 5001
},
"firestore": {
"port": 8080
},
"ui": {
"enabled": true
},
"singleProjectMode": true
}
}
11 changes: 11 additions & 0 deletions Dart/htmx/dart-htmx/functions/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.dart_tool/
build/
*.dart.js
*.info.json
*.js
*.js.map
*.js.deps
*.js.symbols
firebase-debug.log
firebase-debug.*.log
*.local
187 changes: 187 additions & 0 deletions Dart/htmx/dart-htmx/functions/bin/server.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import 'dart:convert';
import 'package:firebase_functions/firebase_functions.dart';
import 'package:google_cloud_firestore/google_cloud_firestore.dart';
import 'package:html/dom.dart' as html;

class Contact {
String firstName;
String lastName;
String email;

Contact({required this.firstName, required this.lastName, required this.email});

factory Contact.fromJson(Map<String, dynamic> json) {
return Contact(
firstName: json['firstName'] as String? ?? 'Joe',
lastName: json['lastName'] as String? ?? 'Blow',
email: json['email'] as String? ?? 'joe@blow.com',
);
}

Map<String, dynamic> toJson() => {
'firstName': firstName,
'lastName': lastName,
'email': email,
};
}

Future<Contact> getContact(DocumentReference ref) async {
final snapshot = await ref.get();
if (!snapshot.exists) {
final defaultContact = Contact(firstName: 'Joe', lastName: 'Blow', email: 'joe@blow.com');
await ref.set(defaultContact.toJson());
return defaultContact;
}
return Contact.fromJson(snapshot.data()!);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using the bang operator ! on snapshot.data() is discouraged. Even though snapshot.exists is checked, it is safer to handle the data nullability explicitly to prevent potential runtime crashes if the document exists but contains no fields.

  final data = snapshot.data();
  if (data == null) throw StateError('Document exists but has no data');
  return Contact.fromJson(data);

}

html.Document createBaseDocument(String titleText, html.Element content) {
final doc = html.Document();
final htmlNode = html.Element.tag('html')..attributes['lang'] = 'en';
doc.append(htmlNode);

final head = html.Element.tag('head');
htmlNode.append(head);

head.append(html.Element.tag('meta')..attributes['charset'] = 'utf-8');
head.append(html.Element.tag('meta')..attributes['name'] = 'viewport'..attributes['content'] = 'width=device-width, initial-scale=1');
head.append(html.Element.tag('title')..text = titleText);
head.append(html.Element.tag('link')..attributes['rel'] = 'stylesheet'..attributes['href'] = 'https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css');
head.append(html.Element.tag('script')..attributes['src'] = 'https://cdn.jsdelivr.net/npm/htmx.org@4.0.0-beta3'..attributes['crossorigin'] = 'anonymous');
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The HTMX version 4.0.0-beta3 is incorrect. The current major version of HTMX is 2.x. Using a non-existent version will cause the script to fail to load from the CDN.

Suggested change
head.append(html.Element.tag('script')..attributes['src'] = 'https://cdn.jsdelivr.net/npm/htmx.org@4.0.0-beta3'..attributes['crossorigin'] = 'anonymous');
head.append(html.Element.tag('script')..attributes['src'] = 'https://cdn.jsdelivr.net/npm/htmx.org@2.0.3'..attributes['crossorigin'] = 'anonymous');


final body = html.Element.tag('body');
htmlNode.append(body);

final main = html.Element.tag('main')..attributes['class'] = 'container';
body.append(main);
main.append(content);

return doc;
}

html.Element createDisplayView(Contact contact) {
final article = html.Element.tag('article')..attributes['id'] = 'contact-card';

final header = html.Element.tag('header')..text = 'Contact Info';
article.append(header);

final grid = html.Element.tag('div')..attributes['class'] = 'grid';
article.append(grid);

final col1 = html.Element.tag('div');
grid.append(col1);
col1.append(html.Element.tag('strong')..text = 'First Name: ');
col1.append(html.Text(contact.firstName));

final col2 = html.Element.tag('div');
grid.append(col2);
col2.append(html.Element.tag('strong')..text = 'Last Name: ');
col2.append(html.Text(contact.lastName));

final col3 = html.Element.tag('div');
grid.append(col3);
col3.append(html.Element.tag('strong')..text = 'Email: ');
col3.append(html.Text(contact.email));

final footer = html.Element.tag('footer');
article.append(footer);

final editBtn = html.Element.tag('button')
..attributes['hx-get'] = '?mode=edit'
..attributes['hx-target'] = '#contact-card'
..attributes['hx-swap'] = 'outerHTML'
..attributes['class'] = 'secondary'
..text = 'Click To Edit';
footer.append(editBtn);

return article;
}

html.Element createEditView(Contact contact) {
final article = html.Element.tag('article')..attributes['id'] = 'contact-card';

final form = html.Element.tag('form')
..attributes['hx-put'] = '?'
..attributes['hx-target'] = '#contact-card'
..attributes['hx-swap'] = 'outerHTML';
article.append(form);

final header = html.Element.tag('header')..text = 'Edit Contact';
form.append(header);

final grid = html.Element.tag('div')..attributes['class'] = 'grid';
form.append(grid);

final label1 = html.Element.tag('label')..text = 'First Name';
grid.append(label1);
label1.append(html.Element.tag('input')..attributes['type'] = 'text'..attributes['name'] = 'firstName'..attributes['value'] = contact.firstName);

final label2 = html.Element.tag('label')..text = 'Last Name';
grid.append(label2);
label2.append(html.Element.tag('input')..attributes['type'] = 'text'..attributes['name'] = 'lastName'..attributes['value'] = contact.lastName);

final label3 = html.Element.tag('label')..text = 'Email';
form.append(label3);
label3.append(html.Element.tag('input')..attributes['type'] = 'email'..attributes['name'] = 'email'..attributes['value'] = contact.email);

final footer = html.Element.tag('footer')..attributes['class'] = 'grid';
form.append(footer);

final cancelBtn = html.Element.tag('button')
..attributes['type'] = 'button'
..attributes['hx-get'] = '?'
..attributes['hx-target'] = '#contact-card'
..attributes['hx-swap'] = 'outerHTML'
..attributes['class'] = 'secondary'
..text = 'Cancel';
footer.append(cancelBtn);

final saveBtn = html.Element.tag('button')..attributes['type'] = 'submit'..text = 'Save';
footer.append(saveBtn);

return article;
}

void main(List<String> args) {
fireUp(args, (firebase) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The function fireUp is not defined in this file nor is it imported from any of the declared dependencies. This will result in a compilation error. If you are using the standard firebase_functions package, you should likely use functions.http.onRequest to export your function.

final firestore = Firestore();

firebase.https.onRequest(
name: 'contact',
(request) async {
final docRef = firestore.collection('contacts').doc('1');
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The document ID is hardcoded to '1'. This limits the application to managing only a single contact record. Consider making this dynamic by extracting an ID from the request path or query parameters.

final contact = await getContact(docRef);

final mode = request.url.queryParameters['mode'];
final isHxRequest = request.headers['hx-request'] == 'true';

if (request.method == 'GET') {
if (mode == 'edit') {
final editView = createEditView(contact);
final htmlStr = isHxRequest ? editView.outerHtml : createBaseDocument('Edit Contact', editView).outerHtml;
return Response(200, body: htmlStr, headers: {'Content-Type': 'text/html'});
} else {
final displayView = createDisplayView(contact);
final htmlStr = isHxRequest ? displayView.outerHtml : createBaseDocument('Contact', displayView).outerHtml;
return Response(200, body: htmlStr, headers: {'Content-Type': 'text/html'});
}
} else if (request.method == 'PUT' || request.method == 'POST') {
final bodyStr = await request.readAsString();
final formData = Uri.splitQueryString(bodyStr);

contact.firstName = formData['firstName'] ?? contact.firstName;
contact.lastName = formData['lastName'] ?? contact.lastName;
contact.email = formData['email'] ?? contact.email;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Form data is assigned directly to the Contact object without validation. This could allow empty strings or malformed email addresses to be persisted in Firestore. It is recommended to validate required fields and formats before saving.


await docRef.set(contact.toJson());

final displayView = createDisplayView(contact);
final htmlStr = isHxRequest ? displayView.outerHtml : createBaseDocument('Contact', displayView).outerHtml;
return Response(200, body: htmlStr, headers: {'Content-Type': 'text/html'});
}

return Response(405, body: 'Method Not Allowed');
},
);
});
}
Loading
Loading