user domains API, .value on input, fix flex sizing
This commit is contained in:
@@ -28,7 +28,6 @@ Issues:
|
|||||||
3. Certain properties like `scale` and `rotate` don't apply to the `active` pseudo-class because they rely on mouse_enter and mouse_exit events
|
3. Certain properties like `scale` and `rotate` don't apply to the `active` pseudo-class because they rely on mouse_enter and mouse_exit events
|
||||||
4. `<div style="bg-[#3b82f6] w-[100px] h-[100px] flex hover:scale-110 transition hover:rotate-45">Box</div>` something like this has the "Box" text (presumably the PanelContainer) as the target of the hover, not the div itself (which has the w/h size)
|
4. `<div style="bg-[#3b82f6] w-[100px] h-[100px] flex hover:scale-110 transition hover:rotate-45">Box</div>` something like this has the "Box" text (presumably the PanelContainer) as the target of the hover, not the div itself (which has the w/h size)
|
||||||
5. font in button doesn't comply with CSS, its the projects default
|
5. font in button doesn't comply with CSS, its the projects default
|
||||||
6. Flex containers, ironically enough, make the page unresponsive. This happens because of our custom `AutoSizingFlexContainer.gd` script, which aims to set a Godot UI size to the flex containers based on their content. However, they don't get resized when the window is resized, leading to unresponsiveness. The fact that we're setting the `custom_minimum_size` is not the root cause, but rather the fact that the script doesn't update the size when the window is resized - or, more likely, I just don't understand how flexbox works.
|
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- **< input />** is sort-of inline in normal web. We render it as a block element (new-line).
|
- **< input />** is sort-of inline in normal web. We render it as a block element (new-line).
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ This is a Domain Management API built with Rust (Actix Web) and PostgreSQL. It p
|
|||||||
- [GET /auth/me](#get-authme)
|
- [GET /auth/me](#get-authme)
|
||||||
- [POST /auth/invite](#post-authinvite)
|
- [POST /auth/invite](#post-authinvite)
|
||||||
- [POST /auth/redeem-invite](#post-authredeem-invite)
|
- [POST /auth/redeem-invite](#post-authredeem-invite)
|
||||||
|
- [GET /auth/domains](#get-authdomains) 🔒
|
||||||
- [Domain Endpoints](#domain-endpoints)
|
- [Domain Endpoints](#domain-endpoints)
|
||||||
- [GET /](#get-)
|
- [GET /](#get-)
|
||||||
- [POST /domain](#post-domain) 🔒
|
- [POST /domain](#post-domain) 🔒
|
||||||
@@ -127,6 +128,50 @@ Redeem an invite code to get 3 additional domain registrations. Requires authent
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### GET /auth/domains 🔒
|
||||||
|
|
||||||
|
Get all domains owned by the authenticated user, including their status. Requires `Authorization: Bearer <token>` header.
|
||||||
|
|
||||||
|
**Query Parameters:**
|
||||||
|
- `page` - Page number (default: 1)
|
||||||
|
- `limit` - Items per page (default: 100, max: 1000)
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"domains": [
|
||||||
|
{
|
||||||
|
"name": "myawesome",
|
||||||
|
"tld": "dev",
|
||||||
|
"ip": "192.168.1.100",
|
||||||
|
"status": "approved",
|
||||||
|
"denial_reason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "pending",
|
||||||
|
"tld": "fr",
|
||||||
|
"ip": "10.0.0.1",
|
||||||
|
"status": "pending",
|
||||||
|
"denial_reason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "rejected",
|
||||||
|
"tld": "mf",
|
||||||
|
"ip": "172.16.0.1",
|
||||||
|
"status": "denied",
|
||||||
|
"denial_reason": "Invalid IP address"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"page": 1,
|
||||||
|
"limit": 100
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Status Values:**
|
||||||
|
- `pending` - Domain is awaiting approval
|
||||||
|
- `approved` - Domain has been approved and is active
|
||||||
|
- `denied` - Domain was rejected (see `denial_reason` for details)
|
||||||
|
|
||||||
## Domain Endpoints
|
## Domain Endpoints
|
||||||
|
|
||||||
### GET /
|
### GET /
|
||||||
@@ -159,15 +204,6 @@ Submit a domain for approval. Requires authentication and consumes one registrat
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"message": "Domain registration submitted for approval",
|
|
||||||
"domain": "myawesome.dev",
|
|
||||||
"status": "pending"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Error Responses:**
|
**Error Responses:**
|
||||||
- `401 Unauthorized` - Missing or invalid JWT token
|
- `401 Unauthorized` - Missing or invalid JWT token
|
||||||
- `400 Bad Request` - No registrations remaining, invalid domain, or offensive name
|
- `400 Bad Request` - No registrations remaining, invalid domain, or offensive name
|
||||||
|
|||||||
@@ -26,15 +26,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.primary-btn {
|
.primary-btn {
|
||||||
p-3 rounded-lg font-medium cursor-pointer transition-colors bg-[#dc2626] text-white
|
p-3 rounded-lg font-medium cursor-pointer transition-colors bg-[#dc2626] text-white w-32 h-12
|
||||||
}
|
}
|
||||||
|
|
||||||
.success-btn {
|
.success-btn {
|
||||||
p-3 rounded-lg font-medium cursor-pointer transition-colors bg-[#ef4444] text-white
|
p-3 rounded-lg font-medium cursor-pointer transition-colors bg-[#ef4444] text-white w-32 h-12
|
||||||
}
|
}
|
||||||
|
|
||||||
.danger-btn {
|
.danger-btn {
|
||||||
p-3 rounded-lg font-medium cursor-pointer transition-colors bg-[#b91c1c] text-white
|
p-3 rounded-lg font-medium cursor-pointer transition-colors bg-[#b91c1c] text-white w-32 h-12
|
||||||
}
|
}
|
||||||
|
|
||||||
.secondary-btn {
|
.secondary-btn {
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.warning-btn {
|
.warning-btn {
|
||||||
p-3 rounded-lg font-medium cursor-pointer transition-colors bg-[#dc2626] text-white
|
p-3 rounded-lg font-medium cursor-pointer transition-colors bg-[#dc2626] text-white w-32 h-12
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group {
|
.form-group {
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.stats-card {
|
.stats-card {
|
||||||
bg-[#1f1f1f] p-4 rounded-lg border border-[#dc2626]
|
p-4 rounded-lg border border-[#dc2626]
|
||||||
}
|
}
|
||||||
|
|
||||||
.domain-item {
|
.domain-item {
|
||||||
@@ -98,48 +98,10 @@
|
|||||||
<div style="stats-card mb-6">
|
<div style="stats-card mb-6">
|
||||||
<div style="flex justify-between items-center w-full">
|
<div style="flex justify-between items-center w-full">
|
||||||
<p id="user-info" style="text-white text-lg font-semibold">Loading...</p>
|
<p id="user-info" style="text-white text-lg font-semibold">Loading...</p>
|
||||||
<button id="logout-btn" style="secondary-btn">Logout</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="card mb-6">
|
|
||||||
<h2 style="mb-4">Register New Domain</h2>
|
|
||||||
<div style="form-group">
|
|
||||||
<p>Domain Name:</p>
|
|
||||||
<input id="domain-name" type="text" style="form-input" placeholder="myawesome" />
|
|
||||||
</div>
|
|
||||||
<div style="form-group">
|
|
||||||
<p>Select TLD:</p>
|
|
||||||
<div id="tld-selector" style="tld-selector">
|
|
||||||
<p id="tld-loading">Loading TLDs...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style="form-group">
|
|
||||||
<p>IP Address:</p>
|
|
||||||
<input id="domain-ip" type="text" style="form-input" placeholder="192.168.1.100" />
|
|
||||||
</div>
|
|
||||||
<div id="domain-error" style="error-text hidden mb-2"></div>
|
|
||||||
<button id="submit-domain-btn" style="success-btn">Submit for Approval</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="card mb-6">
|
|
||||||
<h2>Invite System</h2>
|
|
||||||
<p style="text-[#6b7280] mb-4">Create invite codes to share with friends, or redeem codes to get more domain
|
|
||||||
registrations.</p>
|
|
||||||
|
|
||||||
<p id="invite-code-display" style="invite-code-display mt-2">Placeholder</p>
|
|
||||||
|
|
||||||
<div style="flex flex-col gap-4 items-center justify-center mx-auto">
|
|
||||||
<h3>Create Invite</h3>
|
|
||||||
<button id="create-invite-btn" style="warning-btn">Generate Invite Code</button>
|
|
||||||
</div>
|
|
||||||
<div style="flex flex-col gap-4 mx-auto">
|
|
||||||
<h3>Redeem Invite</h3>
|
|
||||||
<div style="flex gap-2">
|
<div style="flex gap-2">
|
||||||
<input id="invite-code-input" type="text" style="form-input" placeholder="Enter invite code" />
|
<button id="new-btn" style="success-btn">New</button>
|
||||||
<button id="redeem-invite-btn" style="primary-btn">Redeem</button>
|
<button id="logout-btn" style="secondary-btn">Logout</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="redeem-error" style="error-text hidden mt-2"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,12 @@
|
|||||||
local user = nil
|
local user = nil
|
||||||
local domains = {}
|
local domains = {}
|
||||||
local tlds = {}
|
|
||||||
local authToken = nil
|
local authToken = nil
|
||||||
|
|
||||||
local userInfo = gurt.select('#user-info')
|
local userInfo = gurt.select('#user-info')
|
||||||
local domainsList = gurt.select('#domains-list')
|
local domainsList = gurt.select('#domains-list')
|
||||||
local tldSelector = gurt.select('#tld-selector')
|
|
||||||
local loadingElement = gurt.select('#tld-loading')
|
local loadingElement = gurt.select('#tld-loading')
|
||||||
local displayElement = gurt.select('#invite-code-display')
|
|
||||||
local options
|
|
||||||
|
|
||||||
displayElement:hide()
|
local options
|
||||||
|
|
||||||
local function showError(elementId, message)
|
local function showError(elementId, message)
|
||||||
local element = gurt.select('#' .. elementId)
|
local element = gurt.select('#' .. elementId)
|
||||||
@@ -29,57 +25,17 @@ local function updateUserInfo()
|
|||||||
userInfo.text = 'Welcome, ' .. user.username .. '!'
|
userInfo.text = 'Welcome, ' .. user.username .. '!'
|
||||||
end
|
end
|
||||||
|
|
||||||
local function renderTLDSelector()
|
|
||||||
loadingElement:remove()
|
|
||||||
|
|
||||||
tldSelector.text = ''
|
|
||||||
local i = 1
|
|
||||||
local total = #tlds
|
|
||||||
local intervalId
|
|
||||||
|
|
||||||
intervalId = gurt.setInterval(function()
|
|
||||||
if i > total then
|
|
||||||
gurt.clearInterval(intervalId)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local tld = tlds[i]
|
|
||||||
local option = gurt.create('button', {
|
|
||||||
text = '.' .. tld,
|
|
||||||
style = 'tld-option',
|
|
||||||
['data-tld'] = tld
|
|
||||||
})
|
|
||||||
|
|
||||||
tldSelector:append(option)
|
|
||||||
|
|
||||||
option:on('click', function()
|
|
||||||
-- Clear previous selection
|
|
||||||
if not options then
|
|
||||||
options = gurt.selectAll('.tld-option')
|
|
||||||
end
|
|
||||||
|
|
||||||
for j = 1, #options do
|
|
||||||
if options[j].classList:contains('tld-selected') then
|
|
||||||
options[j].classList:remove('tld-selected')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Select this option
|
|
||||||
option.classList:add('tld-selected')
|
|
||||||
end)
|
|
||||||
i = i + 1
|
|
||||||
end, 16)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function renderDomains()
|
local function renderDomains()
|
||||||
local loadingElement = gurt.select('#domains-loading')
|
local loadingElement = gurt.select('#domains-loading')
|
||||||
loadingElement:remove()
|
if loadingElement then
|
||||||
|
loadingElement:remove()
|
||||||
|
end
|
||||||
|
|
||||||
domainsList.text = ''
|
domainsList.text = ''
|
||||||
|
|
||||||
if #domains == 0 then
|
if #domains == 0 then
|
||||||
local emptyMessage = gurt.create('div', {
|
local emptyMessage = gurt.create('div', {
|
||||||
text = 'No domains registered yet. Submit your first domain below!',
|
text = 'No domains registered yet. Click "New" to register your first domain!',
|
||||||
style = 'text-center text-[#6b7280] py-8'
|
style = 'text-center text-[#6b7280] py-8'
|
||||||
})
|
})
|
||||||
domainsList:append(emptyMessage)
|
domainsList:append(emptyMessage)
|
||||||
@@ -91,7 +47,7 @@ local function renderDomains()
|
|||||||
style = 'domain-item'
|
style = 'domain-item'
|
||||||
})
|
})
|
||||||
|
|
||||||
local domainInfo = gurt.create('div', {})
|
local domainInfo = gurt.create('div', { style = 'w-full' })
|
||||||
|
|
||||||
local domainName = gurt.create('div', {
|
local domainName = gurt.create('div', {
|
||||||
text = domain.name .. '.' .. domain.tld,
|
text = domain.name .. '.' .. domain.tld,
|
||||||
@@ -103,8 +59,14 @@ local function renderDomains()
|
|||||||
style = 'text-[#6b7280]'
|
style = 'text-[#6b7280]'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
local domainStatus = gurt.create('div', {
|
||||||
|
text = 'Status: ' .. (domain.status or 'Unknown'),
|
||||||
|
style = 'text-[#6b7280]'
|
||||||
|
})
|
||||||
|
|
||||||
domainInfo:append(domainName)
|
domainInfo:append(domainName)
|
||||||
domainInfo:append(domainIP)
|
domainInfo:append(domainIP)
|
||||||
|
domainInfo:append(domainStatus)
|
||||||
|
|
||||||
local actions = gurt.create('div', {
|
local actions = gurt.create('div', {
|
||||||
style = 'flex gap-2'
|
style = 'flex gap-2'
|
||||||
@@ -143,7 +105,7 @@ end
|
|||||||
|
|
||||||
local function loadDomains()
|
local function loadDomains()
|
||||||
print('Loading domains...')
|
print('Loading domains...')
|
||||||
local response = fetch('gurt://localhost:8877/domains?page=1&size=100', {
|
local response = fetch('gurt://localhost:8877/auth/domains?page=1&size=100', {
|
||||||
headers = {
|
headers = {
|
||||||
Authorization = 'Bearer ' .. authToken
|
Authorization = 'Bearer ' .. authToken
|
||||||
}
|
}
|
||||||
@@ -159,19 +121,6 @@ local function loadDomains()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function loadTLDs()
|
|
||||||
print('Loading available TLDs...')
|
|
||||||
local response = fetch('gurt://localhost:8877/tlds')
|
|
||||||
|
|
||||||
if response:ok() then
|
|
||||||
tlds = response:json()
|
|
||||||
print('Loaded ' .. #tlds .. ' TLDs')
|
|
||||||
renderTLDSelector()
|
|
||||||
else
|
|
||||||
print('Failed to load TLDs: ' .. response:text())
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function checkAuth()
|
local function checkAuth()
|
||||||
authToken = gurt.crumbs.get("auth_token")
|
authToken = gurt.crumbs.get("auth_token")
|
||||||
|
|
||||||
@@ -188,7 +137,6 @@ local function checkAuth()
|
|||||||
print('Authentication successful for user: ' .. user.username)
|
print('Authentication successful for user: ' .. user.username)
|
||||||
updateUserInfo()
|
updateUserInfo()
|
||||||
loadDomains()
|
loadDomains()
|
||||||
loadTLDs()
|
|
||||||
else
|
else
|
||||||
print('Token invalid, redirecting to login...')
|
print('Token invalid, redirecting to login...')
|
||||||
gurt.crumbs.delete('auth_token')
|
gurt.crumbs.delete('auth_token')
|
||||||
@@ -206,126 +154,13 @@ local function logout()
|
|||||||
gurt.location.goto("../")
|
gurt.location.goto("../")
|
||||||
end
|
end
|
||||||
|
|
||||||
local function submitDomain(name, tld, ip)
|
local function goToRegister()
|
||||||
hideError('domain-error')
|
gurt.location.goto("/register.html")
|
||||||
print('Submitting domain: ' .. name .. '.' .. tld)
|
|
||||||
|
|
||||||
local response = fetch('gurt://localhost:8877/domain', {
|
|
||||||
method = 'POST',
|
|
||||||
headers = {
|
|
||||||
['Content-Type'] = 'application/json',
|
|
||||||
Authorization = 'Bearer ' .. authToken
|
|
||||||
},
|
|
||||||
body = JSON.stringify({ name = name, tld = tld, ip = ip })
|
|
||||||
})
|
|
||||||
|
|
||||||
if response:ok() then
|
|
||||||
local data = response:json()
|
|
||||||
print('Domain submitted successfully: ' .. data.domain)
|
|
||||||
|
|
||||||
-- Update user registrations remaining
|
|
||||||
user.registrations_remaining = user.registrations_remaining - 1
|
|
||||||
updateUserInfo()
|
|
||||||
|
|
||||||
-- Clear form
|
|
||||||
gurt.select('#domain-name').text = ''
|
|
||||||
gurt.select('#domain-ip').text = ''
|
|
||||||
|
|
||||||
-- Refresh domains list
|
|
||||||
loadDomains()
|
|
||||||
else
|
|
||||||
local error = response:text()
|
|
||||||
showError('domain-error', 'Domain submission failed: ' .. error)
|
|
||||||
print('Domain submission failed: ' .. error)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function createInvite()
|
|
||||||
print('Creating invite code...')
|
|
||||||
local response = fetch('gurt://localhost:8877/auth/invite', {
|
|
||||||
method = 'POST',
|
|
||||||
headers = {
|
|
||||||
Authorization = 'Bearer ' .. authToken
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if response:ok() then
|
|
||||||
local data = response:json()
|
|
||||||
local inviteCode = data.invite_code
|
|
||||||
displayElement.text = 'Invite code: ' .. inviteCode .. ' (copied to clipboard)'
|
|
||||||
displayElement:show()
|
|
||||||
Clipboard.write(inviteCode)
|
|
||||||
print('Invite code created and copied to clipboard: ' .. inviteCode)
|
|
||||||
else
|
|
||||||
print('Failed to create invite: ' .. response:text())
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function redeemInvite(code)
|
|
||||||
hideError('redeem-error')
|
|
||||||
print('Redeeming invite code: ' .. code)
|
|
||||||
|
|
||||||
local response = fetch('gurt://localhost:8877/auth/redeem-invite', {
|
|
||||||
method = 'POST',
|
|
||||||
headers = {
|
|
||||||
['Content-Type'] = 'application/json',
|
|
||||||
Authorization = 'Bearer ' .. authToken
|
|
||||||
},
|
|
||||||
body = JSON.stringify({ invite_code = code })
|
|
||||||
})
|
|
||||||
|
|
||||||
if response:ok() then
|
|
||||||
local data = response:json()
|
|
||||||
print('Invite redeemed: +' .. data.registrations_added .. ' registrations')
|
|
||||||
|
|
||||||
-- Update user info
|
|
||||||
user.registrations_remaining = user.registrations_remaining + data.registrations_added
|
|
||||||
updateUserInfo()
|
|
||||||
|
|
||||||
-- Clear form
|
|
||||||
gurt.select('#invite-code-input').text = ''
|
|
||||||
else
|
|
||||||
local error = response:text()
|
|
||||||
showError('redeem-error', 'Failed to redeem invite: ' .. error)
|
|
||||||
print('Failed to redeem invite: ' .. error)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Event handlers
|
-- Event handlers
|
||||||
gurt.select('#logout-btn'):on('click', logout)
|
gurt.select('#logout-btn'):on('click', logout)
|
||||||
|
gurt.select('#new-btn'):on('click', goToRegister)
|
||||||
gurt.select('#submit-domain-btn'):on('click', function()
|
|
||||||
local name = gurt.select('#domain-name').text
|
|
||||||
local ip = gurt.select('#domain-ip').text
|
|
||||||
local selectedTLD = gurt.select('.tld-selected')
|
|
||||||
|
|
||||||
if not name or name == '' then
|
|
||||||
showError('domain-error', 'Domain name is required')
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
if not ip or ip == '' then
|
|
||||||
showError('domain-error', 'IP address is required')
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
if not selectedTLD then
|
|
||||||
showError('domain-error', 'Please select a TLD')
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local tld = selectedTLD:getAttribute('data-tld')
|
|
||||||
submitDomain(name, tld, ip)
|
|
||||||
end)
|
|
||||||
|
|
||||||
gurt.select('#create-invite-btn'):on('click', createInvite)
|
|
||||||
|
|
||||||
gurt.select('#redeem-invite-btn'):on('click', function()
|
|
||||||
local code = gurt.select('#invite-code-input').text
|
|
||||||
if code and code ~= '' then
|
|
||||||
redeemInvite(code)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
-- Initialize
|
-- Initialize
|
||||||
print('Dashboard initialized')
|
print('Dashboard initialized')
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
#log-output { text-white p-4 rounded-md mt-4 font-mono max-h-40 }
|
#log-output { text-white p-4 rounded-md mt-4 font-mono max-h-40 }
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<script src="script.lua" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
136
dns/frontend/register.html
Normal file
136
dns/frontend/register.html
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
<head>
|
||||||
|
<title>Register Domain</title>
|
||||||
|
<icon src="https://cdn-icons-png.flaticon.com/512/295/295128.png">
|
||||||
|
<meta name="theme-color" content="#0891b2">
|
||||||
|
<meta name="description" content="Register a new domain">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
bg-[#171616] font-sans text-white
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
text-[#ef4444] text-3xl font-bold text-center
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
text-[#dc2626] text-xl font-semibold
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
text-[#fca5a5] text-lg font-medium
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
bg-[#262626] p-6 rounded-lg shadow-lg max-w-6xl
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-btn {
|
||||||
|
p-3 rounded-lg font-medium cursor-pointer transition-colors bg-[#dc2626] text-white
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-btn {
|
||||||
|
p-3 rounded-lg font-medium cursor-pointer transition-colors bg-[#ef4444] text-white
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary-btn {
|
||||||
|
p-3 rounded-lg font-medium cursor-pointer transition-colors bg-[#525252] text-white w-32 h-12
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-btn {
|
||||||
|
p-3 rounded-lg font-medium cursor-pointer transition-colors bg-[#dc2626] text-white
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
flex flex-col gap-2 mb-4 w-full
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
w-full p-3 border border-gray-600 rounded-md bg-[#374151] text-white active:border-red-500
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
bg-[#262626] p-6 rounded-lg shadow border border-gray-700
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card {
|
||||||
|
bg-[#1f1f1f] p-4 rounded-lg border border-[#dc2626]
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-text {
|
||||||
|
text-[#fca5a5] text-sm
|
||||||
|
}
|
||||||
|
|
||||||
|
.tld-selector {
|
||||||
|
flex flex-wrap gap-2
|
||||||
|
}
|
||||||
|
|
||||||
|
.tld-option {
|
||||||
|
px-3 py-1 rounded border border-gray-600 cursor-pointer bg-[#374151] text-white w-12 h-12
|
||||||
|
}
|
||||||
|
|
||||||
|
.tld-selected {
|
||||||
|
bg-[#dc2626] text-white
|
||||||
|
}
|
||||||
|
|
||||||
|
.invite-code-display {
|
||||||
|
bg-[#374151] p-3 rounded font-mono text-center mb-2 text-white
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script src="register.lua" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div style="container mt-6">
|
||||||
|
<div style="stats-card mb-6">
|
||||||
|
<div style="flex justify-between items-center w-full">
|
||||||
|
<p id="user-info" style="text-white text-lg font-semibold">Loading...</p>
|
||||||
|
<div style="flex gap-2">
|
||||||
|
<button id="dashboard-btn" style="secondary-btn">Dashboard</button>
|
||||||
|
<button id="logout-btn" style="secondary-btn">Logout</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="card mb-6">
|
||||||
|
<h2 id="remaining" style="mb-4">Register New Domain</h2>
|
||||||
|
<div style="form-group">
|
||||||
|
<p>Domain Name:</p>
|
||||||
|
<input id="domain-name" type="text" style="form-input" placeholder="myawesome" />
|
||||||
|
</div>
|
||||||
|
<div style="form-group">
|
||||||
|
<p>Select TLD:</p>
|
||||||
|
<div id="tld-selector" style="tld-selector">
|
||||||
|
<p id="tld-loading">Loading TLDs...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="form-group">
|
||||||
|
<p>IP Address:</p>
|
||||||
|
<input id="domain-ip" type="text" style="form-input" placeholder="192.168.1.100" />
|
||||||
|
</div>
|
||||||
|
<div id="domain-error" style="error-text hidden mb-2"></div>
|
||||||
|
<button id="submit-domain-btn" style="success-btn">Submit for Approval</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="card mb-6">
|
||||||
|
<h2>Invite System</h2>
|
||||||
|
<p style="text-[#6b7280] mb-4">Create invite codes to share with friends, or redeem codes to get more domain registrations.</p>
|
||||||
|
|
||||||
|
<p id="invite-code-display" style="invite-code-display mt-2">Placeholder</p>
|
||||||
|
|
||||||
|
<div style="flex flex-col gap-4 items-center justify-center mx-auto">
|
||||||
|
<h3>Create Invite</h3>
|
||||||
|
<button id="create-invite-btn" style="warning-btn">Generate Invite Code</button>
|
||||||
|
</div>
|
||||||
|
<div style="flex flex-col gap-4 mx-auto">
|
||||||
|
<h3>Redeem Invite</h3>
|
||||||
|
<div style="flex gap-2">
|
||||||
|
<input id="invite-code-input" type="text" style="form-input" placeholder="Enter invite code" />
|
||||||
|
<button id="redeem-invite-btn" style="primary-btn">Redeem</button>
|
||||||
|
</div>
|
||||||
|
<div id="redeem-error" style="error-text hidden mt-2"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
257
dns/frontend/register.lua
Normal file
257
dns/frontend/register.lua
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
local user = nil
|
||||||
|
local tlds = {}
|
||||||
|
local authToken = nil
|
||||||
|
|
||||||
|
local userInfo = gurt.select('#user-info')
|
||||||
|
local tldSelector = gurt.select('#tld-selector')
|
||||||
|
local loadingElement = gurt.select('#tld-loading')
|
||||||
|
local displayElement = gurt.select('#invite-code-display')
|
||||||
|
local remainingElement = gurt.select('#remaining')
|
||||||
|
|
||||||
|
local options
|
||||||
|
|
||||||
|
displayElement:hide()
|
||||||
|
|
||||||
|
local function showError(elementId, message)
|
||||||
|
local element = gurt.select('#' .. elementId)
|
||||||
|
|
||||||
|
element.text = message
|
||||||
|
element.classList:remove('hidden')
|
||||||
|
end
|
||||||
|
|
||||||
|
local function hideError(elementId)
|
||||||
|
local element = gurt.select('#' .. elementId)
|
||||||
|
|
||||||
|
element.classList:add('hidden')
|
||||||
|
end
|
||||||
|
|
||||||
|
local function updateUserInfo()
|
||||||
|
userInfo.text = 'Welcome, ' .. user.username .. '!'
|
||||||
|
remainingElement.text = 'Register New Domain (' .. user.registrations_remaining .. ' remaining)'
|
||||||
|
end
|
||||||
|
|
||||||
|
local function renderTLDSelector()
|
||||||
|
loadingElement:remove()
|
||||||
|
|
||||||
|
tldSelector.text = ''
|
||||||
|
local i = 1
|
||||||
|
local total = #tlds
|
||||||
|
local intervalId
|
||||||
|
|
||||||
|
intervalId = gurt.setInterval(function()
|
||||||
|
if i > total then
|
||||||
|
gurt.clearInterval(intervalId)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local tld = tlds[i]
|
||||||
|
local option = gurt.create('button', {
|
||||||
|
text = '.' .. tld,
|
||||||
|
style = 'tld-option',
|
||||||
|
['data-tld'] = tld
|
||||||
|
})
|
||||||
|
|
||||||
|
tldSelector:append(option)
|
||||||
|
|
||||||
|
option:on('click', function()
|
||||||
|
-- Clear previous selection
|
||||||
|
if not options then
|
||||||
|
options = gurt.selectAll('.tld-option')
|
||||||
|
end
|
||||||
|
|
||||||
|
for j = 1, #options do
|
||||||
|
if options[j].classList:contains('tld-selected') then
|
||||||
|
options[j].classList:remove('tld-selected')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Select this option
|
||||||
|
option.classList:add('tld-selected')
|
||||||
|
end)
|
||||||
|
i = i + 1
|
||||||
|
end, 16)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function loadTLDs()
|
||||||
|
print('Loading available TLDs...')
|
||||||
|
local response = fetch('gurt://localhost:8877/tlds')
|
||||||
|
|
||||||
|
if response:ok() then
|
||||||
|
tlds = response:json()
|
||||||
|
print('Loaded ' .. #tlds .. ' TLDs')
|
||||||
|
renderTLDSelector()
|
||||||
|
else
|
||||||
|
print('Failed to load TLDs: ' .. response:text())
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function checkAuth()
|
||||||
|
authToken = gurt.crumbs.get("auth_token")
|
||||||
|
|
||||||
|
if authToken then
|
||||||
|
print('Found auth token, checking validity...')
|
||||||
|
local response = fetch('gurt://localhost:8877/auth/me', {
|
||||||
|
headers = {
|
||||||
|
Authorization = 'Bearer ' .. authToken
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if response:ok() then
|
||||||
|
user = response:json()
|
||||||
|
print('Authentication successful for user: ' .. user.username)
|
||||||
|
updateUserInfo()
|
||||||
|
loadTLDs()
|
||||||
|
else
|
||||||
|
print('Token invalid, redirecting to login...')
|
||||||
|
gurt.crumbs.delete('auth_token')
|
||||||
|
gurt.location.goto('../')
|
||||||
|
end
|
||||||
|
else
|
||||||
|
print('No auth token found, redirecting to login...')
|
||||||
|
gurt.location.goto('../')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function logout()
|
||||||
|
gurt.crumbs.delete('auth_token')
|
||||||
|
print('Logged out successfully')
|
||||||
|
gurt.location.goto("../")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function goToDashboard()
|
||||||
|
gurt.location.goto("/dashboard.html")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function submitDomain(name, tld, ip)
|
||||||
|
hideError('domain-error')
|
||||||
|
print('Submitting domain: ' .. name .. '.' .. tld)
|
||||||
|
|
||||||
|
local response = fetch('gurt://localhost:8877/domain', {
|
||||||
|
method = 'POST',
|
||||||
|
headers = {
|
||||||
|
['Content-Type'] = 'application/json',
|
||||||
|
Authorization = 'Bearer ' .. authToken
|
||||||
|
},
|
||||||
|
body = JSON.stringify({ name = name, tld = tld, ip = ip })
|
||||||
|
})
|
||||||
|
|
||||||
|
if response:ok() then
|
||||||
|
print('Domain submitted successfully.')
|
||||||
|
|
||||||
|
-- Update user registrations remaining
|
||||||
|
user.registrations_remaining = user.registrations_remaining - 1
|
||||||
|
updateUserInfo()
|
||||||
|
|
||||||
|
-- Clear form
|
||||||
|
gurt.select('#domain-name').text = ''
|
||||||
|
gurt.select('#domain-ip').text = ''
|
||||||
|
|
||||||
|
-- Redirect to dashboard
|
||||||
|
gurt.location.goto('/dashboard.html')
|
||||||
|
else
|
||||||
|
local error = response:text()
|
||||||
|
showError('domain-error', 'Domain submission failed: ' .. error)
|
||||||
|
print('Domain submission failed: ' .. error)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function createInvite()
|
||||||
|
print('Creating invite code...')
|
||||||
|
local response = fetch('gurt://localhost:8877/auth/invite', {
|
||||||
|
method = 'POST',
|
||||||
|
headers = {
|
||||||
|
Authorization = 'Bearer ' .. authToken
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if response:ok() then
|
||||||
|
local data = response:json()
|
||||||
|
local inviteCode = data.invite_code
|
||||||
|
displayElement.text = 'Invite code: ' .. inviteCode .. ' (copied to clipboard)'
|
||||||
|
displayElement:show()
|
||||||
|
Clipboard.write(inviteCode)
|
||||||
|
print('Invite code created and copied to clipboard: ' .. inviteCode)
|
||||||
|
else
|
||||||
|
print('Failed to create invite: ' .. response:text())
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function redeemInvite(code)
|
||||||
|
hideError('redeem-error')
|
||||||
|
print('Redeeming invite code: ' .. code)
|
||||||
|
|
||||||
|
local response = fetch('gurt://localhost:8877/auth/redeem-invite', {
|
||||||
|
method = 'POST',
|
||||||
|
headers = {
|
||||||
|
['Content-Type'] = 'application/json',
|
||||||
|
Authorization = 'Bearer ' .. authToken
|
||||||
|
},
|
||||||
|
body = JSON.stringify({ invite_code = code })
|
||||||
|
})
|
||||||
|
|
||||||
|
if response:ok() then
|
||||||
|
local data = response:json()
|
||||||
|
print('Invite redeemed: +' .. data.registrations_added .. ' registrations')
|
||||||
|
|
||||||
|
-- Update user info
|
||||||
|
user.registrations_remaining = user.registrations_remaining + data.registrations_added
|
||||||
|
updateUserInfo()
|
||||||
|
|
||||||
|
-- Clear form
|
||||||
|
gurt.select('#invite-code-input').text = ''
|
||||||
|
else
|
||||||
|
local error = response:text()
|
||||||
|
showError('redeem-error', 'Failed to redeem invite: ' .. error)
|
||||||
|
print('Failed to redeem invite: ' .. error)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Event handlers
|
||||||
|
gurt.select('#logout-btn'):on('click', logout)
|
||||||
|
gurt.select('#dashboard-btn'):on('click', goToDashboard)
|
||||||
|
|
||||||
|
gurt.select('#submit-domain-btn'):on('click', function()
|
||||||
|
local name = gurt.select('#domain-name').value
|
||||||
|
local ip = gurt.select('#domain-ip').value
|
||||||
|
local selectedTLD = gurt.select('.tld-selected')
|
||||||
|
|
||||||
|
print('Submit domain button clicked')
|
||||||
|
print('Input name:', name)
|
||||||
|
print('Input IP:', ip)
|
||||||
|
print('Selected TLD element:', selectedTLD)
|
||||||
|
|
||||||
|
if not name or name == '' then
|
||||||
|
print('Validation failed: Domain name is required')
|
||||||
|
showError('domain-error', 'Domain name is required')
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if not ip or ip == '' then
|
||||||
|
print('Validation failed: IP address is required')
|
||||||
|
showError('domain-error', 'IP address is required')
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if not selectedTLD then
|
||||||
|
print('Validation failed: No TLD selected')
|
||||||
|
showError('domain-error', 'Please select a TLD')
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local tld = selectedTLD:getAttribute('data-tld')
|
||||||
|
print('Submitting domain with name:', name, 'tld:', tld, 'ip:', ip)
|
||||||
|
submitDomain(name, tld, ip)
|
||||||
|
end)
|
||||||
|
|
||||||
|
gurt.select('#create-invite-btn'):on('click', createInvite)
|
||||||
|
|
||||||
|
gurt.select('#redeem-invite-btn'):on('click', function()
|
||||||
|
local code = gurt.select('#invite-code-input').text
|
||||||
|
if code and code ~= '' then
|
||||||
|
redeemInvite(code)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Initialize
|
||||||
|
print('Register page initialized')
|
||||||
|
checkAuth()
|
||||||
@@ -93,6 +93,7 @@ enum HandlerType {
|
|||||||
CreateDomain,
|
CreateDomain,
|
||||||
UpdateDomain,
|
UpdateDomain,
|
||||||
DeleteDomain,
|
DeleteDomain,
|
||||||
|
GetUserDomains,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GurtHandler for AppHandler {
|
impl GurtHandler for AppHandler {
|
||||||
@@ -128,6 +129,7 @@ impl GurtHandler for AppHandler {
|
|||||||
HandlerType::RedeemInvite => handle_authenticated!(ctx, app_state, auth_routes::redeem_invite),
|
HandlerType::RedeemInvite => handle_authenticated!(ctx, app_state, auth_routes::redeem_invite),
|
||||||
HandlerType::CreateDomainInvite => handle_authenticated!(ctx, app_state, auth_routes::create_domain_invite),
|
HandlerType::CreateDomainInvite => handle_authenticated!(ctx, app_state, auth_routes::create_domain_invite),
|
||||||
HandlerType::RedeemDomainInvite => handle_authenticated!(ctx, app_state, auth_routes::redeem_domain_invite),
|
HandlerType::RedeemDomainInvite => handle_authenticated!(ctx, app_state, auth_routes::redeem_domain_invite),
|
||||||
|
HandlerType::GetUserDomains => handle_authenticated!(ctx, app_state, routes::get_user_domains),
|
||||||
HandlerType::CreateDomain => {
|
HandlerType::CreateDomain => {
|
||||||
// Check rate limit first
|
// Check rate limit first
|
||||||
if let Some(ref rate_limit_state) = rate_limit_state {
|
if let Some(ref rate_limit_state) = rate_limit_state {
|
||||||
@@ -205,6 +207,7 @@ pub async fn start(cli: crate::Cli) -> std::io::Result<()> {
|
|||||||
.route(Route::post("/auth/redeem-invite"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::RedeemInvite })
|
.route(Route::post("/auth/redeem-invite"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::RedeemInvite })
|
||||||
.route(Route::post("/auth/create-domain-invite"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::CreateDomainInvite })
|
.route(Route::post("/auth/create-domain-invite"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::CreateDomainInvite })
|
||||||
.route(Route::post("/auth/redeem-domain-invite"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::RedeemDomainInvite })
|
.route(Route::post("/auth/redeem-domain-invite"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::RedeemDomainInvite })
|
||||||
|
.route(Route::get("/auth/domains"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::GetUserDomains })
|
||||||
.route(Route::post("/domain"), AppHandler { app_state: app_state.clone(), rate_limit_state: Some(rate_limit_state), handler_type: HandlerType::CreateDomain })
|
.route(Route::post("/domain"), AppHandler { app_state: app_state.clone(), rate_limit_state: Some(rate_limit_state), handler_type: HandlerType::CreateDomain })
|
||||||
.route(Route::put("/domain/*"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::UpdateDomain })
|
.route(Route::put("/domain/*"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::UpdateDomain })
|
||||||
.route(Route::delete("/domain/*"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::DeleteDomain });
|
.route(Route::delete("/domain/*"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::DeleteDomain });
|
||||||
|
|||||||
@@ -101,3 +101,19 @@ pub(crate) struct DomainList {
|
|||||||
pub(crate) domain: String,
|
pub(crate) domain: String,
|
||||||
pub(crate) taken: bool,
|
pub(crate) taken: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub(crate) struct UserDomain {
|
||||||
|
pub(crate) name: String,
|
||||||
|
pub(crate) tld: String,
|
||||||
|
pub(crate) ip: String,
|
||||||
|
pub(crate) status: String,
|
||||||
|
pub(crate) denial_reason: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub(crate) struct UserDomainResponse {
|
||||||
|
pub(crate) domains: Vec<UserDomain>,
|
||||||
|
pub(crate) page: u32,
|
||||||
|
pub(crate) limit: u32,
|
||||||
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ pub(crate) async fn create_logic(domain: Domain, user_id: i32, app: &AppState) -
|
|||||||
}
|
}
|
||||||
|
|
||||||
let existing_count: i64 = sqlx::query_scalar(
|
let existing_count: i64 = sqlx::query_scalar(
|
||||||
"SELECT COUNT(*) FROM domains WHERE name = ? AND tld = ?"
|
"SELECT COUNT(*) FROM domains WHERE name = $1 AND tld = $2"
|
||||||
)
|
)
|
||||||
.bind(&domain.name)
|
.bind(&domain.name)
|
||||||
.bind(&domain.tld)
|
.bind(&domain.tld)
|
||||||
@@ -306,6 +306,56 @@ pub(crate) async fn delete_domain(ctx: &ServerContext, app_state: AppState, clai
|
|||||||
Ok(GurtResponse::ok().with_string_body("Domain deleted successfully"))
|
Ok(GurtResponse::ok().with_string_body("Domain deleted successfully"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn get_user_domains(ctx: &ServerContext, app_state: AppState, claims: Claims) -> Result<GurtResponse> {
|
||||||
|
// Parse pagination from query parameters
|
||||||
|
let path = ctx.path();
|
||||||
|
let query_params = if let Some(query_start) = path.find('?') {
|
||||||
|
let query_string = &path[query_start + 1..];
|
||||||
|
parse_query_string(query_string)
|
||||||
|
} else {
|
||||||
|
HashMap::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
let page = query_params.get("page")
|
||||||
|
.and_then(|p| p.parse::<u32>().ok())
|
||||||
|
.unwrap_or(1)
|
||||||
|
.max(1);
|
||||||
|
|
||||||
|
let page_size = query_params.get("limit")
|
||||||
|
.and_then(|l| l.parse::<u32>().ok())
|
||||||
|
.unwrap_or(100)
|
||||||
|
.clamp(1, 1000);
|
||||||
|
|
||||||
|
let offset = (page - 1) * page_size;
|
||||||
|
|
||||||
|
let domains: Vec<Domain> = sqlx::query_as::<_, Domain>(
|
||||||
|
"SELECT id, name, tld, ip, user_id, status, denial_reason, created_at FROM domains WHERE user_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3"
|
||||||
|
)
|
||||||
|
.bind(claims.user_id)
|
||||||
|
.bind(page_size as i64)
|
||||||
|
.bind(offset as i64)
|
||||||
|
.fetch_all(&app_state.db)
|
||||||
|
.await
|
||||||
|
.map_err(|_| GurtError::invalid_message("Database error"))?;
|
||||||
|
|
||||||
|
let response_domains: Vec<UserDomain> = domains.into_iter().map(|domain| {
|
||||||
|
UserDomain {
|
||||||
|
name: domain.name,
|
||||||
|
tld: domain.tld,
|
||||||
|
ip: domain.ip,
|
||||||
|
status: domain.status.unwrap_or_else(|| "pending".to_string()),
|
||||||
|
denial_reason: domain.denial_reason,
|
||||||
|
}
|
||||||
|
}).collect();
|
||||||
|
|
||||||
|
let response = UserDomainResponse {
|
||||||
|
domains: response_domains,
|
||||||
|
page,
|
||||||
|
limit: page_size,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(GurtResponse::ok().with_json_body(&response)?)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
#[derive(serde::Serialize)]
|
||||||
struct Error {
|
struct Error {
|
||||||
|
|||||||
@@ -66,77 +66,55 @@ SpinBox/styles/down_background_pressed = SubResource("StyleBoxEmpty_xxc4c")
|
|||||||
SpinBox/styles/up_background_hovered = SubResource("StyleBoxEmpty_unki5")
|
SpinBox/styles/up_background_hovered = SubResource("StyleBoxEmpty_unki5")
|
||||||
SpinBox/styles/up_background_pressed = SubResource("StyleBoxEmpty_3wmat")
|
SpinBox/styles/up_background_pressed = SubResource("StyleBoxEmpty_3wmat")
|
||||||
|
|
||||||
[node name="input" type="Control"]
|
[node name="VBoxContainer" type="VBoxContainer"]
|
||||||
layout_mode = 3
|
offset_right = 40.0
|
||||||
anchors_preset = 15
|
offset_bottom = 40.0
|
||||||
anchor_right = 1.0
|
|
||||||
anchor_bottom = 1.0
|
|
||||||
offset_right = -1791.0
|
|
||||||
offset_bottom = -996.0
|
|
||||||
grow_horizontal = 2
|
|
||||||
grow_vertical = 2
|
|
||||||
size_flags_horizontal = 3
|
|
||||||
size_flags_vertical = 0
|
|
||||||
script = ExtResource("1_input")
|
script = ExtResource("1_input")
|
||||||
|
|
||||||
[node name="LineEdit" type="LineEdit" parent="."]
|
[node name="LineEdit" type="LineEdit" parent="."]
|
||||||
visible = false
|
visible = false
|
||||||
layout_mode = 1
|
layout_mode = 2
|
||||||
offset_right = 200.0
|
|
||||||
offset_bottom = 35.0
|
|
||||||
theme = ExtResource("2_theme")
|
theme = ExtResource("2_theme")
|
||||||
placeholder_text = "Enter text..."
|
placeholder_text = "Enter text..."
|
||||||
caret_blink = true
|
caret_blink = true
|
||||||
|
|
||||||
[node name="CheckBox" type="CheckBox" parent="."]
|
[node name="CheckBox" type="CheckBox" parent="."]
|
||||||
visible = false
|
visible = false
|
||||||
layout_mode = 0
|
layout_mode = 2
|
||||||
offset_right = 31.0
|
|
||||||
offset_bottom = 31.0
|
|
||||||
theme = ExtResource("2_theme")
|
theme = ExtResource("2_theme")
|
||||||
flat = true
|
flat = true
|
||||||
|
|
||||||
[node name="RadioButton" type="CheckBox" parent="."]
|
[node name="RadioButton" type="CheckBox" parent="."]
|
||||||
visible = false
|
visible = false
|
||||||
layout_mode = 0
|
layout_mode = 2
|
||||||
offset_right = 31.0
|
|
||||||
offset_bottom = 31.0
|
|
||||||
theme = ExtResource("2_theme")
|
theme = ExtResource("2_theme")
|
||||||
theme_override_constants/icon_max_width = 24
|
theme_override_constants/icon_max_width = 24
|
||||||
button_group = SubResource("ButtonGroup_06us3")
|
button_group = SubResource("ButtonGroup_06us3")
|
||||||
flat = true
|
flat = true
|
||||||
|
|
||||||
[node name="ColorPickerButton" type="ColorPickerButton" parent="."]
|
[node name="ColorPickerButton" type="ColorPickerButton" parent="."]
|
||||||
layout_mode = 0
|
layout_mode = 2
|
||||||
offset_right = 83.0
|
|
||||||
offset_bottom = 35.0
|
|
||||||
toggle_mode = false
|
toggle_mode = false
|
||||||
|
|
||||||
[node name="DateButton" parent="." instance=ExtResource("3_a88g6")]
|
[node name="DateButton" parent="." instance=ExtResource("3_a88g6")]
|
||||||
visible = false
|
visible = false
|
||||||
layout_mode = 0
|
layout_mode = 2
|
||||||
|
|
||||||
[node name="HSlider" type="HSlider" parent="."]
|
[node name="HSlider" type="HSlider" parent="."]
|
||||||
visible = false
|
visible = false
|
||||||
layout_mode = 0
|
layout_mode = 2
|
||||||
offset_right = 200.0
|
|
||||||
offset_bottom = 35.0
|
|
||||||
theme = ExtResource("2_theme")
|
theme = ExtResource("2_theme")
|
||||||
value = 50.0
|
value = 50.0
|
||||||
|
|
||||||
[node name="SpinBox" type="SpinBox" parent="."]
|
[node name="SpinBox" type="SpinBox" parent="."]
|
||||||
visible = false
|
visible = false
|
||||||
layout_mode = 0
|
layout_mode = 2
|
||||||
offset_right = 94.5625
|
|
||||||
offset_bottom = 35.0
|
|
||||||
theme = SubResource("Theme_poopw")
|
theme = SubResource("Theme_poopw")
|
||||||
min_value = -99999.0
|
min_value = -99999.0
|
||||||
max_value = 99999.0
|
max_value = 99999.0
|
||||||
|
|
||||||
[node name="FileContainer" type="HBoxContainer" parent="."]
|
[node name="FileContainer" type="HBoxContainer" parent="."]
|
||||||
layout_mode = 0
|
layout_mode = 2
|
||||||
offset_right = 300.0
|
|
||||||
offset_bottom = 35.0
|
|
||||||
|
|
||||||
[node name="FileButton" type="Button" parent="FileContainer"]
|
[node name="FileButton" type="Button" parent="FileContainer"]
|
||||||
custom_minimum_size = Vector2(100, 0)
|
custom_minimum_size = Vector2(100, 0)
|
||||||
@@ -159,5 +137,3 @@ ok_button_text = "Open"
|
|||||||
file_mode = 0
|
file_mode = 0
|
||||||
access = 2
|
access = 2
|
||||||
use_native_dialog = true
|
use_native_dialog = true
|
||||||
|
|
||||||
[connection signal="popup_closed" from="ColorPickerButton" to="." method="_on_color_picker_popup_closed"]
|
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ func _resort() -> void:
|
|||||||
|
|
||||||
if not auto_size_width:
|
if not auto_size_width:
|
||||||
available_width = calculate_available_dimension(true)
|
available_width = calculate_available_dimension(true)
|
||||||
elif flex_wrap == FlexContainer.FlexWrap.Wrap:
|
elif flex_wrap != FlexContainer.FlexWrap.NoWrap:
|
||||||
available_width = get_parent_or_fallback_size(true)
|
available_width = get_parent_or_fallback_size(true)
|
||||||
|
|
||||||
if not auto_size_height:
|
if not auto_size_height:
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ func get_element_styles_with_inheritance(element: HTMLElement, event: String = "
|
|||||||
styles[property] = inline_parsed[property]
|
styles[property] = inline_parsed[property]
|
||||||
|
|
||||||
# Inherit certain properties from parent elements
|
# Inherit certain properties from parent elements
|
||||||
var inheritable_properties = ["width", "height", "font-size", "color", "font-family", "cursor", "font-bold", "font-italic", "underline"]
|
var inheritable_properties = ["font-size", "color", "font-family", "cursor", "font-bold", "font-italic", "underline"]
|
||||||
var parent_element = element.parent
|
var parent_element = element.parent
|
||||||
while parent_element:
|
while parent_element:
|
||||||
var parent_styles = get_element_styles_internal(parent_element, event)
|
var parent_styles = get_element_styles_internal(parent_element, event)
|
||||||
|
|||||||
@@ -663,10 +663,7 @@ func _handle_text_setting(operation: Dictionary):
|
|||||||
var text_node = get_dom_node(dom_node, "text")
|
var text_node = get_dom_node(dom_node, "text")
|
||||||
if text_node:
|
if text_node:
|
||||||
if text_node is RichTextLabel:
|
if text_node is RichTextLabel:
|
||||||
var formatted_text = element.get_bbcode_formatted_text(dom_parser)
|
StyleManager.apply_styles_to_label(text_node, dom_parser.get_element_styles_with_inheritance(element, "", []), element, dom_parser, text)
|
||||||
formatted_text = "[font_size=24]%s[/font_size]" % formatted_text
|
|
||||||
|
|
||||||
text_node.text = formatted_text
|
|
||||||
text_node.call_deferred("_auto_resize_to_content")
|
text_node.call_deferred("_auto_resize_to_content")
|
||||||
elif text_node.has_method("set_text"):
|
elif text_node.has_method("set_text"):
|
||||||
text_node.set_text(text)
|
text_node.set_text(text)
|
||||||
@@ -677,10 +674,7 @@ func _handle_text_setting(operation: Dictionary):
|
|||||||
else:
|
else:
|
||||||
var rich_text_label = _find_rich_text_label_recursive(dom_node)
|
var rich_text_label = _find_rich_text_label_recursive(dom_node)
|
||||||
if rich_text_label:
|
if rich_text_label:
|
||||||
var formatted_text = element.get_bbcode_formatted_text(dom_parser)
|
StyleManager.apply_styles_to_label(rich_text_label, dom_parser.get_element_styles_with_inheritance(element, "", []), element, dom_parser, text)
|
||||||
formatted_text = "[font_size=24]%s[/font_size]" % formatted_text
|
|
||||||
|
|
||||||
rich_text_label.text = formatted_text
|
|
||||||
rich_text_label.call_deferred("_auto_resize_to_content")
|
rich_text_label.call_deferred("_auto_resize_to_content")
|
||||||
|
|
||||||
func _find_rich_text_label_recursive(node: Node) -> RichTextLabel:
|
func _find_rich_text_label_recursive(node: Node) -> RichTextLabel:
|
||||||
|
|||||||
@@ -60,55 +60,24 @@ static func apply_element_styles(node: Control, element: HTMLParser.HTMLElement,
|
|||||||
if styles.has("height"):
|
if styles.has("height"):
|
||||||
height = parse_size(styles["height"])
|
height = parse_size(styles["height"])
|
||||||
|
|
||||||
# Skip width/height inheritance for buttons when inheriting from auto-sized containers
|
|
||||||
var skip_sizing = SizingUtils.should_skip_sizing(node, element, parser)
|
var skip_sizing = SizingUtils.should_skip_sizing(node, element, parser)
|
||||||
|
|
||||||
if (width != null or height != null) and not skip_sizing:
|
if (width != null or height != null) and not skip_sizing:
|
||||||
# FlexContainers handle percentage sizing differently than regular controls
|
if width != null:
|
||||||
if node is FlexContainer:
|
if width is String and width == "100%":
|
||||||
if width != null:
|
|
||||||
if SizingUtils.is_percentage(width):
|
|
||||||
# For FlexContainers with percentage width, use proportion sizing
|
|
||||||
var percentage_value = float(width.replace("%", "")) / 100.0
|
|
||||||
node.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
||||||
node.size_flags_stretch_ratio = percentage_value
|
|
||||||
else:
|
|
||||||
node.custom_minimum_size.x = width
|
|
||||||
node.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
|
||||||
node.set_meta("size_flags_horizontal_set", true)
|
|
||||||
|
|
||||||
if height != null:
|
|
||||||
if SizingUtils.is_percentage(height):
|
|
||||||
node.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
|
||||||
else:
|
|
||||||
node.custom_minimum_size.y = height
|
|
||||||
node.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
|
|
||||||
node.set_meta("size_flags_vertical_set", true)
|
|
||||||
|
|
||||||
node.set_meta("size_flags_set_by_style_manager", true)
|
|
||||||
elif node is VBoxContainer or node is HBoxContainer or node is Container:
|
|
||||||
# Hcontainer nodes (like ul, ol)
|
|
||||||
SizingUtils.apply_container_dimension_sizing(node, width, height, styles)
|
|
||||||
elif node is HTMLP:
|
|
||||||
# Only apply sizing if element has explicit size, otherwise preserve natural sizing
|
|
||||||
var element_styles = parser.get_element_styles_internal(element, "")
|
|
||||||
if element_styles.has("width") or element_styles.has("height"):
|
|
||||||
var orig_h_flag = node.size_flags_horizontal
|
|
||||||
var orig_v_flag = node.size_flags_vertical
|
|
||||||
SizingUtils.apply_regular_control_sizing(node, width, height, styles)
|
|
||||||
if not element_styles.has("width"):
|
|
||||||
node.size_flags_horizontal = orig_h_flag
|
|
||||||
if not element_styles.has("height"):
|
|
||||||
node.size_flags_vertical = orig_v_flag
|
|
||||||
else:
|
|
||||||
if element.tag_name == "img" and SizingUtils.is_percentage(width) and SizingUtils.is_percentage(height):
|
|
||||||
node.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
node.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
node.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
node.custom_minimum_size.x = 0
|
||||||
# Clear any hardcoded sizing
|
|
||||||
node.custom_minimum_size = Vector2.ZERO
|
|
||||||
else:
|
else:
|
||||||
# regular controls
|
node.custom_minimum_size.x = width
|
||||||
SizingUtils.apply_regular_control_sizing(node, width, height, styles)
|
node.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||||||
|
|
||||||
|
if height != null:
|
||||||
|
if height is String and height == "100%":
|
||||||
|
node.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
|
node.custom_minimum_size.y = 0
|
||||||
|
else:
|
||||||
|
node.custom_minimum_size.y = height
|
||||||
|
node.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
|
||||||
|
|
||||||
apply_element_centering(node, styles)
|
apply_element_centering(node, styles)
|
||||||
|
|
||||||
@@ -539,7 +508,7 @@ static func apply_body_styles(body: HTMLParser.HTMLElement, parser: HTMLParser,
|
|||||||
original_parent.move_child(margin_container, container_index)
|
original_parent.move_child(margin_container, container_index)
|
||||||
margin_container.add_child(website_container)
|
margin_container.add_child(website_container)
|
||||||
|
|
||||||
var padding_val = parse_size(styles["padding"])
|
var padding_val = parse_size(styles["padding"] if styles.has("padding") else 0)
|
||||||
|
|
||||||
margin_container.add_theme_constant_override("margin_left", padding_val)
|
margin_container.add_theme_constant_override("margin_left", padding_val)
|
||||||
margin_container.add_theme_constant_override("margin_right", padding_val)
|
margin_container.add_theme_constant_override("margin_right", padding_val)
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ static func apply_flex_container_properties(node, styles: Dictionary) -> void:
|
|||||||
|
|
||||||
if styles.has("width"):
|
if styles.has("width"):
|
||||||
var width_val = styles["width"]
|
var width_val = styles["width"]
|
||||||
if width_val == "full":
|
if width_val == "full" or width_val == "100%":
|
||||||
# For flex containers, w-full should expand to fill parent
|
# For flex containers, w-full should expand to fill parent
|
||||||
node.set_meta("should_fill_horizontal", true)
|
node.set_meta("should_fill_horizontal", true)
|
||||||
elif typeof(width_val) == TYPE_STRING and width_val.ends_with("%"):
|
elif typeof(width_val) == TYPE_STRING and width_val.ends_with("%"):
|
||||||
@@ -79,6 +79,7 @@ static func apply_flex_container_properties(node, styles: Dictionary) -> void:
|
|||||||
node.set_meta("custom_css_height", SizingUtils.parse_size_value(height_val))
|
node.set_meta("custom_css_height", SizingUtils.parse_size_value(height_val))
|
||||||
if styles.has("background-color"):
|
if styles.has("background-color"):
|
||||||
node.set_meta("custom_css_background_color", styles["background-color"])
|
node.set_meta("custom_css_background_color", styles["background-color"])
|
||||||
|
|
||||||
node.update_layout()
|
node.update_layout()
|
||||||
|
|
||||||
static func apply_flex_item_properties(node: Control, styles: Dictionary) -> void:
|
static func apply_flex_item_properties(node: Control, styles: Dictionary) -> void:
|
||||||
|
|||||||
@@ -290,6 +290,84 @@ static func render_new_element(element: HTMLParser.HTMLElement, parent_node: Nod
|
|||||||
|
|
||||||
main_scene.safe_add_child(container_node, element_node)
|
main_scene.safe_add_child(container_node, element_node)
|
||||||
|
|
||||||
|
static func _get_input_value(element: HTMLParser.HTMLElement, dom_node: Node) -> Variant:
|
||||||
|
var input_type = element.get_attribute("type").to_lower()
|
||||||
|
|
||||||
|
match input_type:
|
||||||
|
"checkbox", "radio":
|
||||||
|
if dom_node is CheckBox:
|
||||||
|
return dom_node.button_pressed
|
||||||
|
return false
|
||||||
|
"color":
|
||||||
|
if dom_node is ColorPickerButton:
|
||||||
|
return "#" + dom_node.color.to_html(false)
|
||||||
|
return "#ffffff"
|
||||||
|
"range":
|
||||||
|
if dom_node is HSlider:
|
||||||
|
return dom_node.value
|
||||||
|
return 0.0
|
||||||
|
"number":
|
||||||
|
if dom_node is SpinBox:
|
||||||
|
return dom_node.value
|
||||||
|
return 0.0
|
||||||
|
"file":
|
||||||
|
# For file inputs, need to find the input control that has get_file_info method
|
||||||
|
var input_control = _find_input_control_with_file_info(dom_node)
|
||||||
|
if input_control:
|
||||||
|
var file_info = input_control.get_file_info()
|
||||||
|
return file_info.get("fileName", "")
|
||||||
|
return ""
|
||||||
|
"date":
|
||||||
|
if dom_node is DateButton:
|
||||||
|
if dom_node.has_method("get_date_text"):
|
||||||
|
return dom_node.get_date_text()
|
||||||
|
return ""
|
||||||
|
_: # text, password, email, etc.
|
||||||
|
if dom_node is LineEdit:
|
||||||
|
return dom_node.text
|
||||||
|
elif dom_node is TextEdit:
|
||||||
|
return dom_node.text
|
||||||
|
return ""
|
||||||
|
|
||||||
|
static func _find_input_control_with_file_info(node: Node) -> Node:
|
||||||
|
if node and node.has_method("get_file_info"):
|
||||||
|
return node
|
||||||
|
|
||||||
|
var current = node
|
||||||
|
while current:
|
||||||
|
current = current.get_parent()
|
||||||
|
if current and current.has_method("get_file_info"):
|
||||||
|
return current
|
||||||
|
|
||||||
|
return null
|
||||||
|
|
||||||
|
static func _set_input_value(element: HTMLParser.HTMLElement, dom_node: Node, value: Variant) -> void:
|
||||||
|
var input_type = element.get_attribute("type").to_lower()
|
||||||
|
|
||||||
|
match input_type:
|
||||||
|
"checkbox", "radio":
|
||||||
|
if dom_node is CheckBox:
|
||||||
|
dom_node.button_pressed = bool(value)
|
||||||
|
"color":
|
||||||
|
if dom_node is ColorPickerButton:
|
||||||
|
var color_value = str(value)
|
||||||
|
if color_value.begins_with("#"):
|
||||||
|
dom_node.color = Color.from_string(color_value, Color.WHITE)
|
||||||
|
"range":
|
||||||
|
if dom_node is HSlider:
|
||||||
|
dom_node.value = float(value)
|
||||||
|
"number":
|
||||||
|
if dom_node is SpinBox:
|
||||||
|
dom_node.value = float(value)
|
||||||
|
"date":
|
||||||
|
if dom_node is DateButton and dom_node.has_method("set_date_from_string"):
|
||||||
|
dom_node.set_date_from_string(str(value))
|
||||||
|
_: # text, password, email, etc.
|
||||||
|
if dom_node is LineEdit:
|
||||||
|
dom_node.text = str(value)
|
||||||
|
elif dom_node is TextEdit:
|
||||||
|
dom_node.text = str(value)
|
||||||
|
|
||||||
# Helper functions
|
# Helper functions
|
||||||
static func find_element_by_id(element_id: String, dom_parser: HTMLParser) -> HTMLParser.HTMLElement:
|
static func find_element_by_id(element_id: String, dom_parser: HTMLParser) -> HTMLParser.HTMLElement:
|
||||||
if element_id == "body":
|
if element_id == "body":
|
||||||
@@ -852,6 +930,23 @@ static func _element_index_wrapper(vm: LuauVM) -> int:
|
|||||||
return LuaAudioUtils.handle_dom_audio_index(vm, element_id, key)
|
return LuaAudioUtils.handle_dom_audio_index(vm, element_id, key)
|
||||||
|
|
||||||
match key:
|
match key:
|
||||||
|
"value":
|
||||||
|
if lua_api and tag_name == "input":
|
||||||
|
vm.lua_getfield(1, "_element_id")
|
||||||
|
var element_id: String = vm.lua_tostring(-1)
|
||||||
|
vm.lua_pop(1)
|
||||||
|
|
||||||
|
var element = lua_api.dom_parser.find_by_id(element_id) if element_id != "body" else lua_api.dom_parser.find_first("body")
|
||||||
|
var dom_node = lua_api.dom_parser.parse_result.dom_nodes.get(element_id, null)
|
||||||
|
|
||||||
|
if element and dom_node:
|
||||||
|
var input_value = _get_input_value(element, dom_node)
|
||||||
|
vm.lua_pushstring(str(input_value))
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# Fallback to empty string
|
||||||
|
vm.lua_pushstring("")
|
||||||
|
return 1
|
||||||
"text":
|
"text":
|
||||||
if lua_api:
|
if lua_api:
|
||||||
# Get element ID and find the element
|
# Get element ID and find the element
|
||||||
@@ -929,12 +1024,10 @@ static func _element_index_wrapper(vm: LuauVM) -> int:
|
|||||||
return 1
|
return 1
|
||||||
|
|
||||||
static func _add_classlist_support(vm: LuauVM, lua_api: LuaAPI) -> void:
|
static func _add_classlist_support(vm: LuauVM, lua_api: LuaAPI) -> void:
|
||||||
# Create classList table with threaded methods
|
|
||||||
vm.lua_newtable()
|
vm.lua_newtable()
|
||||||
|
|
||||||
# Store the element_id in the classList table
|
vm.lua_getfield(-2, "_element_id")
|
||||||
vm.lua_getfield(-2, "_element_id") # Get element_id from parent element
|
vm.lua_setfield(-2, "_element_id")
|
||||||
vm.lua_setfield(-2, "_element_id") # Store it in classList table
|
|
||||||
|
|
||||||
# Add classList methods
|
# Add classList methods
|
||||||
vm.lua_pushcallable(LuaDOMUtils._classlist_add_wrapper, "classList.add")
|
vm.lua_pushcallable(LuaDOMUtils._classlist_add_wrapper, "classList.add")
|
||||||
@@ -961,12 +1054,11 @@ static func _add_classlist_support(vm: LuauVM, lua_api: LuaAPI) -> void:
|
|||||||
vm.lua_setfield(-2, "classList")
|
vm.lua_setfield(-2, "classList")
|
||||||
|
|
||||||
static func _classlist_add_wrapper(vm: LuauVM) -> int:
|
static func _classlist_add_wrapper(vm: LuauVM) -> int:
|
||||||
# Get lua_api from VM metadata
|
|
||||||
var lua_api = vm.get_meta("lua_api") as LuaAPI
|
var lua_api = vm.get_meta("lua_api") as LuaAPI
|
||||||
if not lua_api:
|
if not lua_api:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
vm.luaL_checktype(1, vm.LUA_TTABLE) # classList table
|
vm.luaL_checktype(1, vm.LUA_TTABLE)
|
||||||
var cls: String = vm.luaL_checkstring(2)
|
var cls: String = vm.luaL_checkstring(2)
|
||||||
|
|
||||||
# Get element_id from classList table
|
# Get element_id from classList table
|
||||||
@@ -1132,6 +1224,21 @@ static func _element_newindex_wrapper(vm: LuauVM) -> int:
|
|||||||
return LuaAudioUtils.handle_dom_audio_newindex(vm, element_id, key, value)
|
return LuaAudioUtils.handle_dom_audio_newindex(vm, element_id, key, value)
|
||||||
|
|
||||||
match key:
|
match key:
|
||||||
|
"value":
|
||||||
|
if tag_name == "input":
|
||||||
|
vm.lua_getfield(1, "_element_id")
|
||||||
|
var element_id: String = vm.lua_tostring(-1)
|
||||||
|
vm.lua_pop(1)
|
||||||
|
|
||||||
|
var element = lua_api.dom_parser.find_by_id(element_id) if element_id != "body" else lua_api.dom_parser.find_first("body")
|
||||||
|
var dom_node = lua_api.dom_parser.parse_result.dom_nodes.get(element_id, null)
|
||||||
|
|
||||||
|
if element and dom_node:
|
||||||
|
# Update the HTML element's value attribute
|
||||||
|
element.set_attribute("value", str(value))
|
||||||
|
|
||||||
|
_set_input_value(element, dom_node, value)
|
||||||
|
return 0
|
||||||
"text":
|
"text":
|
||||||
var text: String = str(value) # Convert value to string
|
var text: String = str(value) # Convert value to string
|
||||||
|
|
||||||
|
|||||||
@@ -53,18 +53,6 @@ func _ready():
|
|||||||
ProjectSettings.set_setting("display/window/size/min_width", MIN_SIZE.x)
|
ProjectSettings.set_setting("display/window/size/min_width", MIN_SIZE.x)
|
||||||
ProjectSettings.set_setting("display/window/size/min_height", MIN_SIZE.y)
|
ProjectSettings.set_setting("display/window/size/min_height", MIN_SIZE.y)
|
||||||
DisplayServer.window_set_min_size(MIN_SIZE)
|
DisplayServer.window_set_min_size(MIN_SIZE)
|
||||||
|
|
||||||
get_viewport().size_changed.connect(_on_viewport_size_changed)
|
|
||||||
|
|
||||||
func _on_viewport_size_changed():
|
|
||||||
recalculate_percentage_elements(website_container)
|
|
||||||
|
|
||||||
func recalculate_percentage_elements(node: Node):
|
|
||||||
if node is Control and node.has_meta("needs_percentage_recalc"):
|
|
||||||
SizingUtils.apply_container_percentage_sizing(node)
|
|
||||||
|
|
||||||
for child in node.get_children():
|
|
||||||
recalculate_percentage_elements(child)
|
|
||||||
|
|
||||||
var current_domain = "" # Store current domain for display
|
var current_domain = "" # Store current domain for display
|
||||||
|
|
||||||
@@ -267,9 +255,6 @@ static func safe_add_child(parent: Node, child: Node) -> void:
|
|||||||
child.get_parent().remove_child(child)
|
child.get_parent().remove_child(child)
|
||||||
parent.add_child(child)
|
parent.add_child(child)
|
||||||
|
|
||||||
if child.has_meta("container_percentage_width") or child.has_meta("container_percentage_height"):
|
|
||||||
SizingUtils.apply_container_percentage_sizing(child)
|
|
||||||
child.set_meta("needs_percentage_recalc", true)
|
|
||||||
|
|
||||||
func contains_hyperlink(element: HTMLParser.HTMLElement) -> bool:
|
func contains_hyperlink(element: HTMLParser.HTMLElement) -> bool:
|
||||||
if element.tag_name == "a":
|
if element.tag_name == "a":
|
||||||
@@ -290,6 +275,7 @@ func is_text_only_element(element: HTMLParser.HTMLElement) -> bool:
|
|||||||
|
|
||||||
func create_element_node(element: HTMLParser.HTMLElement, parser: HTMLParser) -> Control:
|
func create_element_node(element: HTMLParser.HTMLElement, parser: HTMLParser) -> Control:
|
||||||
var styles = parser.get_element_styles_with_inheritance(element, "", [])
|
var styles = parser.get_element_styles_with_inheritance(element, "", [])
|
||||||
|
var hover_styles = parser.get_element_styles_with_inheritance(element, "hover", [])
|
||||||
var is_flex_container = styles.has("display") and ("flex" in styles["display"])
|
var is_flex_container = styles.has("display") and ("flex" in styles["display"])
|
||||||
|
|
||||||
var final_node: Control
|
var final_node: Control
|
||||||
@@ -308,10 +294,22 @@ func create_element_node(element: HTMLParser.HTMLElement, parser: HTMLParser) ->
|
|||||||
|
|
||||||
if is_flex_container:
|
if is_flex_container:
|
||||||
# The element's primary identity IS a flex container.
|
# The element's primary identity IS a flex container.
|
||||||
# We create it directly.
|
if element.tag_name == "div":
|
||||||
final_node = AUTO_SIZING_FLEX_CONTAINER.new()
|
if BackgroundUtils.needs_background_wrapper(styles) or hover_styles.size() > 0:
|
||||||
final_node.name = "Flex_" + element.tag_name
|
final_node = BackgroundUtils.create_panel_container_with_background(styles, hover_styles)
|
||||||
container_for_children = final_node
|
var flex_container = AUTO_SIZING_FLEX_CONTAINER.new()
|
||||||
|
flex_container.name = "Flex_" + element.tag_name
|
||||||
|
var vbox = final_node.get_child(0) as VBoxContainer
|
||||||
|
vbox.add_child(flex_container)
|
||||||
|
container_for_children = flex_container
|
||||||
|
else:
|
||||||
|
final_node = AUTO_SIZING_FLEX_CONTAINER.new()
|
||||||
|
final_node.name = "Flex_" + element.tag_name
|
||||||
|
container_for_children = final_node
|
||||||
|
else:
|
||||||
|
final_node = AUTO_SIZING_FLEX_CONTAINER.new()
|
||||||
|
final_node.name = "Flex_" + element.tag_name
|
||||||
|
container_for_children = final_node
|
||||||
|
|
||||||
# For FLEX ul/ol elements, we need to create the li children directly in the flex container
|
# For FLEX ul/ol elements, we need to create the li children directly in the flex container
|
||||||
if element.tag_name == "ul" or element.tag_name == "ol":
|
if element.tag_name == "ul" or element.tag_name == "ol":
|
||||||
@@ -353,11 +351,20 @@ func create_element_node(element: HTMLParser.HTMLElement, parser: HTMLParser) ->
|
|||||||
# Apply flex CONTAINER properties if it's a flex container
|
# Apply flex CONTAINER properties if it's a flex container
|
||||||
if is_flex_container:
|
if is_flex_container:
|
||||||
var flex_container_node = final_node
|
var flex_container_node = final_node
|
||||||
# If the node was wrapped in a MarginContainer, get the inner FlexContainer
|
|
||||||
if final_node is MarginContainer and final_node.get_child_count() > 0:
|
if final_node is FlexContainer:
|
||||||
|
# Direct FlexContainer
|
||||||
|
flex_container_node = final_node
|
||||||
|
elif final_node is MarginContainer and final_node.get_child_count() > 0:
|
||||||
var first_child = final_node.get_child(0)
|
var first_child = final_node.get_child(0)
|
||||||
if first_child is FlexContainer:
|
if first_child is FlexContainer:
|
||||||
flex_container_node = first_child
|
flex_container_node = first_child
|
||||||
|
elif final_node is PanelContainer and final_node.get_child_count() > 0:
|
||||||
|
var vbox = final_node.get_child(0)
|
||||||
|
if vbox is VBoxContainer and vbox.get_child_count() > 0:
|
||||||
|
var potential_flex = vbox.get_child(0)
|
||||||
|
if potential_flex is FlexContainer:
|
||||||
|
flex_container_node = potential_flex
|
||||||
|
|
||||||
if flex_container_node is FlexContainer:
|
if flex_container_node is FlexContainer:
|
||||||
FlexUtils.apply_flex_container_properties(flex_container_node, styles)
|
FlexUtils.apply_flex_container_properties(flex_container_node, styles)
|
||||||
@@ -476,7 +483,7 @@ func create_element_node_internal(element: HTMLParser.HTMLElement, parser: HTMLP
|
|||||||
var hover_styles = parser.get_element_styles_with_inheritance(element, "hover", [])
|
var hover_styles = parser.get_element_styles_with_inheritance(element, "hover", [])
|
||||||
var is_flex_container = styles.has("display") and ("flex" in styles["display"])
|
var is_flex_container = styles.has("display") and ("flex" in styles["display"])
|
||||||
|
|
||||||
# For flex divs, don't create div scene - the AutoSizingFlexContainer handles it
|
# For flex divs, let the general flex container logic handle them
|
||||||
if is_flex_container:
|
if is_flex_container:
|
||||||
return null
|
return null
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user