~repos /gromer

#golang#htmx#ssr

git clone https://pyrossh.dev/repos/gromer.git

gromer is a framework and cli to build multipage web apps in golang using htmx and alpinejs.


5c77aad9 Peter John

tag: v0.13.0

v0.13.0

3 years ago
fix api explorer
api_explorer.go CHANGED
@@ -1,13 +1,784 @@
1
1
  package gromer
2
2
 
3
3
  import (
4
+ "context"
4
5
  "encoding/json"
6
+ "html/template"
7
+ "strings"
5
8
  )
6
9
 
7
- func ApiExplorer() (HtmlContent, int, error) {
10
+ func ApiExplorer(ctx context.Context) (HtmlContent, int, error) {
11
+ apiRoutes := []RouteDefinition{}
12
+ for _, v := range RouteDefs {
13
+ if strings.Contains(v.Path, "/api/") {
14
+ apiRoutes = append(apiRoutes, v)
15
+ }
16
+ }
8
- _, err := json.Marshal(RouteDefs)
17
+ apiData, err := json.Marshal(apiRoutes)
9
18
  if err != nil {
10
- return HtmlErr(500, err)
19
+ return HtmlErr(400, err)
11
20
  }
12
- return Html("", nil)
21
+ return Html(`
22
+ <!DOCTYPE html>
23
+ <html lang="en">
24
+ <head>
25
+ <meta charset="UTF-8">
26
+ <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
27
+ <meta http-equiv="encoding" content="utf-8">
28
+ <title> API Explorer </title>
29
+ <meta name="description" content="API Explorer">
30
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0, viewport-fit=cover">
31
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/codemirror@5.63.1/lib/codemirror.css">
32
+ <style>
33
+ html,
34
+ body {
35
+ height: 100vh;
36
+ }
37
+
38
+ #left .CodeMirror {
39
+ height: 400px;
40
+ }
41
+
42
+ #right .CodeMirror {
43
+ height: calc(100vh - 60px);
44
+ }
45
+
46
+ .form-select {
47
+ background-image: url("data:image/svg+xml,%3csvg
48
+ xmlns='http://www.w3.org/2000/svg'viewBox='0 0 24 24'fill='%23a0aec0'%3e%3cpath d='M15.3 9.3a1 1 0 0 1 1.4 1.4l-4 4a1 1 0 0 1-1.4 0l-4-4a1 1 0 0 1 1.4-1.4l3.3 3.29 3.3-3.3z'/%3e%3c/svg%3e");
49
+ -webkit-appearance: none;
50
+ -moz-appearance: none;
51
+ appearance: none;
52
+ -webkit-print-color-adjust: exact;
53
+ color-adjust: exact;
54
+ background-repeat: no-repeat;
55
+ background-color: #fff;
56
+ border-color: #e2e8f0;
57
+ border-width: 1px;
58
+ border-radius: 0.25rem;
59
+ padding-top: 0.5rem;
60
+ padding-right: 2.5rem;
61
+ padding-bottom: 0.5rem;
62
+ padding-left: 0.75rem;
63
+ font-size: 1rem;
64
+ line-height: 1.5;
65
+ background-position: right 0.5rem center;
66
+ background-size: 1.5em 1.5em;
67
+ }
68
+
69
+ .form-select:focus {
70
+ outline: none;
71
+ box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5);
72
+ border-color: #63b3ed;
73
+ }
74
+
75
+ table {
76
+ width: 100%;
77
+ }
78
+
79
+ tr {
80
+ width: 100%;
81
+ }
82
+
83
+ td, th {
84
+ border-bottom: 1px solid rgb(204, 204, 204);
85
+ border-left: 1px solid rgb(204, 204, 204);
86
+ text-align: left;
87
+ }
88
+
89
+ textarea:focus, input:focus {
90
+ outline: none;
91
+ box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5);
92
+ border-color: #63b3ed;
93
+ }
94
+
95
+ *:focus {
96
+ outline: none;
97
+ box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5);
98
+ border-color: #63b3ed;
99
+ }
100
+
101
+ .spinner {
102
+ animation: rotate 2s linear infinite;
103
+ width: 24px;
104
+ height: 24px;
105
+ }
106
+
107
+ .spinner .path {
108
+ stroke: rgba(249, 250, 251, 1);
109
+ stroke-linecap: round;
110
+ animation: dash 1.5s ease-in-out infinite;
111
+ }
112
+
113
+ @keyframes rotate {
114
+ 100% {
115
+ transform: rotate(360deg);
116
+ }
117
+ }
118
+
119
+ @keyframes dash {
120
+ 0% {
121
+ stroke-dasharray: 1, 150;
122
+ stroke-dashoffset: 0;
123
+ }
124
+
125
+ 50% {
126
+ stroke-dasharray: 90, 150;
127
+ stroke-dashoffset: -35;
128
+ }
129
+
130
+ 100% {
131
+ stroke-dasharray: 90, 150;
132
+ stroke-dashoffset: -124;
133
+ }
134
+ }
135
+ </style>
136
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.63.1/codemirror.min.js"></script>
137
+ <script src="https://cdn.jsdelivr.net/npm/codemirror@5.63.1/mode/javascript/javascript.js"></script>
138
+ <style>
139
+ *,
140
+ ::before,
141
+ ::after {
142
+ box-sizing: border-box;
143
+ }
144
+
145
+ html {
146
+ -moz-tab-size: 4;
147
+ -o-tab-size: 4;
148
+ tab-size: 4;
149
+ line-height: 1.15;
150
+ -webkit-text-size-adjust: 100%;
151
+ }
152
+
153
+ body {
154
+ margin: 0;
155
+ font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji';
156
+ }
157
+
158
+ hr {
159
+ height: 0;
160
+ color: inherit;
161
+ }
162
+
163
+ abbr[title] {
164
+ -webkit-text-decoration: underline dotted;
165
+ text-decoration: underline dotted;
166
+ }
167
+
168
+ b,
169
+ strong {
170
+ font-weight: bolder;
171
+ }
172
+
173
+ code,
174
+ kbd,
175
+ samp,
176
+ pre {
177
+ font-family: ui-monospace, SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace;
178
+ font-size: 1em;
179
+ }
180
+
181
+ small {
182
+ font-size: 80%;
183
+ }
184
+
185
+ sub,
186
+ sup {
187
+ font-size: 75%;
188
+ line-height: 0;
189
+ position: relative;
190
+ vertical-align: baseline;
191
+ }
192
+
193
+ sub {
194
+ bottom: -0.25em;
195
+ }
196
+
197
+ sup {
198
+ top: -0.5em;
199
+ }
200
+
201
+ table {
202
+ text-indent: 0;
203
+ border-color: inherit;
204
+ }
205
+
206
+ button,
207
+ input,
208
+ optgroup,
209
+ select,
210
+ textarea {
211
+ font-family: inherit;
212
+ font-size: 100%;
213
+ line-height: 1.15;
214
+ margin: 0;
215
+ }
216
+
217
+ button,
218
+ select {
219
+ text-transform: none;
220
+ }
221
+
222
+ button,
223
+ [type='button'],
224
+ [type='reset'],
225
+ [type='submit'] {
226
+ -webkit-appearance: button;
227
+ }
228
+
229
+ ::-moz-focus-inner {
230
+ border-style: none;
231
+ padding: 0;
232
+ }
233
+
234
+ :-moz-focusring {
235
+ outline: 1px dotted ButtonText;
236
+ outline: auto;
237
+ }
238
+
239
+ :-moz-ui-invalid {
240
+ box-shadow: none;
241
+ }
242
+
243
+ legend {
244
+ padding: 0;
245
+ }
246
+
247
+ progress {
248
+ vertical-align: baseline;
249
+ }
250
+
251
+ ::-webkit-inner-spin-button,
252
+ ::-webkit-outer-spin-button {
253
+ height: auto;
254
+ }
255
+
256
+ [type='search'] {
257
+ -webkit-appearance: textfield;
258
+ outline-offset: -2px;
259
+ }
260
+
261
+ ::-webkit-search-decoration {
262
+ -webkit-appearance: none;
263
+ }
264
+
265
+ ::-webkit-file-upload-button {
266
+ -webkit-appearance: button;
267
+ font: inherit;
268
+ }
269
+
270
+ summary {
271
+ display: list-item;
272
+ }
273
+
274
+ blockquote,
275
+ dl,
276
+ dd,
277
+ h1,
278
+ h2,
279
+ h3,
280
+ h4,
281
+ h5,
282
+ h6,
283
+ hr,
284
+ figure,
285
+ p,
286
+ pre {
287
+ margin: 0;
288
+ }
289
+
290
+ button {
291
+ background-color: transparent;
292
+ background-image: none;
293
+ }
294
+
295
+ fieldset {
296
+ margin: 0;
297
+ padding: 0;
298
+ }
299
+
300
+ ol,
301
+ ul {
302
+ list-style: none;
303
+ margin: 0;
304
+ padding: 0;
305
+ }
306
+
307
+ html {
308
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
309
+ line-height: 1.5;
310
+ }
311
+
312
+ body {
313
+ font-family: inherit;
314
+ line-height: inherit;
315
+ }
316
+
317
+ *,
318
+ ::before,
319
+ ::after {
320
+ box-sizing: border-box;
321
+ border-width: 0;
322
+ border-style: solid;
323
+ border-color: currentColor;
324
+ }
325
+
326
+ hr {
327
+ border-top-width: 1px;
328
+ }
329
+
330
+ img {
331
+ border-style: solid;
332
+ }
333
+
334
+ textarea {
335
+ resize: vertical;
336
+ }
337
+
338
+ input::-moz-placeholder,
339
+ textarea::-moz-placeholder {
340
+ opacity: 1;
341
+ color: #9ca3af;
342
+ }
343
+
344
+ input:-ms-input-placeholder,
345
+ textarea:-ms-input-placeholder {
346
+ opacity: 1;
347
+ color: #9ca3af;
348
+ }
349
+
350
+ input::placeholder,
351
+ textarea::placeholder {
352
+ opacity: 1;
353
+ color: #9ca3af;
354
+ }
355
+
356
+ button,
357
+ [role="button"] {
358
+ cursor: pointer;
359
+ }
360
+
361
+ table {
362
+ border-collapse: collapse;
363
+ }
364
+
365
+ h1,
366
+ h2,
367
+ h3,
368
+ h4,
369
+ h5,
370
+ h6 {
371
+ font-size: inherit;
372
+ font-weight: inherit;
373
+ }
374
+
375
+ a {
376
+ color: inherit;
377
+ text-decoration: inherit;
378
+ }
379
+
380
+ button,
381
+ input,
382
+ optgroup,
383
+ select,
384
+ textarea {
385
+ padding: 0;
386
+ line-height: inherit;
387
+ color: inherit;
388
+ }
389
+
390
+ pre,
391
+ code,
392
+ kbd,
393
+ samp {
394
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
395
+ }
396
+
397
+ img,
398
+ svg,
399
+ video,
400
+ canvas,
401
+ audio,
402
+ iframe,
403
+ embed,
404
+ object {
405
+ display: block;
406
+ vertical-align: middle;
407
+ }
408
+
409
+ img,
410
+ video {
411
+ max-width: 100%;
412
+ height: auto;
413
+ }
414
+
415
+ [hidden] {
416
+ display: none;
417
+ }
418
+
419
+ *,
420
+ ::before,
421
+ ::after {
422
+ --tw-border-opacity: 1;
423
+ border-color: rgba(229, 231, 235, var(--tw-border-opacity));
424
+ }
425
+
426
+ .flex {
427
+ display: flex;
428
+ }
429
+
430
+ .flex-col {
431
+ flex-direction: column;
432
+ }
433
+
434
+ .w-full {
435
+ width: 100%;
436
+ }
437
+
438
+ .p-2 {
439
+ padding: 0.5rem;
440
+ }
441
+
442
+ .bg-gray-50 {
443
+ background-color: rgba(249, 250, 251, 1);
444
+ }
445
+
446
+ .border-b {
447
+ border-bottom-width: 1px;
448
+ }
449
+
450
+ .border-gray-200 {
451
+ border-color: rgba(229, 231, 235, 1);
452
+ }
453
+
454
+ .items-center {
455
+ align-items: center;
456
+ }
457
+
458
+ .justify-start {
459
+ justify-content: flex-start;
460
+ }
461
+
462
+ .mr-4 {
463
+ margin-right: 1rem;
464
+ }
465
+
466
+ .text-gray-700 {
467
+ color: rgba(55, 65, 81, 1);
468
+ }
469
+
470
+ .text-2xl {
471
+ font-size: 1.5rem;
472
+ line-height: 2rem;
473
+ }
474
+
475
+ .font-bold {
476
+ font-weight: 700;
477
+ }
478
+
479
+ .text-xl {
480
+ font-size: 1.25rem;
481
+ line-height: 1.75rem;
482
+ }
483
+
484
+ .block {
485
+ display: block;
486
+ }
487
+
488
+ .ml-3 {
489
+ margin-left: 0.75rem;
490
+ }
491
+
492
+ .mr-3 {
493
+ margin-right: 0.75rem;
494
+ }
495
+
496
+ .bg-gray-200 {
497
+ background-color: rgba(229, 231, 235, 1);
498
+ }
499
+
500
+ .border {
501
+ border-width: 1px;
502
+ }
503
+
504
+ .border-gray-400 {
505
+ border-color: rgba(156, 163, 175, 1);
506
+ }
507
+
508
+ .rounded-md {
509
+ border-radius: 0.375rem;
510
+ }
511
+
512
+ .pt-2 {
513
+ padding-top: 0.5rem;
514
+ }
515
+
516
+ .pb-2 {
517
+ padding-bottom: 0.5rem;
518
+ }
519
+
520
+ .pl-6 {
521
+ padding-left: 1.5rem;
522
+ }
523
+
524
+ .pr-6 {
525
+ padding-right: 1.5rem;
526
+ }
527
+
528
+ .flex-row {
529
+ flex-direction: row;
530
+ }
531
+
532
+ .pr-8 {
533
+ padding-right: 2rem;
534
+ }
535
+
536
+ .border-r {
537
+ border-right-width: 1px;
538
+ }
539
+
540
+ .border-gray-300 {
541
+ border-color: rgba(209, 213, 219, 1);
542
+ }
543
+
544
+ .text-sm {
545
+ font-size: 0.875rem;
546
+ line-height: 1.25rem;
547
+ }
548
+
549
+ .uppercase {
550
+ text-transform: uppercase
551
+ }
552
+
553
+ .pl-2 {
554
+ padding-left: 0.5rem;
555
+ }
556
+
557
+ .p-1 {
558
+ padding: 0.25rem;
559
+ }
560
+
561
+ .border-l {
562
+ border-left-width: 1px;
563
+ }
564
+
565
+ .border-l-gray-200 {
566
+ border-left-color: rgba(229, 231, 235, 1);
567
+ }
568
+ </style>
569
+ </head>
570
+ <body>
571
+ <div class="flex flex-col">
572
+ <div class="flex w-full p-2 bg-gray-50 border-b border-gray-200 items-center justify-start">
573
+ <div class="flex mr-4 text-gray-700 text-2xl font-bold"> API Explorer </div>
574
+ <div class="text-xl">
575
+ <select id="api-select" class="form-select block">
576
+ {{#each routes as |route|}}
577
+ <option value="{{ @index }}">
578
+ <div> {{ route.Method }} {{ route.Path }} </div>
579
+ </option>
580
+ {{/each}}
581
+ </select>
582
+ </div>
583
+ <div class="flex ml-3 mr-3">
584
+ <button id="run" class="bg-gray-200 border border-gray-400 hover:bg-gray-200 focus:outline-none rounded-md text-gray-700 text-md font-bold pt-2 pb-2 pl-6 pr-6"> RUN </button>
585
+ </div>
586
+ </div>
587
+ <div class="flex">
588
+ <div class="flex flex-row" style="width: 50%;;">
589
+ <div class="pr-8 border-r border-gray-300" style="background: #f7f7f7;;"></div>
590
+ <div class="w-full">
591
+ <div class="text-gray-700 text-sm font-bold uppercase pl-2 pt-2 pb-2 bg-gray-50 border-b border-gray-200"> Headers </div>
592
+ <table id="headersTable">
593
+ <tr>
594
+ <td>
595
+ <input class="w-full p-1" value="Authorization">
596
+ </td>
597
+ <td>
598
+ <input class="w-full p-1">
599
+ </td>
600
+ </tr>
601
+ </table>
602
+ <div class="text-gray-700 text-sm font-bold uppercase pl-2 pt-2 pb-2 bg-gray-50 border-b border-gray-200"> Path Params </div>
603
+ <table id="pathParamsTable">
604
+ <tr>
605
+ <td class="text-gray-700" style="width: 50%;;">
606
+ <div class="p-1"> 123 </div>
607
+ </td>
608
+ <td style="width: 50%;;">
609
+ <input class="w-full p-1">
610
+ </td>
611
+ </tr>
612
+ </table>
613
+ <div class="text-gray-700 text-sm font-bold uppercase pl-2 pt-2 pb-2 bg-gray-50 border-b border-gray-200"> Query Params </div>
614
+ <table id="queryParamsTable">
615
+ <tr>
616
+ <td class="text-gray-700" style="width: 50%;;">
617
+ <div class="p-1"> 123 </div>
618
+ </td>
619
+ <td style="width: 50%;;">
620
+ <input class="w-full p-1">
621
+ </td>
622
+ </tr>
623
+ </table>
624
+ <div class="text-gray-700 text-sm font-bold uppercase pl-2 pt-2 pb-2 bg-gray-50 border-b border-gray-200"> Body </div>
625
+ <div id="left" class="border-b border-gray-200 text-md"></div>
626
+ </div>
627
+ </div>
628
+ <div class="flex flex-row" style="width: 50%;;">
629
+ <div id="right" class="w-full border-l border-l-gray-200 text-md"></div>
630
+ <div class="pr-8 border-l border-gray-300" style="background: #f7f7f7;;"></div>
631
+ </div>
632
+ </div>
633
+ </div>
634
+ <script>
635
+ window.apiDefs = {{ apiData }}
636
+ </script>
637
+ <script>
638
+ window.codeLeft = CodeMirror(document.getElementById('left'), {
639
+ value: '{}',
640
+ mode: 'javascript'
641
+ })
642
+ </script>
643
+ <script>
644
+ window.codeRight = CodeMirror(document.getElementById('right'), {
645
+ value: '',
646
+ mode: 'javascript',
647
+ lineNumbers: true,
648
+ readOnly: true,
649
+ lineWrapping: true
650
+ })
651
+ </script>
652
+ <script>
653
+ const getCurrentApiCall = () => {
654
+ const index = document.getElementById("api-select").value;
655
+ return window.apiDefs[index];
656
+ }
657
+ const updatePathParams = (apiCall) => {
658
+ const table = document.getElementById("pathParamsTable");
659
+ if (apiCall.pathParams.length === 0) {
660
+ table.innerHTML = " <div style='background-color: rgb(245, 245, 245); padding: 0.25rem; text-align: center; color: gray;'>NONE</div>";
661
+ } else {
662
+ table.innerHTML = "";
663
+ }
664
+ for (const param of apiCall.pathParams.reverse()) {
665
+ const row = table.insertRow(0);
666
+ const cell1 = row.insertCell(0);
667
+ const cell2 = row.insertCell(1);
668
+ cell1.style = "width: 30%; border-left: 0px;";
669
+ cell1.class = "text-gray-700";
670
+ cell2.style = "width: 70%;";
671
+ cell1.innerHTML = " <div class='p-1'> " + param + " </div>";
672
+ cell2.innerHTML = " <input id='path-param-" + param + " class='w-full p-1'>";
673
+ }
674
+ }
675
+ const updateParams = (apiCall) => {
676
+ const table = document.getElementById("queryParamsTable");
677
+ if (!apiCall.params) {
678
+ table.innerHTML = " <div style='background-color: rgb(245, 245, 245); padding: 0.25rem; text-align: center; color: gray;'> NONE </div>";
679
+ } else {
680
+ table.innerHTML = "";
681
+ }
682
+ if (apiCall.method === "GET" || apiCall.method === "DELETE") {
683
+ for (const key of Object.keys(apiCall.params)) {
684
+ const row = table.insertRow(0);
685
+ const cell1 = row.insertCell(0);
686
+ const cell2 = row.insertCell(1);
687
+ cell1.style = "width: 30%; border-left: 0px;";
688
+ cell1.class = "text-gray-700";
689
+ cell2.style = "width: 70%;";
690
+ cell1.innerHTML = " <div class='p-1'> " + key + " </div>";
691
+ cell2.innerHTML = " <input id='query-param-" + key + "' class='w-full p-1'>";
692
+ }
693
+ }
694
+ }
695
+ const updateBody = (apiCall) => {
696
+ if (apiCall.method !== "GET" && apiCall.method !== "DELETE") {
697
+ window.codeLeft.setValue(JSON.stringify(apiCall.params, 2, 2));
698
+ } else {
699
+ window.codeLeft.setValue("");
700
+ }
701
+ }
702
+ const init = () => {
703
+ updatePathParams(window.apiDefs[0]);
704
+ updateParams(window.apiDefs[0]);
705
+ updateBody(window.apiDefs[0]);
706
+ const headersJson = localStorage.getItem("headers");
707
+ if (headersJson) {
708
+ const table = document.getElementById("headersTable");
709
+ const headers = JSON.parse(headersJson);
710
+ table.innerHTML = "";
711
+ for (const key of Object.keys(headers)) {
712
+ const value = headers[key];
713
+ const row = table.insertRow(0);
714
+ const cell1 = row.insertCell(0);
715
+ const cell2 = row.insertCell(1);
716
+ cell1.style = "width: 30%; border-left: 0px;";
717
+ cell2.style = "width: 70%;";
718
+ cell1.innerHTML = "<input value = '" + key + "' class='w-full p-1'>";
719
+ cell2.innerHTML = "<input value = '" + value + "' class='w-full p-1'>";
720
+ }
721
+ }
722
+ }
723
+ window.onload = () => {
724
+ init();
725
+ }
726
+ document.getElementById("api-select").onchange = () => {
727
+ const apiCall = getCurrentApiCall();
728
+ updatePathParams(apiCall);
729
+ updateParams(apiCall);
730
+ updateBody(apiCall);
731
+ }
732
+ const run = document.getElementById("run");
733
+ run.onclick = async () => {
734
+ run.innerHTML = "<svg class='spinner' viewBox='0 0 50 50'><circle class='path' cx='25' cy='25' r='20' fill='none' stroke-width='5'></circle></svg>";
735
+ const table = document.getElementById("headersTable");
736
+ const headers = {};
737
+ for (const row of table.rows) {
738
+ const key = row.cells[0].children[0].value;
739
+ const value = row.cells[1].children[0].value;
740
+ headers[key] = value;
741
+ }
742
+ const apiCall = getCurrentApiCall();
743
+ let path = apiCall.path;
744
+ const bodyParams = {};
745
+ if (apiCall.method !== "GET" && apiCall.method != "DELETE") {
746
+ bodyParams["body"] = window.codeLeft.getValue();
747
+ } else {
748
+ for (const param of apiCall.pathParams) {
749
+ const value = document.getElementById('path-param-' + param).value;
750
+ path = path.replace('{' + param + '}', value);
751
+ }
752
+ const paramsKeys = Object.keys(apiCall.params);
753
+ if (paramsKeys.length > 0) {
754
+ path += "?";
755
+ paramsKeys.forEach((key, i) => {
756
+ const value = document.getElementById('query-param-' + key).value;
757
+ path += key + "=" + value;
758
+ if (i !== paramsKeys.length - 1) {
759
+ path += "&";
760
+ }
761
+ });
762
+ }
763
+ }
764
+ localStorage.setItem("headers", JSON.stringify(headers));
765
+ try {
766
+ const res = await fetch(path, {
767
+ method: apiCall.method,
768
+ headers,
769
+ ...bodyParams
770
+ });
771
+ const json = await res.json();
772
+ window.codeRight.setValue(JSON.stringify(json, 2, 2));
773
+ } catch (err) {
774
+ window.codeRight.setValue(JSON.stringify({
775
+ error: err.message
776
+ }, 2, 2));
777
+ }
778
+ run.innerHTML = "RUN";
779
+ }
780
+ </script>
781
+ </body>
782
+ </html>
783
+ `, M{"routes": apiRoutes, "apiData": template.HTML(string(apiData))})
13
784
  }
example/components/header.go CHANGED
@@ -1,30 +1,60 @@
1
1
  package components
2
2
 
3
+ import (
4
+ . "github.com/pyros2097/gromer"
5
+ )
6
+
3
7
  func Header() string {
4
- return (`
8
+ return Component(`
5
- <div class="flex flex-row justify-center items-center w-full mb-20 font-bold text-xl text-gray-700 p-4">
9
+ <nav class="navbar" role="navigation" aria-label="main navigation">
6
- <div class="text-blue-700">
10
+ <div class="navbar-brand">
7
- <a href="https://pyros.sh"> pyros.sh </a>
11
+ <a class="navbar-item" href="https://bulma.io">
12
+ <img src="https://bulma.io/images/bulma-logo.png" width="112" height="28">
13
+ </a>
14
+
15
+ <a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="navbarBasicExample">
16
+ <span aria-hidden="true"></span>
17
+ <span aria-hidden="true"></span>
18
+ <span aria-hidden="true"></span>
19
+ </a>
8
- </div>
20
+ </div>
21
+
9
- <div class="flex flex-row flex-1 justify-end items-end p-2">
22
+ <div id="navbarBasicExample" class="navbar-menu">
10
- <div class="border-b-2 border-white text-lg text-blue-700 mr-4">Examples:</div>
11
- <div class="border-b-2 border-white hover:border-red-700 mr-4">
23
+ <div class="navbar-start">
12
- <a href="/"> Home </a>
24
+ <a class="navbar-item" href="/">
25
+ Home
26
+ </a>
27
+
28
+ <a class="navbar-item" href="/about">
29
+ About
30
+ </a>
31
+
32
+ <a class="navbar-item" href="/clock">
33
+ Clock
34
+ </a>
35
+
36
+ <a class="navbar-item" href="/counter">
37
+ Counter
38
+ </a>
39
+
40
+ <a class="navbar-item" href="/api">
41
+ API
42
+ </a>
13
- </div>
43
+ </div>
44
+
45
+ <div class="navbar-end">
14
- <div class="border-b-2 border-white hover:border-red-700 mr-4">
46
+ <div class="navbar-item">
47
+ <div class="buttons">
48
+ <a class="button is-primary">
49
+ <strong>Sign up</strong>
50
+ </a>
15
- <a href="/clock"> Clock </a>
51
+ <a class="button is-light">
52
+ Log in
53
+ </a>
16
- </div>
54
+ </div>
17
- <div class="border-b-2 border-white hover:border-red-700 mr-4">
18
- <a href="/about"> About </a>
19
- </div>
55
+ </div>
20
- <div class="border-b-2 border-white hover:border-red-700 mr-4">
21
- <a href="/container"> Container </a>
22
- </div>
56
+ </div>
23
- <div class="border-b-2 border-white hover:border-red-700 mr-4">
24
- <a href="/panic"> Panic </a>
25
- </div>
57
+ </div>
26
- </div>
58
+ </nav>
27
- {{ children }}
28
- </div>
29
59
  `)
30
60
  }
example/components/page.go CHANGED
@@ -1,7 +1,11 @@
1
1
  package components
2
2
 
3
+ import (
4
+ . "github.com/pyros2097/gromer"
5
+ )
6
+
3
7
  func Page() string {
4
- return (`
8
+ return Component(`
5
9
  <!DOCTYPE html>
6
10
  <html lang="en">
7
11
  <head>
example/pages/get.go CHANGED
@@ -23,39 +23,56 @@ func GET(ctx context.Context, params GetParams) (HtmlContent, int, error) {
23
23
  }
24
24
  return Html(`
25
25
  {{#Page "gromer example"}}
26
- <div class="flex flex-col justify-center items-center">
27
- {{#Header "123"}}
26
+ {{#Header "123"}}
28
- A new link is here
29
- {{/Header}}
27
+ {{/Header}}
28
+ <main class="box center">
29
+ <div class="columns">
30
+ <div>
31
+ <img src="/assets/icon.png" width="48" height="48" />
32
+ </div>
33
+ <div style="margin-left: 8px;">
30
- <h1>Hello this is a h1</h1>
34
+ <h1>Hello this is a h1</h1>
31
- <h2>Hello this is a h2</h2>
35
+ <h2>Hello this is a h2</h2>
32
- <img src="/assets/icon.png" width="48" height="48" />
36
+ </div>
37
+ </div>
33
- <h3 x-data="{ message: 'I ❤️ Alpine' }" x-text="message">I ❤️ Alpine</h3>
38
+ <h3 x-data="{ message: 'I ❤️ Alpine' }" x-text="message">I ❤️ Alpine</h3>
34
- <table class="table">
39
+ <table class="table">
35
- <thead>
40
+ <thead>
36
- <tr>
41
+ <tr>
37
- <th>ID</th>
42
+ <th>ID</th>
38
- <th>Title</th>
43
+ <th>Title</th>
39
- <th>Author</th>
44
+ <th>Author</th>
40
- </tr>
45
+ </tr>
41
- </thead>
46
+ </thead>
42
- <tbody id="new-book" hx-target="closest tr" hx-swap="outerHTML swap:0.5s">
47
+ <tbody id="new-book" hx-target="closest tr" hx-swap="outerHTML swap:0.5s">
43
- {{#each todos as |todo|}}
48
+ {{#each todos as |todo|}}
44
- <tr>
49
+ <tr>
45
- <td>{{todo.ID}}</td>
50
+ <td>{{todo.ID}}</td>
46
- <td>Book1</td>
51
+ <td>Book1</td>
47
- <td>Author1</td>
52
+ <td>Author1</td>
48
- <td>
53
+ <td>
49
- <button class="button is-primary">Edit</button>
54
+ <button class="button is-primary">Edit</button>
50
- </td>
55
+ </td>
51
- <td>
56
+ <td>
52
- <button hx-swap="delete" class="button is-danger" hx-delete="/api/todos/{{todo.ID}}">Delete</button>
57
+ <button hx-swap="delete" class="button is-danger" hx-delete="/api/todos/{{todo.ID}}">Delete</button>
53
- </td>
58
+ </td>
54
- </tr>
59
+ </tr>
55
- {{/each}}
60
+ {{/each}}
56
- </tbody>
61
+ </tbody>
57
- </table>
62
+ </table>
63
+ <nav class="pagination" role="navigation" aria-label="pagination">
64
+ <a class="pagination-previous">Previous</a>
65
+ <a class="pagination-next">Next page</a>
66
+ <ul class="pagination-list">
67
+ <li>
68
+ <a class="pagination-link" aria-label="Goto page 1">1</a>
69
+ </li>
70
+ <li>
71
+ <a class="pagination-link" aria-label="Goto page 2">2</a>
72
+ </li>
73
+ </ul>
58
- </div>
74
+ </nav>
75
+ </main>
59
76
  {{/Page}}
60
77
  `, M{"todos": todos})
61
78
  }
http.go CHANGED
@@ -31,11 +31,12 @@ var IsCloundRun bool
31
31
  var RouteDefs []RouteDefinition
32
32
 
33
33
  type RouteDefinition struct {
34
- Pkg string `json:"pkg"`
34
+ Pkg string `json:"pkg"`
35
- PkgPath string `json:"pkgPath"`
35
+ PkgPath string `json:"pkgPath"`
36
- Method string `json:"method"`
36
+ Method string `json:"method"`
37
- Path string `json:"path"`
37
+ Path string `json:"path"`
38
+ PathParams []string `json:"pathParams"`
38
- Params interface{} `json:"params"`
39
+ Params interface{} `json:"params"`
39
40
  }
40
41
 
41
42
  type HtmlContent string
@@ -52,6 +53,10 @@ func Html(tpl string, params map[string]interface{}) (HtmlContent, int, error) {
52
53
  return HtmlContent(s), 200, nil
53
54
  }
54
55
 
56
+ func Component(tpl string) string {
57
+ return tpl
58
+ }
59
+
55
60
  func HtmlErr(status int, err error) (HtmlContent, int, error) {
56
61
  return HtmlContent("ErrorPage/AccessDeniedPage/NotFoundPage based on status code"), status, err
57
62
  }
@@ -135,9 +140,10 @@ func addRouteDef(method, route string, h interface{}) {
135
140
  body = instance.Interface()
136
141
  }
137
142
  RouteDefs = append(RouteDefs, RouteDefinition{
138
- Method: method,
143
+ Method: method,
139
- Path: route,
144
+ Path: route,
145
+ PathParams: pathParams,
140
- Params: body,
146
+ Params: body,
141
147
  })
142
148
  }
143
149