@strav/spring 0.3.4 → 0.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/templates/web/app/controllers/home_controller.ts +2 -16
- package/src/templates/web/index.ts +8 -2
- package/src/templates/web/public/builds/.gitkeep +1 -0
- package/src/templates/web/public/css/.gitkeep +1 -0
- package/src/templates/web/resources/css/app.scss +65 -164
- package/src/templates/web/resources/islands/counter.vue +0 -42
- package/src/templates/web/resources/views/layouts/app.strav +5 -19
- package/src/templates/web/resources/views/pages/home.strav +25 -46
- package/src/templates/web/start/routes.ts +1 -2
- package/src/templates/shared/.env +0 -14
- package/src/templates/shared/storage/cache/.gitkeep +0 -1
- package/src/templates/shared/storage/logs/.gitkeep +0 -1
- package/src/templates/web/resources/islands/user_manager.vue +0 -127
- package/src/templates/web/resources/islands/user_search.vue +0 -71
- package/src/templates/web/resources/views/pages/users.strav +0 -63
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@strav/spring",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Flagship framework scaffolding tool for the Strav ecosystem.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"README.md"
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@strav/kernel": "0.3.
|
|
25
|
+
"@strav/kernel": "0.3.6"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/bun": "latest"
|
|
@@ -1,24 +1,10 @@
|
|
|
1
1
|
import type { Context } from '@strav/http'
|
|
2
|
-
import { Controller } from './controller.ts'
|
|
3
|
-
import User from '../models/user.ts'
|
|
4
2
|
|
|
5
|
-
export default class HomeController
|
|
3
|
+
export default class HomeController {
|
|
6
4
|
async index(ctx: Context) {
|
|
7
|
-
const userCount = await User.count()
|
|
8
|
-
|
|
9
5
|
return ctx.view('pages/home', {
|
|
10
|
-
title: 'Welcome
|
|
11
|
-
userCount,
|
|
6
|
+
title: 'Welcome',
|
|
12
7
|
message: 'Welcome to your new Strav application!',
|
|
13
8
|
})
|
|
14
9
|
}
|
|
15
|
-
|
|
16
|
-
async users(ctx: Context) {
|
|
17
|
-
const users = await User.all()
|
|
18
|
-
|
|
19
|
-
return ctx.view('pages/users', {
|
|
20
|
-
title: 'Users',
|
|
21
|
-
users,
|
|
22
|
-
})
|
|
23
|
-
}
|
|
24
10
|
}
|
|
@@ -3,9 +3,15 @@ import { app } from '@strav/kernel'
|
|
|
3
3
|
import { IslandBuilder, ViewEngine } from '@strav/view'
|
|
4
4
|
import { providers } from './start/providers'
|
|
5
5
|
|
|
6
|
-
// Build islands + CSS before the server starts
|
|
6
|
+
// Build islands + CSS before the server starts
|
|
7
|
+
// Outputs: public/css/app.css (from SCSS) and public/builds/islands.js (Vue components)
|
|
7
8
|
const builder = new IslandBuilder({
|
|
8
|
-
css: {
|
|
9
|
+
css: {
|
|
10
|
+
entry: 'resources/css/app.scss',
|
|
11
|
+
outDir: './public/css',
|
|
12
|
+
outFile: 'app.css'
|
|
13
|
+
},
|
|
14
|
+
outDir: './public/builds',
|
|
9
15
|
})
|
|
10
16
|
|
|
11
17
|
// Register service providers
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# This directory will contain compiled Vue islands and manifest files
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# This directory will contain compiled CSS files
|
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
:root {
|
|
2
|
+
--bg: #f4f4f0;
|
|
3
|
+
--text: #1e293b;
|
|
4
|
+
--accent: #f97316;
|
|
5
|
+
--bg-feature: #e8e8e3;
|
|
6
|
+
--border-feature: #d8d8d0;
|
|
7
|
+
--border-link-hover: #abab9c;
|
|
8
|
+
}
|
|
3
9
|
|
|
4
10
|
* {
|
|
5
11
|
margin: 0;
|
|
@@ -8,169 +14,64 @@
|
|
|
8
14
|
}
|
|
9
15
|
|
|
10
16
|
body {
|
|
11
|
-
font-family:
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
.bg-gray-50 { background-color: #f9fafb; }
|
|
19
|
-
.bg-white { background-color: #ffffff; }
|
|
20
|
-
.bg-blue-600 { background-color: #2563eb; }
|
|
21
|
-
.bg-blue-700 { background-color: #1d4ed8; }
|
|
22
|
-
.bg-red-500 { background-color: #ef4444; }
|
|
23
|
-
.bg-red-600 { background-color: #dc2626; }
|
|
24
|
-
.bg-green-500 { background-color: #10b981; }
|
|
25
|
-
.bg-green-600 { background-color: #059669; }
|
|
26
|
-
.bg-gray-100 { background-color: #f3f4f6; }
|
|
27
|
-
.bg-yellow-50 { background-color: #fffbeb; }
|
|
28
|
-
|
|
29
|
-
.text-white { color: #ffffff; }
|
|
30
|
-
.text-gray-900 { color: #111827; }
|
|
31
|
-
.text-gray-600 { color: #4b5563; }
|
|
32
|
-
.text-gray-500 { color: #6b7280; }
|
|
33
|
-
.text-blue-600 { color: #2563eb; }
|
|
34
|
-
.text-green-600 { color: #059669; }
|
|
35
|
-
|
|
36
|
-
.text-xs { font-size: 0.75rem; }
|
|
37
|
-
.text-sm { font-size: 0.875rem; }
|
|
38
|
-
.text-base { font-size: 1rem; }
|
|
39
|
-
.text-lg { font-size: 1.125rem; }
|
|
40
|
-
.text-xl { font-size: 1.25rem; }
|
|
41
|
-
.text-2xl { font-size: 1.5rem; }
|
|
42
|
-
.text-4xl { font-size: 2.25rem; }
|
|
43
|
-
|
|
44
|
-
.font-bold { font-weight: 700; }
|
|
45
|
-
.font-semibold { font-weight: 600; }
|
|
46
|
-
.font-medium { font-weight: 500; }
|
|
47
|
-
|
|
48
|
-
.max-w-7xl { max-width: 80rem; }
|
|
49
|
-
.max-w-4xl { max-width: 56rem; }
|
|
50
|
-
.max-w-md { max-width: 28rem; }
|
|
51
|
-
.mx-auto { margin-left: auto; margin-right: auto; }
|
|
52
|
-
|
|
53
|
-
.px-4 { padding-left: 1rem; padding-right: 1rem; }
|
|
54
|
-
.py-6 { padding-top: 1.5rem; padding-bottom: 1.5rem; }
|
|
55
|
-
.p-6 { padding: 1.5rem; }
|
|
56
|
-
.px-6 { padding-left: 1.5rem; padding-right: 1.5rem; }
|
|
57
|
-
.py-4 { padding-top: 1rem; padding-bottom: 1rem; }
|
|
58
|
-
.px-3 { padding-left: 0.75rem; padding-right: 0.75rem; }
|
|
59
|
-
.py-1 { padding-top: 0.25rem; padding-bottom: 0.25rem; }
|
|
60
|
-
.py-2 { padding-top: 0.5rem; padding-bottom: 0.5rem; }
|
|
61
|
-
.py-12 { padding-top: 3rem; padding-bottom: 3rem; }
|
|
62
|
-
|
|
63
|
-
.mb-4 { margin-bottom: 1rem; }
|
|
64
|
-
.mb-8 { margin-bottom: 2rem; }
|
|
65
|
-
.mt-8 { margin-top: 2rem; }
|
|
66
|
-
.mt-2 { margin-top: 0.5rem; }
|
|
67
|
-
|
|
68
|
-
.flex { display: flex; }
|
|
69
|
-
.grid { display: grid; }
|
|
70
|
-
.grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
|
|
71
|
-
.grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
72
|
-
.gap-6 { gap: 1.5rem; }
|
|
73
|
-
.space-x-4 > * + * { margin-left: 1rem; }
|
|
74
|
-
.space-y-6 > * + * { margin-top: 1.5rem; }
|
|
75
|
-
.items-center { align-items: center; }
|
|
76
|
-
.justify-between { justify-content: space-between; }
|
|
77
|
-
|
|
78
|
-
.rounded { border-radius: 0.25rem; }
|
|
79
|
-
.rounded-lg { border-radius: 0.5rem; }
|
|
80
|
-
.shadow { box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); }
|
|
81
|
-
|
|
82
|
-
.border { border-width: 1px; }
|
|
83
|
-
.border-4 { border-width: 4px; }
|
|
84
|
-
.border-dashed { border-style: dashed; }
|
|
85
|
-
.border-gray-200 { border-color: #e5e7eb; }
|
|
86
|
-
|
|
87
|
-
.text-center { text-align: center; }
|
|
88
|
-
|
|
89
|
-
.hover\:bg-blue-700:hover { background-color: #1d4ed8; }
|
|
90
|
-
.hover\:bg-red-600:hover { background-color: #dc2626; }
|
|
91
|
-
.hover\:bg-green-600:hover { background-color: #059669; }
|
|
92
|
-
.hover\:text-gray-900:hover { color: #111827; }
|
|
93
|
-
|
|
94
|
-
.transition-colors {
|
|
95
|
-
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
|
|
96
|
-
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
97
|
-
transition-duration: 150ms;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/* Navigation styles */
|
|
101
|
-
nav {
|
|
102
|
-
border-bottom: 1px solid #e5e7eb;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
nav a {
|
|
106
|
-
text-decoration: none;
|
|
107
|
-
transition: color 0.15s ease-in-out;
|
|
17
|
+
font-family: "Barlow Semi Condensed", sans-serif;
|
|
18
|
+
font-weight: 400;
|
|
19
|
+
font-style: normal;
|
|
20
|
+
font-size: 15px;
|
|
21
|
+
color: var(--text);
|
|
22
|
+
background-color: var(--bg);
|
|
23
|
+
line-height: 1.8;
|
|
108
24
|
}
|
|
109
25
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
width: 100%;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
th, td {
|
|
117
|
-
text-align: left;
|
|
118
|
-
border-bottom: 1px solid #e5e7eb;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
th {
|
|
122
|
-
background-color: #f9fafb;
|
|
123
|
-
font-weight: 500;
|
|
124
|
-
text-transform: uppercase;
|
|
125
|
-
letter-spacing: 0.025em;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/* Form styles */
|
|
129
|
-
input[type="text"],
|
|
130
|
-
input[type="email"],
|
|
131
|
-
textarea {
|
|
132
|
-
border: 1px solid #d1d5db;
|
|
133
|
-
border-radius: 0.375rem;
|
|
134
|
-
padding: 0.5rem 0.75rem;
|
|
135
|
-
width: 100%;
|
|
136
|
-
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
input:focus,
|
|
140
|
-
textarea:focus {
|
|
141
|
-
outline: none;
|
|
142
|
-
border-color: #3b82f6;
|
|
143
|
-
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
button {
|
|
147
|
-
cursor: pointer;
|
|
148
|
-
border: none;
|
|
149
|
-
border-radius: 0.375rem;
|
|
150
|
-
font-weight: 500;
|
|
26
|
+
main {
|
|
27
|
+
width: 800px;
|
|
28
|
+
margin: 50px auto 0;
|
|
151
29
|
text-align: center;
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
30
|
+
h3 {
|
|
31
|
+
font-size: 50px;
|
|
32
|
+
font-weight: 400;
|
|
33
|
+
color: #374151;
|
|
34
|
+
margin-bottom: 30px;
|
|
35
|
+
em {
|
|
36
|
+
font-style: normal;
|
|
37
|
+
color: var(--accent);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
.features {
|
|
41
|
+
display: grid;
|
|
42
|
+
grid-template-columns: repeat(2, 1fr);
|
|
43
|
+
grid-template-rows: repeat(2, 1fr);
|
|
44
|
+
grid-column-gap: 20px;
|
|
45
|
+
grid-row-gap: 20px;
|
|
46
|
+
margin-top: 50px;
|
|
47
|
+
.feature {
|
|
48
|
+
padding: 20px;
|
|
49
|
+
background-color: var(--bg-feature);
|
|
50
|
+
border: 1px solid var(--border-feature);
|
|
51
|
+
border-radius: 10px;
|
|
52
|
+
text-align: left;
|
|
53
|
+
h4 {
|
|
54
|
+
text-align: left;
|
|
55
|
+
color: var(--accent);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
.links {
|
|
60
|
+
display: flex;
|
|
61
|
+
justify-content: center;
|
|
62
|
+
margin-top: 50px;
|
|
63
|
+
a {
|
|
64
|
+
display: inline-block;
|
|
65
|
+
padding: 7px 15px;
|
|
66
|
+
margin: 0 20px;
|
|
67
|
+
border-radius: 10px;
|
|
68
|
+
text-decoration: none;
|
|
69
|
+
color: var(--text);
|
|
70
|
+
background-color: var(--bg-feature);
|
|
71
|
+
border: 1px solid var(--border-feature);
|
|
72
|
+
&:hover {
|
|
73
|
+
border-color: var(--border-link-hover);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
176
77
|
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="flex items-center space-x-4">
|
|
3
|
-
<button
|
|
4
|
-
@click="decrement"
|
|
5
|
-
class="px-3 py-1 bg-red-500 text-white rounded hover:bg-red-600 transition-colors"
|
|
6
|
-
>
|
|
7
|
-
-
|
|
8
|
-
</button>
|
|
9
|
-
|
|
10
|
-
<span class="text-xl font-bold text-gray-900 min-w-[3rem] text-center">
|
|
11
|
-
{{ count }}
|
|
12
|
-
</span>
|
|
13
|
-
|
|
14
|
-
<button
|
|
15
|
-
@click="increment"
|
|
16
|
-
class="px-3 py-1 bg-green-500 text-white rounded hover:bg-green-600 transition-colors"
|
|
17
|
-
>
|
|
18
|
-
+
|
|
19
|
-
</button>
|
|
20
|
-
|
|
21
|
-
<span class="text-sm text-gray-600 ml-4">{{ label }}</span>
|
|
22
|
-
</div>
|
|
23
|
-
</template>
|
|
24
|
-
|
|
25
|
-
<script setup>
|
|
26
|
-
import { ref } from 'vue'
|
|
27
|
-
|
|
28
|
-
const props = defineProps({
|
|
29
|
-
initial: { type: Number, default: 0 },
|
|
30
|
-
label: { type: String, default: 'Counter' }
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
const count = ref(props.initial)
|
|
34
|
-
|
|
35
|
-
function increment() {
|
|
36
|
-
count.value++
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function decrement() {
|
|
40
|
-
count.value--
|
|
41
|
-
}
|
|
42
|
-
</script>
|
|
@@ -3,30 +3,16 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title>{{ title }} -
|
|
6
|
+
<title>{{ title }} - Strav Spring</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
9
|
+
<link href="https://fonts.googleapis.com/css2?family=Barlow+Semi+Condensed:ital,wght@0,400;0,500;1,400;1,500&display=swap" rel="stylesheet">
|
|
7
10
|
<link rel="stylesheet" href="{{ asset('/css/app.css') }}">
|
|
8
|
-
@stack('styles')
|
|
9
11
|
</head>
|
|
10
12
|
<body class="bg-gray-50">
|
|
11
|
-
<
|
|
12
|
-
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
13
|
-
<div class="flex justify-between h-16">
|
|
14
|
-
<div class="flex items-center">
|
|
15
|
-
<a href="/" class="text-xl font-bold text-gray-900">__PROJECT_NAME__</a>
|
|
16
|
-
</div>
|
|
17
|
-
<div class="flex items-center space-x-4">
|
|
18
|
-
<a href="/" class="text-gray-600 hover:text-gray-900">Home</a>
|
|
19
|
-
<a href="/users" class="text-gray-600 hover:text-gray-900">Users</a>
|
|
20
|
-
</div>
|
|
21
|
-
</div>
|
|
22
|
-
</div>
|
|
23
|
-
</nav>
|
|
24
|
-
|
|
25
|
-
<main class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
|
|
13
|
+
<main>
|
|
26
14
|
@show('content')
|
|
27
15
|
</main>
|
|
28
|
-
|
|
29
16
|
@islands
|
|
30
|
-
@stack('scripts')
|
|
31
17
|
</body>
|
|
32
18
|
</html>
|
|
@@ -1,52 +1,31 @@
|
|
|
1
1
|
@layout('layouts/app')
|
|
2
2
|
|
|
3
3
|
@section('content')
|
|
4
|
-
<
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
</div>
|
|
22
|
-
|
|
23
|
-
<div class="bg-white p-6 rounded-lg shadow">
|
|
24
|
-
<h3 class="text-lg font-semibold text-gray-900 mb-2">TypeScript</h3>
|
|
25
|
-
<p class="text-gray-600">Full type safety everywhere</p>
|
|
26
|
-
</div>
|
|
27
|
-
</div>
|
|
28
|
-
|
|
29
|
-
{{-- Demo Vue Islands --}}
|
|
30
|
-
<div class="space-y-6">
|
|
31
|
-
<h2 class="text-2xl font-bold text-gray-900">Vue Islands Demo</h2>
|
|
32
|
-
|
|
33
|
-
<div class="bg-white p-6 rounded-lg shadow max-w-md mx-auto">
|
|
34
|
-
<h3 class="text-lg font-semibold mb-4">Counter Component</h3>
|
|
35
|
-
<vue:counter :initial="5" label="Click me!" />
|
|
36
|
-
</div>
|
|
37
|
-
|
|
38
|
-
<div class="bg-white p-6 rounded-lg shadow max-w-md mx-auto">
|
|
39
|
-
<h3 class="text-lg font-semibold mb-4">User Search</h3>
|
|
40
|
-
<vue:user-search placeholder="Search users..." :userCount="{{ userCount }}" />
|
|
41
|
-
</div>
|
|
42
|
-
</div>
|
|
43
|
-
|
|
44
|
-
<div class="mt-8">
|
|
45
|
-
<a href="/users" class="inline-flex items-center px-4 py-2 border border-transparent text-base font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700">
|
|
46
|
-
View Users
|
|
47
|
-
</a>
|
|
48
|
-
</div>
|
|
49
|
-
</div>
|
|
4
|
+
<h3>Welcome <em>Strav Spring</em></h3>
|
|
5
|
+
<p>
|
|
6
|
+
You're all set. Your project is live and ready to build. <br/>
|
|
7
|
+
Blazing fast, powered by Bun and Vue.js
|
|
8
|
+
</p>
|
|
9
|
+
<div class="features">
|
|
10
|
+
<div class="feature">
|
|
11
|
+
<h4>Schema-First Architecture</h4>
|
|
12
|
+
<p>Define your database schema once, automatically migrations and models. No more manual SQL and model boilerplate.</p>
|
|
13
|
+
</div>
|
|
14
|
+
<div class="feature">
|
|
15
|
+
<h4>Vue Islands</h4>
|
|
16
|
+
<p>Embed Vue.js components as interactive islands within server-rendered HTML. Get reactivity where you need it without the complexity of a full SPA.</p>
|
|
17
|
+
</div>
|
|
18
|
+
<div class="feature">
|
|
19
|
+
<h4>AI as a First-Class Citizen</h4>
|
|
20
|
+
<p>Built-in AI agents, workflows, and memory management with multi-provider support (Claude, GPT, Gemini). Create AI-powered features with a single import.</p>
|
|
50
21
|
</div>
|
|
22
|
+
<div class="feature">
|
|
23
|
+
<h4>Zero-Config Real-Time Stack</h4>
|
|
24
|
+
<p>WebSocket broadcasting, multi-channel notifications, and transactional email built-in. From chat to live updates, ship real-time features instantly.</p>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
<div class="links">
|
|
28
|
+
<a href="https://spring.strav.dev/docs" target="_blank">Documentation</a>
|
|
29
|
+
<a href="https://github.com/stravigor/strav" target="_blank">GitHub</a>
|
|
51
30
|
</div>
|
|
52
31
|
@end
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { router } from '@strav/http'
|
|
2
|
-
import HomeController from '../app/controllers/home_controller'
|
|
2
|
+
import HomeController from '../app/controllers/home_controller.ts'
|
|
3
3
|
|
|
4
4
|
// Web routes
|
|
5
5
|
router.get('/', [HomeController, 'index'])
|
|
6
|
-
router.get('/users', [HomeController, 'users'])
|
|
7
6
|
|
|
8
7
|
// Health check endpoint
|
|
9
8
|
router.get('/health', async (ctx) => {
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
APP_ENV=local
|
|
2
|
-
APP_KEY=__APP_KEY__
|
|
3
|
-
APP_DEBUG=true
|
|
4
|
-
APP_URL=http://localhost:3000
|
|
5
|
-
APP_PORT=3000
|
|
6
|
-
|
|
7
|
-
DB_HOST=127.0.0.1
|
|
8
|
-
DB_PORT=5432
|
|
9
|
-
DB_USER=liva
|
|
10
|
-
DB_PASSWORD=password1234
|
|
11
|
-
DB_DATABASE=__DB_NAME__
|
|
12
|
-
|
|
13
|
-
SESSION_SECRET=__APP_KEY__
|
|
14
|
-
SESSION_COOKIE_NAME=session
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
# This file keeps the cache directory in git
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
# This file keeps the logs directory in git
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="bg-white p-6 rounded-lg shadow">
|
|
3
|
-
<h3 class="text-lg font-semibold text-gray-900 mb-4">Interactive User Management</h3>
|
|
4
|
-
|
|
5
|
-
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
6
|
-
<!-- Add User Form -->
|
|
7
|
-
<div>
|
|
8
|
-
<h4 class="font-medium text-gray-900 mb-3">Add New User</h4>
|
|
9
|
-
<form @submit.prevent="addUser" class="space-y-3">
|
|
10
|
-
<input
|
|
11
|
-
v-model="newUser.name"
|
|
12
|
-
type="text"
|
|
13
|
-
placeholder="Full Name"
|
|
14
|
-
required
|
|
15
|
-
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500"
|
|
16
|
-
/>
|
|
17
|
-
<input
|
|
18
|
-
v-model="newUser.email"
|
|
19
|
-
type="email"
|
|
20
|
-
placeholder="Email Address"
|
|
21
|
-
required
|
|
22
|
-
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500"
|
|
23
|
-
/>
|
|
24
|
-
<button
|
|
25
|
-
type="submit"
|
|
26
|
-
:disabled="isLoading"
|
|
27
|
-
class="w-full px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50"
|
|
28
|
-
>
|
|
29
|
-
{{ isLoading ? 'Adding...' : 'Add User' }}
|
|
30
|
-
</button>
|
|
31
|
-
</form>
|
|
32
|
-
</div>
|
|
33
|
-
|
|
34
|
-
<!-- User Stats -->
|
|
35
|
-
<div>
|
|
36
|
-
<h4 class="font-medium text-gray-900 mb-3">Statistics</h4>
|
|
37
|
-
<div class="space-y-2">
|
|
38
|
-
<div class="flex justify-between">
|
|
39
|
-
<span class="text-gray-600">Total Users:</span>
|
|
40
|
-
<span class="font-semibold">{{ users.length }}</span>
|
|
41
|
-
</div>
|
|
42
|
-
<div class="flex justify-between">
|
|
43
|
-
<span class="text-gray-600">Users Added:</span>
|
|
44
|
-
<span class="font-semibold text-green-600">+{{ addedCount }}</span>
|
|
45
|
-
</div>
|
|
46
|
-
<div class="flex justify-between">
|
|
47
|
-
<span class="text-gray-600">Last Added:</span>
|
|
48
|
-
<span class="text-sm text-gray-500">
|
|
49
|
-
{{ lastAdded || 'None yet' }}
|
|
50
|
-
</span>
|
|
51
|
-
</div>
|
|
52
|
-
</div>
|
|
53
|
-
</div>
|
|
54
|
-
</div>
|
|
55
|
-
|
|
56
|
-
<!-- Recent Users List -->
|
|
57
|
-
<div class="mt-6">
|
|
58
|
-
<h4 class="font-medium text-gray-900 mb-3">Recent Users</h4>
|
|
59
|
-
<div v-if="users.length === 0" class="text-center text-gray-500 py-8">
|
|
60
|
-
No users yet. Add one above!
|
|
61
|
-
</div>
|
|
62
|
-
<div v-else class="space-y-2 max-h-48 overflow-y-auto">
|
|
63
|
-
<div
|
|
64
|
-
v-for="(user, index) in users.slice(-5).reverse()"
|
|
65
|
-
:key="user.id || index"
|
|
66
|
-
class="flex items-center justify-between p-3 bg-gray-50 rounded-md"
|
|
67
|
-
>
|
|
68
|
-
<div>
|
|
69
|
-
<div class="font-medium text-gray-900">{{ user.name }}</div>
|
|
70
|
-
<div class="text-sm text-gray-500">{{ user.email }}</div>
|
|
71
|
-
</div>
|
|
72
|
-
<span v-if="index === 0" class="text-xs bg-green-100 text-green-800 px-2 py-1 rounded">
|
|
73
|
-
Latest
|
|
74
|
-
</span>
|
|
75
|
-
</div>
|
|
76
|
-
</div>
|
|
77
|
-
</div>
|
|
78
|
-
</div>
|
|
79
|
-
</template>
|
|
80
|
-
|
|
81
|
-
<script setup>
|
|
82
|
-
import { ref, reactive, computed } from 'vue'
|
|
83
|
-
|
|
84
|
-
const props = defineProps({
|
|
85
|
-
initialUsers: { type: String, default: '[]' }
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
// Parse initial users from JSON string
|
|
89
|
-
const users = ref(JSON.parse(props.initialUsers))
|
|
90
|
-
const addedCount = ref(0)
|
|
91
|
-
const isLoading = ref(false)
|
|
92
|
-
|
|
93
|
-
const newUser = reactive({
|
|
94
|
-
name: '',
|
|
95
|
-
email: ''
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
const lastAdded = computed(() => {
|
|
99
|
-
if (addedCount.value === 0) return null
|
|
100
|
-
const latest = users.value[users.value.length - 1]
|
|
101
|
-
return latest ? `${latest.name} (${latest.email})` : null
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
async function addUser() {
|
|
105
|
-
if (!newUser.name || !newUser.email) return
|
|
106
|
-
|
|
107
|
-
isLoading.value = true
|
|
108
|
-
|
|
109
|
-
// Simulate API call
|
|
110
|
-
await new Promise(resolve => setTimeout(resolve, 500))
|
|
111
|
-
|
|
112
|
-
// Add the new user
|
|
113
|
-
users.value.push({
|
|
114
|
-
id: crypto.randomUUID(),
|
|
115
|
-
name: newUser.name,
|
|
116
|
-
email: newUser.email,
|
|
117
|
-
created_at: new Date()
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
addedCount.value++
|
|
121
|
-
|
|
122
|
-
// Reset form
|
|
123
|
-
newUser.name = ''
|
|
124
|
-
newUser.email = ''
|
|
125
|
-
isLoading.value = false
|
|
126
|
-
}
|
|
127
|
-
</script>
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="space-y-4">
|
|
3
|
-
<div class="relative">
|
|
4
|
-
<input
|
|
5
|
-
v-model="searchTerm"
|
|
6
|
-
type="text"
|
|
7
|
-
:placeholder="placeholder"
|
|
8
|
-
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
9
|
-
/>
|
|
10
|
-
<div class="absolute inset-y-0 right-0 pr-3 flex items-center">
|
|
11
|
-
<svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
12
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
|
13
|
-
</svg>
|
|
14
|
-
</div>
|
|
15
|
-
</div>
|
|
16
|
-
|
|
17
|
-
<div class="text-sm text-gray-600">
|
|
18
|
-
<span v-if="searchTerm">
|
|
19
|
-
Searching for "{{ searchTerm }}"...
|
|
20
|
-
</span>
|
|
21
|
-
<span v-else>
|
|
22
|
-
Search through {{ userCount }} users
|
|
23
|
-
</span>
|
|
24
|
-
</div>
|
|
25
|
-
|
|
26
|
-
<div v-if="searchTerm && searchResults.length > 0" class="border rounded-lg p-4 bg-gray-50">
|
|
27
|
-
<p class="text-sm font-medium text-gray-900 mb-2">Search Results:</p>
|
|
28
|
-
<ul class="space-y-1">
|
|
29
|
-
<li v-for="result in searchResults" :key="result" class="text-sm text-gray-700">
|
|
30
|
-
• {{ result }}
|
|
31
|
-
</li>
|
|
32
|
-
</ul>
|
|
33
|
-
</div>
|
|
34
|
-
|
|
35
|
-
<div v-else-if="searchTerm" class="border rounded-lg p-4 bg-gray-50 text-center text-gray-500">
|
|
36
|
-
No results found for "{{ searchTerm }}"
|
|
37
|
-
</div>
|
|
38
|
-
</div>
|
|
39
|
-
</template>
|
|
40
|
-
|
|
41
|
-
<script setup>
|
|
42
|
-
import { ref, computed, watch } from 'vue'
|
|
43
|
-
|
|
44
|
-
const props = defineProps({
|
|
45
|
-
placeholder: { type: String, default: 'Search...' },
|
|
46
|
-
userCount: { type: Number, default: 0 }
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
const searchTerm = ref('')
|
|
50
|
-
const searchResults = computed(() => {
|
|
51
|
-
if (!searchTerm.value) return []
|
|
52
|
-
|
|
53
|
-
// Mock search results - in a real app, this would call an API
|
|
54
|
-
const mockResults = [
|
|
55
|
-
'John Doe (john@example.com)',
|
|
56
|
-
'Jane Smith (jane@example.com)',
|
|
57
|
-
'Bob Johnson (bob@example.com)'
|
|
58
|
-
]
|
|
59
|
-
|
|
60
|
-
return mockResults.filter(result =>
|
|
61
|
-
result.toLowerCase().includes(searchTerm.value.toLowerCase())
|
|
62
|
-
)
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
// Demo of reactive watchers
|
|
66
|
-
watch(searchTerm, (newTerm) => {
|
|
67
|
-
if (newTerm) {
|
|
68
|
-
console.log(`Searching for: ${newTerm}`)
|
|
69
|
-
}
|
|
70
|
-
})
|
|
71
|
-
</script>
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
@layout('layouts/app')
|
|
2
|
-
|
|
3
|
-
@section('content')
|
|
4
|
-
<div class="px-4 py-6 sm:px-0">
|
|
5
|
-
<div class="sm:flex sm:items-center">
|
|
6
|
-
<div class="sm:flex-auto">
|
|
7
|
-
<h1 class="text-2xl font-semibold text-gray-900">Users</h1>
|
|
8
|
-
<p class="mt-2 text-sm text-gray-700">
|
|
9
|
-
A list of all users in your application.
|
|
10
|
-
</p>
|
|
11
|
-
</div>
|
|
12
|
-
</div>
|
|
13
|
-
|
|
14
|
-
<div class="mt-8 flow-root">
|
|
15
|
-
<div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
|
16
|
-
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
|
|
17
|
-
<div class="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg">
|
|
18
|
-
<table class="min-w-full divide-y divide-gray-300">
|
|
19
|
-
<thead class="bg-gray-50">
|
|
20
|
-
<tr>
|
|
21
|
-
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wide">Name</th>
|
|
22
|
-
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wide">Email</th>
|
|
23
|
-
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wide">Created</th>
|
|
24
|
-
</tr>
|
|
25
|
-
</thead>
|
|
26
|
-
<tbody class="bg-white divide-y divide-gray-200">
|
|
27
|
-
@each(user in users)
|
|
28
|
-
<tr @class([
|
|
29
|
-
'hover:bg-gray-50',
|
|
30
|
-
'bg-yellow-50' => $first,
|
|
31
|
-
])>
|
|
32
|
-
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
|
33
|
-
{{ user.name }}
|
|
34
|
-
</td>
|
|
35
|
-
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
36
|
-
{{ user.email }}
|
|
37
|
-
</td>
|
|
38
|
-
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
39
|
-
{{ user.created_at?.toLocaleDateString() }}
|
|
40
|
-
</td>
|
|
41
|
-
</tr>
|
|
42
|
-
@end
|
|
43
|
-
|
|
44
|
-
@if(users.length === 0)
|
|
45
|
-
<tr>
|
|
46
|
-
<td colspan="3" class="px-6 py-12 text-center text-sm text-gray-500">
|
|
47
|
-
No users found. Run <code class="bg-gray-100 px-2 py-1 rounded">bun strav seed</code> to add sample data.
|
|
48
|
-
</td>
|
|
49
|
-
</tr>
|
|
50
|
-
@end
|
|
51
|
-
</tbody>
|
|
52
|
-
</table>
|
|
53
|
-
</div>
|
|
54
|
-
</div>
|
|
55
|
-
</div>
|
|
56
|
-
</div>
|
|
57
|
-
|
|
58
|
-
{{-- Interactive user management with Vue island --}}
|
|
59
|
-
<div class="mt-8">
|
|
60
|
-
<vue:user-manager :initialUsers="{{ JSON.stringify(users) }}" />
|
|
61
|
-
</div>
|
|
62
|
-
</div>
|
|
63
|
-
@end
|