Skip to content
GitLab
Explore
Sign in
Register
Hide whitespace changes
Inline
Side-by-side
packages/admin/src/lib/utils.ts
0 → 100644
View file @
51eacdaf
import
{
toast
}
from
"
react-toastify
"
;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export
const
api
=
async
<
T
=
any
>
(
endpoint
:
string
,
method
:
"
GET
"
|
"
POST
"
=
"
GET
"
,
body
?:
unknown
):
Promise
<
{
status
:
number
;
data
:
({
success
:
true
}
&
T
)
|
{
success
:
false
;
error
:
string
};
}
>
=>
{
const
API_HOST
=
import
.
meta
.
env
.
VITE_API_ROOT
||
""
;
const
req
=
await
fetch
(
API_HOST
+
endpoint
,
{
method
,
credentials
:
"
include
"
,
headers
:
{
...(
body
?
{
"
Content-Type
"
:
"
application/json
"
}
:
{}),
},
body
:
JSON
.
stringify
(
body
),
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let
data
:
any
;
try
{
data
=
await
req
.
json
();
}
catch
(
e
)
{
/* empty */
}
return
{
status
:
req
.
status
,
data
,
};
};
export
const
handleError
=
(
...
props
:
[
status
:
number
,
data
:
{
success
:
true
}
|
{
success
:
false
;
error
:
string
},
]
)
=>
{
console
.
error
(...
props
);
if
(
typeof
props
[
0
]
===
"
number
"
)
{
const
[
status
,
data
]
=
props
;
toast
.
error
(
`
${
status
}
${
"
error
"
in
data
?
data
.
error
:
JSON
.
stringify
(
data
)}
`
);
}
};
packages/admin/src/main.tsx
View file @
51eacdaf
...
...
@@ -7,6 +7,9 @@ import { RouterProvider, createBrowserRouter } from "react-router-dom";
import
{
Root
}
from
"
./Root.tsx
"
;
import
{
HomePage
}
from
"
./pages/Home/page.tsx
"
;
import
{
AccountsPage
}
from
"
./pages/Accounts/Accounts/page.tsx
"
;
import
{
ServiceSettingsPage
}
from
"
./pages/Service/settings.tsx
"
;
import
{
AuditLog
}
from
"
./pages/AuditLog/auditlog.tsx
"
;
import
{
ToastWrapper
}
from
"
./components/ToastWrapper.tsx
"
;
const
router
=
createBrowserRouter
(
[
...
...
@@ -22,11 +25,19 @@ const router = createBrowserRouter(
path
:
"
/accounts
"
,
element
:
<
AccountsPage
/>,
},
{
path
:
"
/service/settings
"
,
element
:
<
ServiceSettingsPage
/>,
},
{
path
:
"
/audit
"
,
element
:
<
AuditLog
/>,
},
],
},
],
{
basename
:
import
.
meta
.
env
.
VITE
_APP_ROOT
,
basename
:
_
_APP_ROOT
__
,
}
);
...
...
@@ -35,6 +46,8 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
<
NextUIProvider
>
<
ThemeProvider
defaultTheme
=
"system"
>
<
RouterProvider
router
=
{
router
}
/>
<
ToastWrapper
/>
</
ThemeProvider
>
</
NextUIProvider
>
</
React
.
StrictMode
>
...
...
packages/admin/src/pages/AuditLog/auditlog.tsx
0 → 100644
View file @
51eacdaf
import
{
useEffect
,
useState
}
from
"
react
"
;
import
{
api
,
handleError
}
from
"
../../lib/utils
"
;
import
{
Table
,
TableBody
,
TableCell
,
TableColumn
,
TableHeader
,
TableRow
,
}
from
"
@nextui-org/react
"
;
type
AuditLogAction
=
"
BAN_CREATE
"
|
"
BAN_UPDATE
"
|
"
BAN_DELETE
"
;
type
AuditLog
=
{
id
:
number
;
userId
:
string
;
action
:
AuditLogAction
;
reason
?:
string
;
comment
?:
string
;
banId
?:
number
;
createdAt
:
string
;
updatedAt
?:
string
;
};
export
const
AuditLog
=
()
=>
{
const
[
auditLogs
,
setAuditLogs
]
=
useState
<
AuditLog
[]
>
([]);
useEffect
(()
=>
{
api
<
{
auditLogs
:
AuditLog
[]
}
>
(
"
/api/admin/audit
"
,
"
GET
"
).
then
(
({
status
,
data
})
=>
{
if
(
status
===
200
)
{
if
(
data
.
success
)
{
setAuditLogs
(
data
.
auditLogs
);
}
else
{
handleError
(
status
,
data
);
}
}
else
{
handleError
(
status
,
data
);
}
}
);
},
[]);
return
(
<>
<
h4
className
=
"text-l font-semibold"
>
Audit Log
</
h4
>
<
div
className
=
"relative"
>
<
Table
>
<
TableHeader
>
<
TableColumn
>
ID
</
TableColumn
>
<
TableColumn
>
User ID
</
TableColumn
>
<
TableColumn
>
Action
</
TableColumn
>
<
TableColumn
>
Reason
</
TableColumn
>
<
TableColumn
>
Comment
</
TableColumn
>
<
TableColumn
>
Created At / Updated At
</
TableColumn
>
</
TableHeader
>
<
TableBody
>
{
auditLogs
.
map
((
log
)
=>
(
<
TableRow
key
=
{
log
.
id
}
>
<
TableCell
>
{
log
.
id
}
</
TableCell
>
<
TableCell
>
{
log
.
userId
}
</
TableCell
>
<
TableCell
>
{
log
.
action
}
</
TableCell
>
<
TableCell
>
{
log
.
reason
}
</
TableCell
>
<
TableCell
>
{
log
.
comment
}
</
TableCell
>
<
TableCell
>
{
log
.
createdAt
}
/
{
log
.
updatedAt
}
</
TableCell
>
</
TableRow
>
))
}
</
TableBody
>
</
Table
>
</
div
>
</>
);
};
packages/admin/src/pages/Service/settings.tsx
0 → 100644
View file @
51eacdaf
import
{
BreadcrumbItem
,
Breadcrumbs
,
Button
,
Input
}
from
"
@nextui-org/react
"
;
import
{
useEffect
,
useState
}
from
"
react
"
;
import
{
api
,
handleError
}
from
"
../../lib/utils
"
;
import
{
LoadingOverlay
}
from
"
../../components/LoadingOverlay
"
;
import
{
toast
}
from
"
react-toastify
"
;
export
const
ServiceSettingsPage
=
()
=>
{
return
(
<
div
className
=
"my-14 lg:px-6 max-w-[95rem] mx-auto w-full flex flex-col gap-4"
>
<
Breadcrumbs
>
<
BreadcrumbItem
href
=
"/"
>
Home
</
BreadcrumbItem
>
<
BreadcrumbItem
>
Service
</
BreadcrumbItem
>
<
BreadcrumbItem
>
Settings
</
BreadcrumbItem
>
</
Breadcrumbs
>
<
h3
className
=
"text-xl font-semibold"
>
Service Settings
</
h3
>
<
CanvasSettings
/>
</
div
>
);
};
const
CanvasSettings
=
()
=>
{
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
width
,
setWidth
]
=
useState
(
""
);
const
[
height
,
setHeight
]
=
useState
(
""
);
useEffect
(()
=>
{
api
<
{
size
:
{
width
:
number
;
height
:
number
}
}
>
(
"
/api/admin/canvas/size
"
)
.
then
(({
status
,
data
})
=>
{
if
(
status
===
200
)
{
if
(
data
.
success
)
{
setWidth
(
data
.
size
.
width
+
""
);
setHeight
(
data
.
size
.
height
+
""
);
}
else
{
handleError
(
status
,
data
);
}
}
else
{
handleError
(
status
,
data
);
}
})
.
finally
(()
=>
{
setLoading
(
false
);
});
},
[]);
const
doSaveSize
=
()
=>
{
setLoading
(
true
);
api
(
"
/api/admin/canvas/size
"
,
"
POST
"
,
{
width
,
height
,
})
.
then
(({
status
,
data
})
=>
{
if
(
status
===
200
)
{
if
(
data
.
success
)
{
toast
.
success
(
"
Canvas size has been changed
"
);
}
else
{
handleError
(
status
,
data
);
}
}
else
{
handleError
(
status
,
data
);
}
})
.
finally
(()
=>
{
setLoading
(
false
);
});
};
return
(
<>
<
h4
className
=
"text-l font-semibold"
>
Canvas
</
h4
>
<
div
className
=
"relative"
>
{
loading
&&
<
LoadingOverlay
/>
}
<
b
>
Canvas size is resource intensive, this will take a minute to complete
</
b
>
<
Input
type
=
"number"
size
=
"sm"
min
=
"100"
max
=
"10000"
label
=
"Width"
value
=
{
width
}
onValueChange
=
{
setWidth
}
/>
<
Input
type
=
"number"
size
=
"sm"
min
=
"100"
max
=
"10000"
label
=
"Height"
value
=
{
height
}
onValueChange
=
{
setHeight
}
/>
<
Button
onPress
=
{
doSaveSize
}
isLoading
=
{
loading
}
>
Save
</
Button
>
</
div
>
</>
);
};
packages/admin/src/vite-env.d.ts
View file @
51eacdaf
/// <reference types="vite/client" />
declare
const
__APP_ROOT__
:
string
;
packages/admin/vite.config.ts
View file @
51eacdaf
...
...
@@ -15,4 +15,7 @@ export default defineConfig({
include
:
"
**/*.{jsx,tsx}
"
,
}),
],
define
:
{
__APP_ROOT__
:
JSON
.
stringify
(
process
.
env
.
APP_ROOT
),
},
});
packages/client/.eslintrc.json
0 → 100644
View file @
51eacdaf
{
"extends"
:
[
"react-app"
],
"rules"
:
{
"no-unused-vars"
:
"off"
,
"@typescript-eslint/no-namespace"
:
"off"
,
"@typescript-eslint/no-explicit-any"
:
"off"
,
"@typescript-eslint/no-unused-vars"
:
[
"error"
,
{
"argsIgnorePattern"
:
"^_"
,
"caughtErrors"
:
"all"
,
"caughtErrorsIgnorePattern"
:
"^_"
,
"destructuredArrayIgnorePattern"
:
"^_"
,
"varsIgnorePattern"
:
"^_"
,
"ignoreRestSiblings"
:
true
}
]
},
"overrides"
:
[
{
"files"
:
[
"**/*.ts?(x)"
],
"rules"
:
{}
}
]
}
packages/client/dev.env.example
0 → 100644
View file @
51eacdaf
# where the backend is located
VITE_API_HOST=http://localhost:3000
# what homeserver hosts the matrix users
VITE_MATRIX_HOST=aftermath.gg
# what hostname does the element instance run on
VITE_ELEMENT_HOST=https://chat.fediverse.events
\ No newline at end of file
packages/client/package.json
View file @
51eacdaf
...
...
@@ -7,47 +7,31 @@
"build"
:
"vite build"
,
"dev"
:
"vite serve"
,
"preview"
:
"vite preview"
,
"lint"
:
"eslint ."
"lint"
:
"eslint
--format gitlab
."
},
"type"
:
"module"
,
"keywords"
:
[],
"author"
:
""
,
"license"
:
"ISC"
,
"eslintConfig"
:
{
"extends"
:
"react-app"
},
"dependencies"
:
{
"@fortawesome/fontawesome-svg-core"
:
"^6.5.1"
,
"@fortawesome/free-solid-svg-icons"
:
"^6.5.1"
,
"@fortawesome/react-fontawesome"
:
"^0.2.0"
,
"@nextui-org/react"
:
"^2.2.9"
,
"@fortawesome/free-solid-svg-icons"
:
"^6.7.2"
,
"@icons-pack/react-simple-icons"
:
"^10.2.0"
,
"@nextui-org/react"
:
"^2.6.11"
,
"@sc07-canvas/lib"
:
"^1.0.0"
,
"@t
ypescript-eslint/parser
"
:
"^
7
.1.0"
,
"@t
heme-toggles/react
"
:
"^
4
.1.0"
,
"eventemitter3"
:
"^5.0.1"
,
"framer-motion"
:
"^11.
0.5
"
,
"framer-motion"
:
"^11.
3.2
"
,
"lodash.throttle"
:
"^4.1.1"
,
"prop-types"
:
"^15.8.1"
,
"react"
:
"^18.2.0"
,
"react-dom"
:
"^18.2.0"
,
"react-zoom-pan-pinch"
:
"^3.4.1"
,
"socket.io-client"
:
"^4.
7.4
"
"socket.io-client"
:
"^4.
8.1
"
},
"devDependencies"
:
{
"@t
sconfig/vite-react
"
:
"^3.0.
0
"
,
"@t
ypes/grecaptcha
"
:
"^3.0.
9
"
,
"@types/lodash.throttle"
:
"^4.1.9"
,
"@types/react"
:
"^18.2.48"
,
"@types/react-dom"
:
"^18.2.18"
,
"@types/socket.io-client"
:
"^3.0.0"
,
"@vitejs/plugin-react"
:
"^4.2.1"
,
"autoprefixer"
:
"^10.4.17"
,
"eslint"
:
"^8.56.0"
,
"eslint-config-react-app"
:
"^7.0.1"
,
"eslint-plugin-react"
:
"^7.33.2"
,
"eslint-plugin-react-hooks"
:
"^4.6.0"
,
"postcss"
:
"^8.4.35"
,
"sass"
:
"^1.70.0"
,
"tailwindcss"
:
"^3.4.1"
,
"vite"
:
"^5.1.1"
,
"vite-plugin-simple-html"
:
"^0.1.2"
"sass"
:
"^1.83.0"
}
}
packages/client/src/board.scss
View file @
51eacdaf
...
...
@@ -23,3 +23,13 @@
pointer-events
:
none
;
touch-action
:
none
;
}
.board-overlay
{
position
:
absolute
;
top
:
0
;
left
:
0
;
}
.no-interact
{
pointer-events
:
none
;
}
packages/client/src/components/App.tsx
View file @
51eacdaf
import
{
Header
}
from
"
./Header
"
;
import
{
AppContext
}
from
"
../contexts/AppContext
"
;
import
{
Header
}
from
"
./Header
/Header
"
;
import
{
AppContext
,
useAppContext
}
from
"
../contexts/AppContext
"
;
import
{
CanvasWrapper
}
from
"
./CanvasWrapper
"
;
import
{
Pallete
}
from
"
./Pallete
"
;
import
{
TemplateContext
}
from
"
../contexts/TemplateContext
"
;
import
{
SettingsSidebar
}
from
"
./Settings/SettingsSidebar
"
;
import
{
DebugModal
}
from
"
./Debug/DebugModal
"
;
import
{
ToolbarWrapper
}
from
"
./Toolbar/ToolbarWrapper
"
;
import
{
useEffect
}
from
"
react
"
;
import
{
ChatContext
}
from
"
../contexts/ChatContext
"
;
import
"
react-toastify/dist/ReactToastify.css
"
;
import
{
AuthErrors
}
from
"
./AuthErrors
"
;
import
"
../lib/keybinds
"
;
import
{
PixelWhoisSidebar
}
from
"
./PixelWhoisSidebar
"
;
import
{
KeybindModal
}
from
"
./KeybindModal
"
;
import
{
ProfileModal
}
from
"
./Profile/ProfileModal
"
;
import
{
WelcomeModal
}
from
"
./Welcome/WelcomeModal
"
;
import
{
InfoSidebar
}
from
"
./Info/InfoSidebar
"
;
import
{
ModModal
}
from
"
./Moderation/ModModal
"
;
import
{
DynamicModals
}
from
"
./DynamicModals
"
;
import
{
ToastWrapper
}
from
"
./ToastWrapper
"
;
// const Chat = lazy(() => import("./Chat/Chat"));
console
.
log
(
"
Client init with version
"
+
__COMMIT_HASH__
);
// const DynamicallyLoadChat = () => {
// const { loadChat } = useAppContext();
// return <React.Suspense>{loadChat && <Chat />}</React.Suspense>;
// };
// get access to context data
const
AppInner
=
()
=>
{
const
{
config
}
=
useAppContext
();
useEffect
(()
=>
{
// detect auth callback for chat, regardless of it being loaded
// callback token expires quickly, so we should exchange it as quick as possible
(
async
()
=>
{
const
params
=
new
URLSearchParams
(
window
.
location
.
search
);
if
(
params
.
has
(
"
loginToken
"
))
{
if
(
!
config
)
{
console
.
warn
(
"
[App] loginToken parsing is delayed because config is not available
"
);
return
;
}
// login button opens a new tab that redirects here
// if we're that tab, we should try to close this tab when we're done
// should work because this tab is opened by JS
const
shouldCloseWindow
=
window
.
location
.
pathname
.
startsWith
(
"
/chat_callback
"
);
// token provided by matrix's /sso/redirect
const
token
=
params
.
get
(
"
loginToken
"
)
!
;
// immediately remove from url to prevent reloading
window
.
history
.
replaceState
({},
""
,
"
/
"
);
const
loginReq
=
await
fetch
(
`https://
${
config
.
chat
.
matrix_homeserver
}
/_matrix/client/v3/login`
,
{
method
:
"
POST
"
,
headers
:
{
"
Content-Type
"
:
"
application/json
"
,
},
body
:
JSON
.
stringify
({
type
:
"
m.login.token
"
,
token
,
}),
}
);
const
loginRes
=
await
loginReq
.
json
();
console
.
log
(
"
[Chat] Matrix login
"
,
loginReq
.
status
);
switch
(
loginReq
.
status
)
{
case
200
:
{
// success
console
.
log
(
"
[Chat] Logged in successfully
"
,
loginRes
);
localStorage
.
setItem
(
"
matrix.access_token
"
,
loginRes
.
access_token
+
""
);
localStorage
.
setItem
(
"
matrix.device_id
"
,
loginRes
.
device_id
+
""
);
localStorage
.
setItem
(
"
matrix.user_id
"
,
loginRes
.
user_id
+
""
);
if
(
shouldCloseWindow
)
{
console
.
log
(
"
[Chat] Path matches autoclose, attempting to close window...
"
);
window
.
close
();
alert
(
"
You can close this window and return to the other tab :)
"
);
}
else
{
console
.
log
(
"
[Chat] Path doesn't match autoclose, not doing anything
"
);
}
break
;
}
case
400
:
case
403
:
console
.
log
(
"
[Chat] Matrix login
"
,
loginRes
);
alert
(
"
[Chat] Failed to login
\n
"
+
loginRes
.
errcode
+
"
"
+
loginRes
.
error
);
break
;
case
429
:
alert
(
"
[Chat] Failed to login, ratelimited.
\n
Try again in
"
+
Math
.
floor
(
loginRes
.
retry_after_ms
/
1000
)
+
"
s
\n
"
+
loginRes
.
errcode
+
"
"
+
loginRes
.
error
);
break
;
default
:
alert
(
"
Error
"
+
loginReq
.
status
+
"
returned when trying to login to chat
"
);
}
}
})();
},
[
config
]);
return
(
<>
<
Header
/>
{
config
&&
<
CanvasWrapper
/>
}
<
ToolbarWrapper
/>
{
/* <DynamicallyLoadChat /> */
}
<
DebugModal
/>
<
SettingsSidebar
/>
<
InfoSidebar
/>
<
PixelWhoisSidebar
/>
<
KeybindModal
/>
<
AuthErrors
/>
<
ProfileModal
/>
<
WelcomeModal
/>
<
ModModal
/>
<
ToastWrapper
/>
<
DynamicModals
/>
</>
);
};
const
App
=
()
=>
{
return
(
<
AppContext
>
<
Header
/>
<
CanvasWrapper
/>
<
Pallete
/>
<
ChatContext
>
<
TemplateContext
>
<
AppInner
/>
</
TemplateContext
>
</
ChatContext
>
</
AppContext
>
);
};
...
...
packages/client/src/components/AuthErrors.tsx
0 → 100644
View file @
51eacdaf
import
{
Button
,
Link
,
Modal
,
ModalBody
,
ModalContent
,
ModalFooter
,
ModalHeader
,
}
from
"
@nextui-org/react
"
;
import
{
useEffect
,
useState
}
from
"
react
"
;
const
Params
=
{
TYPE
:
"
auth_type
"
,
ERROR
:
"
auth_error
"
,
ERROR_DESC
:
"
auth_error_desc
"
,
CAN_RETRY
:
"
auth_retry
"
,
};
/**
* Show popups that detail auth error messages
* @returns
*/
export
const
AuthErrors
=
()
=>
{
const
[
params
,
setParams
]
=
useState
(
new
URLSearchParams
(
window
.
location
.
search
)
);
const
onClose
=
()
=>
{
const
url
=
new
URL
(
window
.
location
.
href
);
url
.
search
=
""
;
window
.
history
.
replaceState
({},
""
,
url
.
toString
());
setParams
(
new
URLSearchParams
(
window
.
location
.
search
));
};
return
(
<>
<
OPError
isOpen
=
{
params
.
get
(
Params
.
TYPE
)
===
"
op
"
}
onClose
=
{
onClose
}
params
=
{
params
}
/>
<
BannedError
isOpen
=
{
params
.
get
(
Params
.
TYPE
)
===
"
banned
"
}
onClose
=
{
onClose
}
params
=
{
params
}
/>
</>
);
};
const
BannedError
=
({
isOpen
,
onClose
,
params
,
}:
{
isOpen
:
boolean
;
onClose
:
()
=>
void
;
params
:
URLSearchParams
;
})
=>
{
return
(
<
Modal
isOpen
=
{
isOpen
}
onClose
=
{
onClose
}
isDismissable
=
{
false
}
>
<
ModalContent
>
{
(
_onClose
)
=>
(
<>
<
ModalHeader
>
Login Error
</
ModalHeader
>
<
ModalBody
>
<
b
>
Your instance is banned.
</
b
>
You cannot proceed.
<
br
/>
<
br
/>
{
params
.
has
(
Params
.
ERROR_DESC
)
?
(
<>
Reason:
{
params
.
get
(
Params
.
ERROR_DESC
)
}
</>
)
:
(
<>
No reason provided
</>
)
}
</
ModalBody
>
</>
)
}
</
ModalContent
>
</
Modal
>
);
};
/**
* This is for OP errors, these might not be retryable
* @param param0
* @returns
*/
const
OPError
=
({
isOpen
,
onClose
,
params
,
}:
{
isOpen
:
boolean
;
onClose
:
()
=>
void
;
params
:
URLSearchParams
;
})
=>
{
const
canRetry
=
params
.
has
(
Params
.
CAN_RETRY
);
const
[
error
,
_setError
]
=
useState
(
params
.
get
(
Params
.
ERROR
));
const
[
errorDesc
,
setErrorDesc
]
=
useState
(
params
.
get
(
Params
.
ERROR_DESC
));
useEffect
(()
=>
{
switch
(
params
.
get
(
Params
.
ERROR
))
{
case
"
invalid_grant
"
:
setErrorDesc
(
"
Invalid token, try logging in again
"
);
break
;
}
},
[
params
]);
return
(
<
Modal
isOpen
=
{
isOpen
}
onClose
=
{
onClose
}
isDismissable
=
{
false
}
>
<
ModalContent
>
{
(
_onClose
)
=>
(
<>
<
ModalHeader
>
Login Error
</
ModalHeader
>
<
ModalBody
>
<
b
>
Error:
</
b
>
{
error
}
<
br
/>
<
br
/>
<
b
>
Error Description:
</
b
>
{
errorDesc
}
</
ModalBody
>
<
ModalFooter
>
{
canRetry
&&
(
<
Button
color
=
"primary"
href
=
"/api/login"
as
=
{
Link
}
>
Login
</
Button
>
)
}
</
ModalFooter
>
</>
)
}
</
ModalContent
>
</
Modal
>
);
};
packages/client/src/components/CanvasWrapper.tsx
View file @
51eacdaf
import
{
createRef
,
useContext
,
useEffect
}
from
"
react
"
;
import
{
useCallback
,
useContext
,
useEffect
,
useRef
,
useState
}
from
"
react
"
;
import
{
Canvas
}
from
"
../lib/canvas
"
;
import
{
useAppContext
}
from
"
../contexts/AppContext
"
;
import
{
PanZoomWrapper
}
from
"
@sc07-canvas/lib/src/renderer
"
;
import
{
RendererContext
}
from
"
@sc07-canvas/lib/src/renderer/RendererContext
"
;
import
{
ViewportMoveEvent
}
from
"
@sc07-canvas/lib/src/renderer/PanZoom
"
;
import
throttle
from
"
lodash.throttle
"
;
import
{
Routes
}
from
"
../lib/routes
"
;
import
{
ICanvasPosition
,
IPosition
}
from
"
@sc07-canvas/lib/src/net
"
;
import
{
IPosition
}
from
"
@sc07-canvas/lib/src/net
"
;
import
{
Template
}
from
"
./Templating/Template
"
;
import
{
Template
as
TemplateCl
}
from
"
../lib/template
"
;
import
{
IRouterData
,
Router
}
from
"
../lib/router
"
;
import
{
KeybindManager
}
from
"
../lib/keybinds
"
;
import
{
BlankOverlay
}
from
"
./Overlay/BlankOverlay
"
;
import
{
HeatmapOverlay
}
from
"
./Overlay/HeatmapOverlay
"
;
import
{
useTemplateContext
}
from
"
../contexts/TemplateContext
"
;
import
{
PixelPulses
}
from
"
./Overlay/PixelPulses
"
;
import
{
CanvasUtils
}
from
"
../lib/canvas.utils
"
;
export
const
CanvasWrapper
=
()
=>
{
// to prevent safari from blurring things, use the zoom css property
const
{
config
}
=
useAppContext
();
const
getInitialPosition
=
useCallback
<
(
useCssZoom
:
boolean
)
=>
|
{
x
:
number
;
y
:
number
;
zoom
?:
number
;
}
|
undefined
>
(
(
useCssZoom
)
=>
{
const
router
=
Router
.
get
().
canvas
;
if
(
!
router
)
return
undefined
;
if
(
!
config
)
{
console
.
warn
(
"
getInitialPosition called with no config
"
);
return
undefined
;
}
const
{
transformX
,
transformY
}
=
CanvasUtils
.
canvasToPanZoomTransform
(
router
.
x
,
router
.
y
,
config
.
canvas
.
size
,
useCssZoom
);
return
{
x
:
transformX
,
y
:
transformY
,
zoom
:
router
.
zoom
,
};
},
[
config
]
);
return
(
<
main
>
<
PanZoomWrapper
>
<
PanZoomWrapper
initialPosition
=
{
getInitialPosition
}
>
<
BlankOverlay
/>
<
HeatmapOverlay
/>
<
PixelPulses
/>
{
config
&&
<
Template
/>
}
<
CanvasInner
/>
<
Cursor
/>
</
PanZoomWrapper
>
</
main
>
);
};
const
parseHashParams
=
(
canvas
:
Canvas
)
=>
{
// maybe move this to a utility inside routes.ts
let
{
hash
}
=
new
URL
(
window
.
location
.
href
);
if
(
hash
.
indexOf
(
"
#
"
)
===
0
)
{
hash
=
hash
.
slice
(
1
);
}
let
params
=
new
URLSearchParams
(
hash
);
let
position
:
{
x
?:
number
;
y
?:
number
;
zoom
?:
number
;
}
=
{};
if
(
params
.
has
(
"
x
"
)
&&
!
isNaN
(
parseInt
(
params
.
get
(
"
x
"
)
!
)))
position
.
x
=
parseInt
(
params
.
get
(
"
x
"
)
!
);
if
(
params
.
has
(
"
y
"
)
&&
!
isNaN
(
parseInt
(
params
.
get
(
"
y
"
)
!
)))
position
.
y
=
parseInt
(
params
.
get
(
"
y
"
)
!
);
if
(
params
.
has
(
"
zoom
"
)
&&
!
isNaN
(
parseInt
(
params
.
get
(
"
zoom
"
)
!
)))
position
.
zoom
=
parseInt
(
params
.
get
(
"
zoom
"
)
!
);
if
(
typeof
position
.
x
===
"
number
"
&&
typeof
position
.
y
===
"
number
"
&&
typeof
position
.
zoom
===
"
number
"
)
{
const
{
transformX
,
transformY
}
=
canvas
.
canvasToPanZoomTransform
(
position
.
x
,
position
.
y
);
return
{
x
:
transformX
,
y
:
transformY
,
zoom
:
position
.
zoom
,
};
}
const
Cursor
=
()
=>
{
const
{
cursor
}
=
useAppContext
();
const
[
color
,
setColor
]
=
useState
<
string
>
();
useEffect
(()
=>
{
if
(
typeof
cursor
.
color
===
"
number
"
)
{
const
color
=
Canvas
.
instance
?.
Pallete
.
getColor
(
cursor
.
color
);
setColor
(
color
?.
hex
);
}
else
{
setColor
(
undefined
);
}
},
[
setColor
,
cursor
.
color
]);
if
(
!
color
)
return
<></>;
return
(
<
div
className
=
"noselect"
style
=
{
{
position
:
"
absolute
"
,
top
:
cursor
.
y
,
left
:
cursor
.
x
,
backgroundColor
:
"
#
"
+
color
,
width
:
"
1px
"
,
height
:
"
1px
"
,
opacity
:
0.5
,
}
}
></
div
>
);
};
const
CanvasInner
=
()
=>
{
const
canvasRef
=
createRef
<
HTMLCanvasElement
>
();
const
{
config
,
setCanvasPosition
,
setCursorPosition
}
=
useAppContext
();
const
canvasRef
=
useRef
<
HTMLCanvasElement
|
null
>
();
const
canvas
=
useRef
<
Canvas
>
();
const
{
config
,
setCanvasPosition
,
setCursor
,
setPixelWhois
}
=
useAppContext
();
const
{
x
:
templateX
,
y
:
templateY
,
enable
:
templateEnable
,
}
=
useTemplateContext
();
const
PanZoom
=
useContext
(
RendererContext
);
useEffect
(()
=>
{
if
(
!
config
.
canvas
||
!
canvasRef
.
current
)
return
;
const
canvas
=
canvasRef
.
current
!
;
const
canvasInstance
=
new
Canvas
(
config
,
canvas
,
PanZoom
);
/**
* Is the canvas coordinate within the bounds of the canvas?
*/
const
isCoordInCanvas
=
useCallback
(
(
x
:
number
,
y
:
number
):
boolean
=>
{
if
(
!
canvas
.
current
)
{
console
.
warn
(
"
[CanvasWrapper#isCoordInCanvas] canvas instance does not exist
"
);
return
false
;
}
if
(
x
<
0
||
y
<
0
)
return
false
;
// not positive, impossible to be on canvas
// canvas size can dynamically change, so we need to check the current config
// we're depending on canvas.instance's config so we don't have to use a react dependency
if
(
canvas
.
current
.
hasConfig
())
{
const
{
canvas
:
{
size
:
[
width
,
height
],
},
}
=
canvas
.
current
.
getConfig
();
{
// TODO: handle hash changes and move viewport
// NOTE: this will need to be cancelled if handleViewportMove was executed recently
if
(
x
>=
width
||
y
>=
height
)
return
false
;
// out of bounds
}
else
{
// although this should never happen, log it
console
.
warn
(
"
[CanvasWrapper#isCoordInCanvas] canvas config is not available yet
"
);
}
const
position
=
parseHashParams
(
canvasInstance
);
if
(
position
)
{
PanZoom
.
setPosition
(
position
,
{
suppressEmit
:
true
});
return
true
;
},
[
canvas
.
current
]
);
const
handlePixelWhois
=
useCallback
(
({
clientX
,
clientY
}:
{
clientX
:
number
;
clientY
:
number
})
=>
{
if
(
!
canvas
.
current
)
{
console
.
warn
(
"
[CanvasWrapper#handlePixelWhois] canvas instance does not exist
"
);
return
;
}
}
const
handleViewportMove
=
throttle
((
state
:
ViewportMoveEvent
)
=>
{
const
pos
=
canvasInstance
.
panZoomTransformToCanvas
();
const
[
x
,
y
]
=
canvas
.
current
.
screenToPos
(
clientX
,
clientY
);
if
(
!
isCoordInCanvas
(
x
,
y
))
return
;
// out of bounds
const
canvasPosition
:
ICanvasPosition
=
{
x
:
pos
.
canvasX
,
y
:
pos
.
canvasY
,
zoom
:
state
.
scale
>>
0
,
};
// .......
// .......
// .......
// ...x...
// .......
// .......
// .......
const
surrounding
=
canvas
.
current
.
getSurroundingPixels
(
x
,
y
,
3
);
setPixelWhois
({
x
,
y
,
surrounding
});
},
[
canvas
.
current
]
);
const
getTemplatePixel
=
useCallback
(
(
x
:
number
,
y
:
number
)
=>
{
if
(
!
templateEnable
)
return
;
if
(
x
<
templateX
||
y
<
templateY
)
return
;
x
-=
templateX
;
y
-=
templateY
;
setCanvasPosition
(
canvasPosition
);
return
TemplateCl
.
instance
.
getPixel
(
x
,
y
);
},
[
templateX
,
templateY
]
);
const
handlePickPixel
=
useCallback
(
({
clientX
,
clientY
}:
{
clientX
:
number
;
clientY
:
number
})
=>
{
if
(
!
canvas
.
current
)
{
console
.
warn
(
"
[CanvasWrapper#handlePickPixel] canvas instance does not exist
"
);
return
;
}
const
[
x
,
y
]
=
canvas
.
current
.
screenToPos
(
clientX
,
clientY
);
if
(
!
isCoordInCanvas
(
x
,
y
))
return
;
// out of bounds
let
pixelColor
=
-
1
;
window
.
location
.
replace
(
Routes
.
canvas
(
canvasPosition
));
},
1000
);
const
templatePixel
=
getTemplatePixel
(
x
,
y
);
if
(
templatePixel
)
{
pixelColor
=
canvas
.
current
.
Pallete
.
getColorFromHex
(
templatePixel
.
slice
(
1
))?.
id
||
-
1
;
}
if
(
pixelColor
===
-
1
)
{
pixelColor
=
canvas
.
current
.
getPixel
(
x
,
y
)?.
color
||
-
1
;
}
if
(
pixelColor
===
-
1
)
{
return
;
}
// no need to use canvas#setCursor as Palette.tsx already does that
setCursor
((
v
)
=>
({
...
v
,
color
:
pixelColor
,
}));
},
[
canvas
.
current
]
);
useEffect
(()
=>
{
if
(
!
canvasRef
.
current
)
return
;
canvas
.
current
=
new
Canvas
(
canvasRef
.
current
!
,
PanZoom
);
canvas
.
current
.
on
(
"
canvasReady
"
,
()
=>
{
console
.
log
(
"
[CanvasWrapper] received canvasReady
"
);
});
KeybindManager
.
on
(
"
PIXEL_WHOIS
"
,
handlePixelWhois
);
KeybindManager
.
on
(
"
PICK_COLOR
"
,
handlePickPixel
);
return
()
=>
{
KeybindManager
.
off
(
"
PIXEL_WHOIS
"
,
handlePixelWhois
);
KeybindManager
.
off
(
"
PICK_COLOR
"
,
handlePickPixel
);
canvas
.
current
!
.
destroy
();
};
},
[
PanZoom
]);
useEffect
(()
=>
{
Router
.
PanZoom
=
PanZoom
;
},
[
PanZoom
]);
useEffect
(()
=>
{
if
(
!
canvas
.
current
)
{
console
.
warn
(
"
canvas isntance doesn't exist
"
);
return
;
}
const
handleCursorPos
=
throttle
((
pos
:
IPosition
)
=>
{
if
(
!
canvas
.
current
?.
hasConfig
()
||
!
config
)
{
console
.
warn
(
"
handleCursorPos has no config
"
);
return
;
}
if
(
pos
.
x
<
0
||
pos
.
y
<
0
||
pos
.
x
>
config
.
canvas
.
size
[
0
]
||
pos
.
y
>
config
.
canvas
.
size
[
1
]
)
{
setCursorPosition
();
setCursor
((
v
)
=>
({
...
v
,
x
:
undefined
,
y
:
undefined
,
}));
}
else
{
// fixes not passing the current value
setCursorPosition
({
...
pos
});
setCursor
((
v
)
=>
({
...
v
,
x
:
pos
.
x
,
y
:
pos
.
y
,
}));
}
},
1
);
canvas
.
current
.
on
(
"
cursorPos
"
,
handleCursorPos
);
return
()
=>
{
canvas
.
current
!
.
off
(
"
cursorPos
"
,
handleCursorPos
);
};
},
[
config
,
setCursor
]);
useEffect
(()
=>
{
if
(
!
canvas
.
current
)
{
console
.
warn
(
"
canvasinner config received but no canvas instance
"
);
return
;
}
if
(
!
config
)
{
console
.
warn
(
"
canvasinner config received falsey
"
);
return
;
}
console
.
log
(
"
[CanvasInner] config updated, informing canvas instance
"
);
canvas
.
current
.
loadConfig
(
config
);
},
[
config
]);
const
handleNavigate
=
useCallback
(
(
data
:
IRouterData
)
=>
{
if
(
data
.
canvas
)
{
const
position
=
canvas
.
current
!
.
canvasToPanZoomTransform
(
data
.
canvas
.
x
,
data
.
canvas
.
y
);
PanZoom
.
setPosition
(
{
x
:
position
.
transformX
,
y
:
position
.
transformY
,
zoom
:
data
.
canvas
.
zoom
||
0
,
// TODO: fit canvas to viewport instead of defaulting
},
{
suppressEmit
:
true
}
);
}
},
[
PanZoom
]
);
useEffect
(()
=>
{
// if (!config?.canvas || !canvasRef.current) return;
// const canvas = canvasRef.current!;
// const canvasInstance = new Canvas(canvas, PanZoom);
const
initAt
=
Date
.
now
();
// initial position from Router is setup in <CanvasWrapper>
const
handleViewportMove
=
(
state
:
ViewportMoveEvent
)
=>
{
if
(
Date
.
now
()
-
initAt
<
60
*
1000
)
{
console
.
debug
(
"
[CanvasWrapper] handleViewportMove called soon after init
"
,
Date
.
now
()
-
initAt
);
}
if
(
canvas
.
current
)
{
const
pos
=
canvas
.
current
?.
panZoomTransformToCanvas
();
setCanvasPosition
({
x
:
pos
.
canvasX
,
y
:
pos
.
canvasY
,
zoom
:
state
.
scale
>>
0
,
});
}
else
{
console
.
warn
(
"
[CanvasWrapper] handleViewportMove has no canvas instance
"
);
}
Router
.
queueUpdate
();
};
PanZoom
.
addListener
(
"
viewportMove
"
,
handleViewportMove
);
canvasInstance
.
on
(
"
cursorPos
"
,
handleCursorPos
);
Router
.
on
(
"
navigate
"
,
handleNavigate
);
return
()
=>
{
canvasInstance
.
destroy
();
PanZoom
.
removeListener
(
"
viewportMove
"
,
handleViewportMove
);
canvasInstance
.
off
(
"
cursorPos
"
,
handleCursorPos
);
Router
.
off
(
"
navigate
"
,
handleNavigate
);
};
// ! do not include canvasRef, it causes infinite re-renders
},
[
PanZoom
,
config
,
setCanvasPosition
,
setCursorPosition
]);
},
[
PanZoom
,
setCanvasPosition
]);
return
(
<
canvas
...
...
@@ -125,7 +366,7 @@ const CanvasInner = () => {
width
=
"1000"
height
=
"1000"
className
=
"pixelate"
ref
=
{
canvasRef
}
ref
=
{
(
ref
)
=>
(
canvasRef
.
current
=
ref
)
}
></
canvas
>
);
};
packages/client/src/components/Chat/Chat.tsx
0 → 100644
View file @
51eacdaf
import
{
useRef
}
from
"
react
"
;
const
Chat
=
()
=>
{
const
ref
=
useRef
<
HTMLDivElement
|
null
>
(
null
);
return
(
<
div
ref
=
{
ref
}
style
=
{
{
position
:
"
fixed
"
,
top
:
0
,
left
:
0
,
zIndex
:
999
}
}
>
chat
</
div
>
);
};
export
default
Chat
;
packages/client/src/components/Chat/InnerChatSettings.tsx
0 → 100644
View file @
51eacdaf
import
{
Button
}
from
"
@nextui-org/react
"
;
import
{
useChatContext
}
from
"
../../contexts/ChatContext
"
;
import
{
useAppContext
}
from
"
../../contexts/AppContext
"
;
const
InnerChatSettings
=
()
=>
{
const
{
user
:
authUser
}
=
useAppContext
();
const
{
user
,
doLogin
,
doLogout
}
=
useChatContext
();
if
(
!
authUser
)
{
return
<>
You must be logged in first
</>;
}
return
(
<>
{
!
user
&&
<
Button
onClick
=
{
doLogin
}
>
Login
</
Button
>
}
{
user
&&
(
<>
<
div
className
=
"flex gap-1"
>
<
div
className
=
"flex-grow"
>
{
user
.
userId
}
</
div
>
<
Button
onClick
=
{
doLogout
}
>
Logout
</
Button
>
</
div
>
</>
)
}
</>
);
};
export
default
InnerChatSettings
;
packages/client/src/components/Chat/OpenChatButton.tsx
0 → 100644
View file @
51eacdaf
import
{
Badge
,
Button
}
from
"
@nextui-org/react
"
;
import
{
useChatContext
}
from
"
../../contexts/ChatContext
"
;
import
{
useAppContext
}
from
"
../../contexts/AppContext
"
;
import
{
FontAwesomeIcon
}
from
"
@fortawesome/react-fontawesome
"
;
import
{
faComments
}
from
"
@fortawesome/free-solid-svg-icons
"
;
const
OpenChatButton
=
()
=>
{
const
{
config
}
=
useAppContext
();
const
{
notificationCount
,
doLogin
}
=
useChatContext
();
return
(
<
Badge
content
=
{
notificationCount
}
isInvisible
=
{
notificationCount
===
0
}
color
=
"danger"
size
=
"sm"
>
{
config
?.
chat
?.
element_host
&&
(
<
Button
onPress
=
{
doLogin
}
variant
=
"faded"
>
<
FontAwesomeIcon
icon
=
{
faComments
}
/>
<
p
>
Chat
</
p
>
</
Button
>
)
}
</
Badge
>
);
};
export
default
OpenChatButton
;
packages/client/src/components/Debug/DebugModal.tsx
0 → 100644
View file @
51eacdaf
import
{
useEffect
}
from
"
react
"
;
import
{
Debug
,
FlagCategory
}
from
"
@sc07-canvas/lib/src/debug
"
;
import
{
Button
,
Modal
,
ModalBody
,
ModalContent
,
ModalFooter
,
ModalHeader
,
Switch
,
useDisclosure
,
}
from
"
@nextui-org/react
"
;
export
const
DebugModal
=
()
=>
{
const
{
isOpen
,
onOpen
,
onOpenChange
}
=
useDisclosure
();
useEffect
(()
=>
{
const
handleOpen
=
()
=>
{
onOpen
();
};
Debug
.
on
(
"
openTools
"
,
handleOpen
);
return
()
=>
{
Debug
.
off
(
"
openTools
"
,
handleOpen
);
};
},
[]);
return
(
<
Modal
isOpen
=
{
isOpen
}
onOpenChange
=
{
onOpenChange
}
placement
=
"center"
>
<
ModalContent
>
{
(
onClose
)
=>
(
<>
<
ModalHeader
className
=
"flex flex-col gap-1"
>
Debug Tools
</
ModalHeader
>
<
ModalBody
>
<
Button
onPress
=
{
()
=>
Debug
.
openDebug
()
}
>
Open Debug Information
</
Button
>
{
Debug
.
flags
.
getAll
().
map
((
flag
,
i
,
arr
)
=>
(
<>
{
arr
[
i
-
1
]?.
category
!==
flag
.
category
&&
(
<
p
>
{
FlagCategory
[
flag
.
category
]
}
</
p
>
)
}
<
div
key
=
{
flag
.
id
}
>
<
Switch
size
=
"sm"
defaultSelected
=
{
flag
.
enabled
}
onValueChange
=
{
(
v
)
=>
Debug
.
flags
.
setEnabled
(
flag
.
id
,
v
)
}
>
{
flag
.
id
}
</
Switch
>
</
div
>
</>
))
}
</
ModalBody
>
<
ModalFooter
>
<
Button
onPress
=
{
onClose
}
>
Close
</
Button
>
</
ModalFooter
>
</>
)
}
</
ModalContent
>
</
Modal
>
);
};
packages/client/src/components/DynamicModals.tsx
0 → 100644
View file @
51eacdaf
import
{
useCallback
,
useEffect
,
useState
}
from
"
react
"
;
import
{
DynamicModal
,
IDynamicModal
}
from
"
../lib/alerts
"
;
import
{
Button
,
Modal
,
ModalBody
,
ModalContent
,
ModalFooter
,
ModalHeader
,
}
from
"
@nextui-org/react
"
;
interface
IModal
{
id
:
number
;
open
:
boolean
;
modal
:
IDynamicModal
;
}
/**
* React base to hold dynamic modals
*
* Dynamic modals are created via lib/alerts.tsx
*
* @returns
*/
export
const
DynamicModals
=
()
=>
{
const
[
modals
,
setModals
]
=
useState
<
IModal
[]
>
([]);
const
handleShowModal
=
useCallback
(
(
modal
:
IDynamicModal
)
=>
{
setModals
((
modals
)
=>
[
...
modals
,
{
id
:
Math
.
floor
(
Math
.
random
()
*
9999
),
open
:
true
,
modal
,
},
]);
},
[
setModals
]
);
const
handleHideModal
=
useCallback
(
(
modalId
:
number
)
=>
{
setModals
((
modals_
)
=>
{
const
modals
=
[...
modals_
];
if
(
modals
.
find
((
m
)
=>
m
.
id
===
modalId
))
{
modals
.
find
((
m
)
=>
m
.
id
===
modalId
)
!
.
open
=
false
;
}
return
modals
;
});
setTimeout
(()
=>
{
setModals
((
modals_
)
=>
{
const
modals
=
[...
modals_
];
if
(
modals
.
find
((
m
)
=>
m
.
id
===
modalId
))
{
modals
.
splice
(
modals
.
indexOf
(
modals
.
find
((
m
)
=>
m
.
id
===
modalId
)
!
),
1
);
}
return
modals
;
});
},
1000
);
},
[
setModals
]
);
useEffect
(()
=>
{
DynamicModal
.
on
(
"
showModal
"
,
handleShowModal
);
return
()
=>
{
DynamicModal
.
off
(
"
showModal
"
,
handleShowModal
);
};
},
[]);
return
(
<>
{
modals
.
map
(({
id
,
open
,
modal
})
=>
(
<
Modal
key
=
{
id
}
isOpen
=
{
open
}
onClose
=
{
()
=>
handleHideModal
(
id
)
}
>
<
ModalContent
>
{
(
onClose
)
=>
(
<>
<
ModalHeader
>
{
modal
.
title
}
</
ModalHeader
>
<
ModalBody
>
{
modal
.
body
}
</
ModalBody
>
<
ModalFooter
>
<
Button
onClick
=
{
onClose
}
>
Close
</
Button
>
</
ModalFooter
>
</>
)
}
</
ModalContent
>
</
Modal
>
))
}
</>
);
};
packages/client/src/components/EventInfoOverlay.tsx
0 → 100644
View file @
51eacdaf
import
{
useAppContext
}
from
"
../contexts/AppContext
"
;
import
{
Button
,
Link
}
from
"
@nextui-org/react
"
;
/**
* *oh god the terrible code*
*
* not sure of another clean way to do this
*
* This is used to show details about the event, immediately on page load
*
* used by the canvas preview page to get people hyped up for the event (<7 days before)
*/
export
const
EventInfoOverlay
=
()
=>
{
const
{
setInfoSidebar
,
setSettingsSidebar
}
=
useAppContext
();
return
(
<
div
className
=
"bg-black text-white p-4 fixed top-0 left-0 w-full z-[9999] flex flex-row"
style
=
{
{
pointerEvents
:
"
initial
"
,
}
}
>
<
div
>
<
h1
className
=
"text-4xl font-bold"
>
Canvas 2024
</
h1
>
<
h2
className
=
"text-3xl"
>
Ended July 16th @ 4am UTC
</
h2
>
</
div
>
<
div
className
=
"flex-grow"
/>
<
div
>
<
Button
as
=
{
Link
}
href
=
"https://sc07.shop"
color
=
"primary"
>
Shop (poster prints!)
</
Button
>
<
Button
onPress
=
{
()
=>
setInfoSidebar
(
true
)
}
>
Info
</
Button
>
<
Button
onPress
=
{
()
=>
setSettingsSidebar
(
true
)
}
>
Settings
</
Button
>
</
div
>
</
div
>
);
};
packages/client/src/components/Header.tsx
deleted
100644 → 0
View file @
e09b78cf
import
{
User
}
from
"
./Header/User
"
;
export
const
Header
=
()
=>
{
return
(
<
header
id
=
"main-header"
>
<
div
></
div
>
<
div
className
=
"spacer"
></
div
>
<
div
className
=
"box"
>
<
User
/>
</
div
>
</
header
>
);
};
Prev
1
2
3
4
5
6
…
9
Next