fixes
Signed-off-by: darwinx64 <tiramisyuz@proton.me>
This commit is contained in:
parent
8ed804a5e6
commit
98412adab9
35 changed files with 1821 additions and 1424 deletions
|
@ -1,6 +0,0 @@
|
|||
---
|
||||
platform: "Email"
|
||||
name: "support@nin0.dev"
|
||||
url: "mailto:support@nin0.dev"
|
||||
note: "Replies may be slow"
|
||||
---
|
|
@ -1,6 +0,0 @@
|
|||
---
|
||||
platform: "Telegram"
|
||||
url: "https://nin0dev.t.me"
|
||||
name: "@nin0dev"
|
||||
note: "Use for quick replies"
|
||||
---
|
19
package.json
19
package.json
|
@ -10,22 +10,21 @@
|
|||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"7.css": "^0.16.0",
|
||||
"@astrojs/cloudflare": "^12.2.4",
|
||||
"@astrojs/node": "^9.1.3",
|
||||
"@astrojs/react": "^4.2.1",
|
||||
"@astrojs/cloudflare": "^12.5.0",
|
||||
"@astrojs/node": "^9.2.0",
|
||||
"@astrojs/react": "^4.2.4",
|
||||
"@fontsource-variable/noto-emoji": "^5.2.5",
|
||||
"@fontsource/inter": "^5.2.5",
|
||||
"@types/react": "^19.0.10",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"astro": "^5.5.2",
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"astro": "^5.7.4",
|
||||
"color.js": "^1.2.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-astro": "^0.14.1",
|
||||
"wrangler": "^3.112.0"
|
||||
"wrangler": "^3.114.6"
|
||||
}
|
||||
}
|
2181
pnpm-lock.yaml
generated
2181
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
6
src/assets/svg/envelope.svg
Normal file
6
src/assets/svg/envelope.svg
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 20.0234 14.1406">
|
||||
<g>
|
||||
<path d="M9.80469 7.65625C10.0312 7.65625 10.2344 7.56641 10.4805 7.34766L18.3477 0.339844C17.9336 0.113281 17.4609 0.0078125 16.8555 0.0078125L2.75781 0.0078125C2.15625 0.0078125 1.68359 0.113281 1.26172 0.339844L9.13281 7.34766C9.37891 7.56641 9.58594 7.65625 9.80469 7.65625ZM0.1875 12.5703L6.01953 6.74609L0.195312 1.55469C0.0859375 1.82031 0 2.23438 0 2.75L0 11.3984C0 11.8711 0.0703125 12.2461 0.1875 12.5703ZM2.62891 14.1406L16.9883 14.1406C17.5547 14.1406 18.0273 14.0195 18.3789 13.8281L12.375 7.82422L11.4336 8.66406C10.8984 9.14062 10.3984 9.35156 9.80469 9.35156C9.21875 9.35156 8.71875 9.14062 8.18359 8.66406L7.24219 7.82422L1.23828 13.8281C1.58984 14.0195 2.05859 14.1406 2.62891 14.1406ZM19.4297 12.5703C19.543 12.2461 19.6172 11.8711 19.6172 11.3984L19.6172 2.75C19.6172 2.23438 19.5312 1.82031 19.4219 1.55469L13.5938 6.74609Z" fill="currentColor"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1 KiB |
4
src/assets/svg/external.svg
Normal file
4
src/assets/svg/external.svg
Normal file
|
@ -0,0 +1,4 @@
|
|||
<svg role="img" aria-hidden="true" viewBox="0 0 24 24" stroke-width="2" fill="none">
|
||||
<path d="M21 3h-6m6 0l-9 9m9-9v6" stroke-width="2" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M21 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 348 B |
9
src/assets/svg/forgejo.svg
Normal file
9
src/assets/svg/forgejo.svg
Normal file
|
@ -0,0 +1,9 @@
|
|||
<svg viewBox="0 0 212 212" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="translate(6,6)">
|
||||
<path d="M58 168 v-98 a50 50 0 0 1 50-50 h20" stroke="currentColor" stroke-width="25" fill="none" />
|
||||
<path d="M58 168 v-30 a50 50 0 0 1 50-50 h20" stroke="currentColor" stroke-width="25" fill="none" />
|
||||
<circle cx="142" cy="20" r="18" stroke="currentColor" stroke-width="15" fill="none" />
|
||||
<circle cx="142" cy="88" r="18" stroke="currentColor" stroke-width="15" fill="none" />
|
||||
<circle cx="58" cy="180" r="18" stroke="currentColor" stroke-width="15" fill="none" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 593 B |
1
src/assets/svg/github.svg
Normal file
1
src/assets/svg/github.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg viewBox="0 0 98 96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="currentColor"/></svg>
|
After Width: | Height: | Size: 965 B |
6
src/assets/svg/plane.svg
Normal file
6
src/assets/svg/plane.svg
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 18.0508 17.6562">
|
||||
<g>
|
||||
<path d="M10.0156 17.6562C10.6836 17.6562 11.1406 17.1562 11.4297 16.4023L16.6484 2.74609C16.7773 2.41797 16.8438 2.11719 16.8438 1.86328C16.8438 1.22266 16.4336 0.816406 15.793 0.816406C15.5352 0.816406 15.2344 0.882812 14.9102 1.00781L1.21875 6.24609C0.511719 6.51172 0 6.96875 0 7.64062C0 8.43359 0.574219 8.76953 1.37109 9.01562L5.125 10.1719C5.75 10.3672 6.13672 10.3633 6.59766 9.94922L15.5117 1.79297C15.6328 1.68359 15.7852 1.69531 15.875 1.78516C15.9609 1.875 15.9766 2.02734 15.8633 2.14844L7.72656 11.0547C7.33594 11.4805 7.30078 11.9297 7.48438 12.5312L8.62891 16.2422C8.87891 17.0586 9.21875 17.6562 10.0156 17.6562Z" fill="currentColor"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 851 B |
6
src/assets/svg/xmark.svg
Normal file
6
src/assets/svg/xmark.svg
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 12.9879 12.5923">
|
||||
<g>
|
||||
<path d="M0.250141 12.3415C0.589669 12.6732 1.16232 12.669 1.4856 12.3457L6.29154 7.53973L11.0947 12.3443C11.4236 12.6732 11.9956 12.6782 12.3287 12.3401C12.6619 12.0005 12.6633 11.4413 12.3344 11.111L7.53122 6.30005L12.3344 1.49692C12.6633 1.16802 12.6683 0.602398 12.3287 0.269275C11.9892-0.0702532 11.4236-0.0716594 11.0947 0.263651L6.29154 5.06678L1.4856 0.262244C1.16232-0.0660346 0.583263-0.0780657 0.250141 0.267869C-0.0815755 0.607398-0.0773569 1.17224 0.245922 1.49552L5.05187 6.30005L0.245922 11.1138C-0.0773569 11.4357-0.0879818 12.0083 0.250141 12.3415Z" fill="currentColor"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 776 B |
|
@ -1,29 +1,80 @@
|
|||
---
|
||||
const { position, text, url, id, onClick } = Astro.props;
|
||||
const { position, text, url, id, onClick, name } = Astro.props;
|
||||
import ExternalIcon from "@assets/svg/external.svg";
|
||||
---
|
||||
|
||||
<style>
|
||||
.button-wrapper {
|
||||
background: linear-gradient(#343343, #2E2E3E, #2E2E3E);
|
||||
transition: 0.54s linear(0, 0.0021, 0.0081 1.26%, 0.0302 2.51%, 0.106, 0.2088 7.53%, 0.5809 15.7%, 0.7667 20.72%, 0.8391 23.23%, 0.9106 26.37%, 0.9538 28.88%, 0.9926 32.02%, 1.0134, 1.0269 37.04%, 1.0359, 1.0384 43.32%, 1.0353 47.72%, 1.0124 61.53%, 1.003 70.32%, 0.9988 81.62%, 0.9992 99.83%);
|
||||
clip-path: var(--clip-button-outer);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
button,
|
||||
a {
|
||||
width: 100%;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
button {
|
||||
background-color: #45475a;
|
||||
border: 1px var(--text) solid;
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
background-color: #2B2B3B;
|
||||
clip-path: var(--clip-button-inner);
|
||||
color: var(--text);
|
||||
padding: 8px 20px;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
button:hover {
|
||||
background-color: #585b70;
|
||||
.button-wrapper:has(button:hover:active) {
|
||||
background: #2F2F41;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
button:hover:active {
|
||||
background-color: #2F2F41;
|
||||
}
|
||||
.link-button {
|
||||
padding: 15px;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
.link-button-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.link-button-heading > span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
line-height: 0;
|
||||
}
|
||||
:global(.link-button-heading svg) {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
</style>
|
||||
{
|
||||
url ? (
|
||||
<a href={url}>
|
||||
<button id={id}>{text}</button>
|
||||
<div class="button-wrapper">
|
||||
<button id={id} class="link-button">
|
||||
{ name && <div class="link-button-heading">
|
||||
<span>
|
||||
<slot name="icon" />
|
||||
<b>{name}</b>
|
||||
</span>
|
||||
<ExternalIcon />
|
||||
</div> }
|
||||
<slot />
|
||||
</button>
|
||||
</div>
|
||||
</a>
|
||||
) : (
|
||||
<button id={id}>{text}</button>
|
||||
<div class="button-wrapper">
|
||||
<button id={id}><slot /></button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
<style>
|
||||
div {
|
||||
display: grid;
|
||||
grid-auto-flow: row;
|
||||
row-gap: 8px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
</style>
|
||||
<div class="button-collection">
|
||||
<slot />
|
||||
</div>
|
|
@ -3,17 +3,23 @@ const { name } = Astro.props;
|
|||
---
|
||||
|
||||
<style>
|
||||
div {
|
||||
margin-top: 15px;
|
||||
margin-bottom: 0px;
|
||||
padding: 0px 15px;
|
||||
border-radius: 15px;
|
||||
border: 1px solid #313244;
|
||||
background-color: #313244;
|
||||
.group-box-wrapper {
|
||||
background: linear-gradient(#343343, #2E2E3E, #2E2E3E);
|
||||
clip-path: var(--clip-group-box-outer)
|
||||
}
|
||||
.group-box {
|
||||
padding: 15px;
|
||||
clip-path: var(--clip-group-box-inner);
|
||||
background-color: #2B2B3B;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div>
|
||||
<div class="group-box-wrapper">
|
||||
<div class="group-box">
|
||||
<h3>{name}</h3>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
|
@ -1 +0,0 @@
|
|||
<span></span>
|
|
@ -3,6 +3,8 @@ const { title, showClose, maxWidth, hideByDefault, customClass, offset } =
|
|||
Astro.props;
|
||||
|
||||
const randomID = customClass || Math.random().toString().replace(".", "");
|
||||
|
||||
import XmarkIcon from "@assets/svg/xmark.svg";
|
||||
---
|
||||
|
||||
<style define:vars={{ maxWidth, offset }}>
|
||||
|
@ -10,25 +12,30 @@ const randomID = customClass || Math.random().toString().replace(".", "");
|
|||
width: var(--maxWidth);
|
||||
top: var(--offset);
|
||||
left: var(--offset);
|
||||
border-radius: 17px;
|
||||
border: 1px solid var(--text);
|
||||
background: var(--surface1);
|
||||
clip-path: var(--clip-window-outer);
|
||||
position: absolute;
|
||||
}
|
||||
.title-bar {
|
||||
.title-bar-text {
|
||||
margin-top: 2.5px !important;
|
||||
.window-inner {
|
||||
background-color: #20202c;
|
||||
clip-path: var(--clip-window-inner);
|
||||
}
|
||||
.title-bar {
|
||||
user-select: none;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
background-color: #313244;
|
||||
border-radius: 15px 15px 0 0;
|
||||
padding: 9px 18px 9px 18px;
|
||||
align-items: stretch;
|
||||
height: 42px;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
.title-bar span {
|
||||
align-self: center;
|
||||
}
|
||||
svg {
|
||||
fill: var(--text);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 24px !important;
|
||||
height: 24px;
|
||||
}
|
||||
#close {
|
||||
background: none;
|
||||
|
@ -36,15 +43,16 @@ const randomID = customClass || Math.random().toString().replace(".", "");
|
|||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
height: 24px !important;
|
||||
width: 50px;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
outline: inherit;
|
||||
}
|
||||
.window-body {
|
||||
padding: 20px !important;
|
||||
background-color: #1e1e2e;
|
||||
border-radius: 0 0 17px 17px;
|
||||
gap: 1rem;
|
||||
display: flex;
|
||||
padding: 0 1rem 1rem 1rem;
|
||||
flex-direction: column;
|
||||
}
|
||||
@media (pointer: coarse) {
|
||||
.window {
|
||||
|
@ -64,8 +72,9 @@ const randomID = customClass || Math.random().toString().replace(".", "");
|
|||
style={hideByDefault && "display: none;"}
|
||||
>
|
||||
<div class="window" style="max-width: 100%">
|
||||
<div class="window-inner">
|
||||
<div class="title-bar">
|
||||
<div class="title-bar-text">{title}</div>
|
||||
<span>{title}</span>
|
||||
{
|
||||
showClose && (
|
||||
<button
|
||||
|
@ -73,13 +82,7 @@ const randomID = customClass || Math.random().toString().replace(".", "");
|
|||
aria-label="Close"
|
||||
onclick={`document.querySelector("#window-${randomID}").style.display = "none";`}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height="24px"
|
||||
viewBox="0 -960 960 960"
|
||||
>
|
||||
<path d="m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z" />
|
||||
</svg>
|
||||
<XmarkIcon width="12" height="12" />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
@ -89,3 +92,4 @@ const randomID = customClass || Math.random().toString().replace(".", "");
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
---
|
||||
import GroupBox from "@components/GroupBox.astro";
|
||||
import Button from "@components/Button.astro";
|
||||
import Window from "@components/Window.astro";
|
||||
|
||||
import ForgejoIcon from "@assets/svg/forgejo.svg";
|
||||
import GithubIcon from "@assets/svg/github.svg";
|
||||
---
|
||||
|
||||
<Window
|
||||
|
@ -11,24 +14,16 @@ import Window from "@components/Window.astro";
|
|||
customClass="code-window"
|
||||
offset="55px"
|
||||
>
|
||||
<p>
|
||||
As mentioned in the main page, I write code. Some used by a lot, some
|
||||
not as much. You can find it on:
|
||||
<GroupBox name="GitHub (@nin0-dev)">
|
||||
not as much. You can find it below.
|
||||
<Button name="GitHub" url="https://gh.nin0.dev/">
|
||||
<GithubIcon slot="icon" />
|
||||
Here, you'll find most of my external contributions. I rarely start
|
||||
personal projects on GitHub.
|
||||
<br />
|
||||
<a href="https://gh.nin0.dev">Go!</a>
|
||||
<br />
|
||||
<br />
|
||||
</GroupBox>
|
||||
<GroupBox name="Forgejo (@nin0)">
|
||||
</Button>
|
||||
<Button name="Forgejo" url="https://git.nin0.dev/nin0">
|
||||
<ForgejoIcon slot="icon" />
|
||||
Some more personal/niche projects can be found on my own instance
|
||||
(git.nin0.dev), which will probably be down.
|
||||
<br />
|
||||
<a href="https://git.nin0.dev/nin0">Go!</a>
|
||||
<br />
|
||||
<br />
|
||||
</GroupBox>
|
||||
</p>
|
||||
</Button>
|
||||
</Window>
|
||||
|
|
21
src/components/windows/mainWindow/Chip.astro
Normal file
21
src/components/windows/mainWindow/Chip.astro
Normal file
|
@ -0,0 +1,21 @@
|
|||
---
|
||||
const { ...attrs } = Astro.props
|
||||
---
|
||||
<a {...attrs}>
|
||||
<slot name="icon" />
|
||||
<slot />
|
||||
</a>
|
||||
<style>
|
||||
a {
|
||||
clip-path: var(--clip-chip);
|
||||
background-color: rgba(var(--lavender-rgb), 0.2);
|
||||
color: rgba(var(--lavender-rgb), 0.75);
|
||||
font-weight: 600;
|
||||
padding: 2px 7px;
|
||||
margin-right: 10px;
|
||||
gap: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
|
@ -1,22 +1,22 @@
|
|||
---
|
||||
import Social from "./Social.astro";
|
||||
|
||||
const contacts: any = Object.values(
|
||||
import.meta.glob("../../../../info/contacts/*.md", {
|
||||
eager: true
|
||||
})
|
||||
);
|
||||
import Chip from "./Chip.astro";
|
||||
import Envelope from "@assets/svg/envelope.svg";
|
||||
import Plane from "@assets/svg/plane.svg";
|
||||
---
|
||||
|
||||
<ul>
|
||||
{
|
||||
contacts.map(contact => (
|
||||
<Social
|
||||
platform={contact.frontmatter.platform}
|
||||
url={contact.frontmatter.url}
|
||||
name={contact.frontmatter.name}
|
||||
note={contact.frontmatter.note}
|
||||
/>
|
||||
))
|
||||
<div>
|
||||
<Chip href="mailto:support@nin0.dev">
|
||||
<Envelope slot="icon" />
|
||||
support@nin0.dev
|
||||
</Chip>
|
||||
<Chip href="https://nin0dev.t.me">
|
||||
<Plane slot="icon" />
|
||||
@nin0dev
|
||||
</Chip>
|
||||
</div>
|
||||
<style>
|
||||
div {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
</ul>
|
||||
</style>
|
||||
|
|
|
@ -4,29 +4,22 @@ import GroupBox from "@components/GroupBox.astro";
|
|||
import Me from "./Me";
|
||||
import Window from "@components/Window.astro";
|
||||
import Button from "@components/Button.astro";
|
||||
import ButtonCollection from "@components/ButtonCollection.astro";
|
||||
import Spacer from "@components/Spacer.astro";
|
||||
---
|
||||
|
||||
<script>
|
||||
import { WindowManager } from "@models/WindowManager";
|
||||
|
||||
document
|
||||
.querySelector("#show-code-window")
|
||||
.addEventListener("click", () => {
|
||||
// @ts-ignore
|
||||
document.querySelector("#window-code-window").style.display =
|
||||
"block";
|
||||
});
|
||||
.addEventListener("click", () => WindowManager.topmost = document.getElementById("window-code-window"));
|
||||
</script>
|
||||
|
||||
<Window title="Home" maxWidth="600px" showClose={false}>
|
||||
<Me client:only />
|
||||
<ButtonCollection>
|
||||
<Button position="left" text="My code" id="show-code-window" />
|
||||
</ButtonCollection>
|
||||
<Button position="left" id="show-code-window">My code</Button>
|
||||
<GroupBox name="About me">
|
||||
<p>
|
||||
I'm a Canadian self-taught software developer.
|
||||
<br />
|
||||
I also make things that some people use with varying degrees of usefulness.
|
||||
</p>
|
||||
</GroupBox>
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import "@css/me.css";
|
||||
import { initLanyard } from "@js/lanyard/lanyard";
|
||||
import { type LanyardPresence } from "../../../js/lanyard/types";
|
||||
import { useEffect, useState } from "react";
|
||||
import type { LanyardPresence } from "types/lanyard";
|
||||
import { Lanyard } from "models/Lanyard";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { average } from "color.js";
|
||||
|
||||
const lightenColor = (color: string, percent: number) =>
|
||||
`hsl(from ${color} h s ${percent.toString()}%)`;
|
||||
const api_key = "3c5623fa1abbd11c49f53ca18e992ead";
|
||||
const apiKey = "3c5623fa1abbd11c49f53ca18e992ead";
|
||||
|
||||
export default function Me() {
|
||||
// me-e-eeeee
|
||||
|
@ -26,14 +26,12 @@ export default function Me() {
|
|||
>();
|
||||
|
||||
useEffect(() => {
|
||||
initLanyard(presence => {
|
||||
setPresence(presence);
|
||||
});
|
||||
new Lanyard("886685857560539176", p => setPresence(p));
|
||||
|
||||
async function getLastFmTracks() {
|
||||
const params = new URLSearchParams({
|
||||
method: "user.getrecenttracks",
|
||||
api_key,
|
||||
api_key: apiKey,
|
||||
user: "nin0dev",
|
||||
limit: "50",
|
||||
format: "json",
|
||||
|
@ -61,7 +59,7 @@ export default function Me() {
|
|||
`https://ws.audioscrobbler.com/2.0/?${new URLSearchParams(
|
||||
{
|
||||
method: "track.getinfo",
|
||||
api_key,
|
||||
api_key: apiKey,
|
||||
format: "json",
|
||||
track: currentlyPlayingTrack.name,
|
||||
artist: currentlyPlayingTrack.artist.name
|
||||
|
@ -105,33 +103,43 @@ export default function Me() {
|
|||
return (
|
||||
<>
|
||||
<div className="me-container">
|
||||
<img
|
||||
src="logo.png"
|
||||
alt="the nin0 logo"
|
||||
<div
|
||||
className="img-container"
|
||||
style={{
|
||||
borderColor: presence
|
||||
backgroundColor: presence
|
||||
? ["online", "idle"].includes(
|
||||
presence.discord_status
|
||||
)
|
||||
? "#a6e3a1"
|
||||
: "#6c7086"
|
||||
: "#6c7086"
|
||||
? "var(--green)"
|
||||
: "var(--overlay0)"
|
||||
: "var(--overlay0)"
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src="logo.png"
|
||||
alt="the nin0 logo"
|
||||
/>
|
||||
<h3>
|
||||
nin0 <span style={{ fontSize: "0.9rem" }}>(he/him)</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div>
|
||||
<h2>
|
||||
nin0
|
||||
</h2>
|
||||
<h4>
|
||||
he/him
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{currentlyPlaying && (
|
||||
<div
|
||||
className="spotify-card"
|
||||
className="spotify-card-wrapper"
|
||||
style={{
|
||||
borderColor: lightenColor(
|
||||
currentlyPlaying.mainColor,
|
||||
90
|
||||
)
|
||||
}}
|
||||
"--album-color": currentlyPlaying.mainColor,
|
||||
"--album-art": `url(${currentlyPlaying.albumArt})`
|
||||
} as React.CSSProperties}
|
||||
>
|
||||
<div
|
||||
className="spotify-card"
|
||||
>
|
||||
<h3
|
||||
style={{
|
||||
|
@ -140,12 +148,6 @@ export default function Me() {
|
|||
>
|
||||
Listening to
|
||||
</h3>
|
||||
<div
|
||||
className="spotify-card-background"
|
||||
style={{
|
||||
backgroundImage: `url(${currentlyPlaying.albumArt})`
|
||||
}}
|
||||
></div>
|
||||
<div
|
||||
className="spotify-card-content"
|
||||
style={{
|
||||
|
@ -236,6 +238,7 @@ export default function Me() {
|
|||
})()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
---
|
||||
const { platform, name, url, note } = Astro.props;
|
||||
---
|
||||
|
||||
<style>
|
||||
bl {
|
||||
font-weight: 500;
|
||||
}
|
||||
it {
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
|
||||
<li>
|
||||
<bl>{platform}</bl>: {url ? <a href={url}>{name}</a> : name}{
|
||||
note && <it> ({note})</it>
|
||||
}
|
||||
</li>
|
81
src/css/catppuccin.css
Normal file
81
src/css/catppuccin.css
Normal file
|
@ -0,0 +1,81 @@
|
|||
/* https://github.com/catppuccin/palette/blob/main/docs/css.md */
|
||||
:root {
|
||||
--rosewater: #f5e0dc;
|
||||
--rosewater-rgb: 245, 224, 220;
|
||||
--rosewater-hsl: 9.600, 55.556%, 91.176%;
|
||||
--flamingo: #f2cdcd;
|
||||
--flamingo-rgb: 242, 205, 205;
|
||||
--flamingo-hsl: 0.000, 58.730%, 87.647%;
|
||||
--pink: #f5c2e7;
|
||||
--pink-rgb: 245, 194, 231;
|
||||
--pink-hsl: 316.471, 71.831%, 86.078%;
|
||||
--mauve: #cba6f7;
|
||||
--mauve-rgb: 203, 166, 247;
|
||||
--mauve-hsl: 267.407, 83.505%, 80.980%;
|
||||
--red: #f38ba8;
|
||||
--red-rgb: 243, 139, 168;
|
||||
--red-hsl: 343.269, 81.250%, 74.902%;
|
||||
--maroon: #eba0ac;
|
||||
--maroon-rgb: 235, 160, 172;
|
||||
--maroon-hsl: 350.400, 65.217%, 77.451%;
|
||||
--peach: #fab387;
|
||||
--peach-rgb: 250, 179, 135;
|
||||
--peach-hsl: 22.957, 92.000%, 75.490%;
|
||||
--yellow: #f9e2af;
|
||||
--yellow-rgb: 249, 226, 175;
|
||||
--yellow-hsl: 41.351, 86.047%, 83.137%;
|
||||
--green: #a6e3a1;
|
||||
--green-rgb: 166, 227, 161;
|
||||
--green-hsl: 115.455, 54.098%, 76.078%;
|
||||
--teal: #94e2d5;
|
||||
--teal-rgb: 148, 226, 213;
|
||||
--teal-hsl: 170.000, 57.353%, 73.333%;
|
||||
--sky: #89dceb;
|
||||
--sky-rgb: 137, 220, 235;
|
||||
--sky-hsl: 189.184, 71.014%, 72.941%;
|
||||
--sapphire: #74c7ec;
|
||||
--sapphire-rgb: 116, 199, 236;
|
||||
--sapphire-hsl: 198.500, 75.949%, 69.020%;
|
||||
--blue: #89b4fa;
|
||||
--blue-rgb: 137, 180, 250;
|
||||
--blue-hsl: 217.168, 91.870%, 75.882%;
|
||||
--lavender: #b4befe;
|
||||
--lavender-rgb: 180, 190, 254;
|
||||
--lavender-hsl: 231.892, 97.368%, 85.098%;
|
||||
--text: #cdd6f4;
|
||||
--text-rgb: 205, 214, 244;
|
||||
--text-hsl: 226.154, 63.934%, 88.039%;
|
||||
--subtext1: #bac2de;
|
||||
--subtext1-rgb: 186, 194, 222;
|
||||
--subtext1-hsl: 226.667, 35.294%, 80.000%;
|
||||
--subtext0: #a6adc8;
|
||||
--subtext0-rgb: 166, 173, 200;
|
||||
--subtext0-hsl: 227.647, 23.611%, 71.765%;
|
||||
--overlay2: #9399b2;
|
||||
--overlay2-rgb: 147, 153, 178;
|
||||
--overlay2-hsl: 228.387, 16.757%, 63.725%;
|
||||
--overlay1: #7f849c;
|
||||
--overlay1-rgb: 127, 132, 156;
|
||||
--overlay1-hsl: 229.655, 12.775%, 55.490%;
|
||||
--overlay0: #6c7086;
|
||||
--overlay0-rgb: 108, 112, 134;
|
||||
--overlay0-hsl: 230.769, 10.744%, 47.451%;
|
||||
--surface2: #585b70;
|
||||
--surface2-rgb: 88, 91, 112;
|
||||
--surface2-hsl: 232.500, 12.000%, 39.216%;
|
||||
--surface1: #45475a;
|
||||
--surface1-rgb: 69 71 90;
|
||||
--surface1-hsl: 234.286, 13.208%, 31.176%;
|
||||
--surface0: #313244;
|
||||
--surface0-rgb: 49 50 68;
|
||||
--surface0-hsl: 236.842, 16.239%, 22.941%;
|
||||
--base: #1e1e2e;
|
||||
--base-rgb: 30, 30, 46;
|
||||
--base-hsl: 240.000, 21.053%, 14.902%;
|
||||
--mantle: #181825;
|
||||
--mantle-rgb: 24, 24, 37;
|
||||
--mantle-hsl: 240.000, 21.311%, 11.961%;
|
||||
--crust: #11111b;
|
||||
--crust-rgb: 17, 17, 27;
|
||||
--crust-hsl: 240.000, 22.727%, 8.627%;
|
||||
}
|
18
src/css/clip-paths.css
Normal file
18
src/css/clip-paths.css
Normal file
File diff suppressed because one or more lines are too long
|
@ -1,9 +1,11 @@
|
|||
@import "@css/catppuccin.css";
|
||||
|
||||
:root {
|
||||
--background: url("/background.png");
|
||||
--text: #cdd6f4;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
* {
|
||||
font-family: Inter, sans-serif;
|
||||
font-family: BlinkMacSystemFont, Inter, sans-serif;
|
||||
}
|
||||
body {
|
||||
background: var(--background);
|
||||
|
@ -20,7 +22,12 @@ a {
|
|||
overflow: scroll;
|
||||
}
|
||||
}
|
||||
|
||||
.emoji {
|
||||
font-family: "Inter", "Noto Emoji Variable";
|
||||
}
|
||||
h1,h2,h3,h4,h5,h6,p,ul {
|
||||
margin: 0;
|
||||
}
|
||||
h4,h5,h6 {
|
||||
color: var(--overlay0)
|
||||
}
|
|
@ -1,49 +1,42 @@
|
|||
.me-container {
|
||||
display: flex;
|
||||
h3 {
|
||||
color: #89b4fa;
|
||||
font-weight: 400;
|
||||
font-size: 2rem;
|
||||
margin-left: 20px;
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
}
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
.img-container {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
clip-path: var(--clip-avatar-outer);
|
||||
img {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 6px;
|
||||
box-shadow: 10px black;
|
||||
border-style: solid;
|
||||
border-width: 2px;
|
||||
clip-path: var(--clip-avatar-inner);
|
||||
}
|
||||
}
|
||||
}
|
||||
.spotify-card-wrapper {
|
||||
background: var(--album-color);
|
||||
position: relative;
|
||||
clip-path: var(--clip-sp-1);
|
||||
.spotify-card {
|
||||
h3 {
|
||||
margin: 13px 0;
|
||||
}
|
||||
.secondary-meta {
|
||||
color: #a6adc8;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.spotify-card-background {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding: 15px;
|
||||
clip-path: var(--clip-sp-2);
|
||||
}
|
||||
}
|
||||
|
||||
.spotify-card-wrapper::before {
|
||||
content: "";
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: -1;
|
||||
border-radius: 15px;
|
||||
filter: brightness(0.3) blur(8px);
|
||||
top: 0;
|
||||
left: 0;
|
||||
clip-path: var(--clip-sp-3);
|
||||
position: absolute;
|
||||
background-image: var(--album-art);
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
position: relative;
|
||||
background-color: transparent;
|
||||
padding: 5px 15px;
|
||||
margin-top: 15px;
|
||||
border-radius: 15px;
|
||||
border: 2px solid #cdd6f4;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
filter: blur(20px) brightness(0.5);
|
||||
}
|
1
src/env.d.ts
vendored
1
src/env.d.ts
vendored
|
@ -1 +1,2 @@
|
|||
/// <reference types="astro/client" />
|
||||
/// <reference types="astro/astro-jsx" />
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
// wow i love being fake programmer
|
||||
// source: https://jams.hackclub.com/batch/webOS/part-3
|
||||
export function dragElement(element) {
|
||||
var initialX = 0;
|
||||
var initialY = 0;
|
||||
var currentX = 0;
|
||||
var currentY = 0;
|
||||
|
||||
if (document.getElementById("title-bar"))
|
||||
document.getElementById("title-bar").onmousedown = startDragging;
|
||||
else element.onmousedown = startDragging;
|
||||
|
||||
function startDragging(e) {
|
||||
e = e || window.event;
|
||||
e.preventDefault();
|
||||
initialX = e.clientX;
|
||||
initialY = e.clientY;
|
||||
document.onmouseup = stopDragging;
|
||||
document.onmousemove = dragElement;
|
||||
}
|
||||
|
||||
function dragElement(e) {
|
||||
e = e || window.event;
|
||||
e.preventDefault();
|
||||
currentX = initialX - e.clientX;
|
||||
currentY = initialY - e.clientY;
|
||||
initialX = e.clientX;
|
||||
initialY = e.clientY;
|
||||
element.style.top = element.offsetTop - currentY + "px";
|
||||
element.style.left = element.offsetLeft - currentX + "px";
|
||||
}
|
||||
|
||||
function stopDragging() {
|
||||
document.onmouseup = null;
|
||||
document.onmousemove = null;
|
||||
}
|
||||
}
|
1
src/js/lanyard/lanyard.d.ts
vendored
1
src/js/lanyard/lanyard.d.ts
vendored
|
@ -1 +0,0 @@
|
|||
declare module "@js/lanyard/lanyard";
|
|
@ -1,56 +0,0 @@
|
|||
import type {
|
||||
AnyLanyardPayload,
|
||||
LanyardInitStatePayload,
|
||||
LanyardPayload,
|
||||
LanyardPresence,
|
||||
LanyardPresenceUpdatePayload
|
||||
} from "./types";
|
||||
|
||||
function sendToSocket(socket: WebSocket, data: AnyLanyardPayload) {
|
||||
socket.send(JSON.stringify(data));
|
||||
}
|
||||
|
||||
export function initLanyard(
|
||||
updateCallback: (presence: LanyardPresence) => void
|
||||
) {
|
||||
const socket = new WebSocket("wss://api.lanyard.rest/socket");
|
||||
socket.onmessage = data => {
|
||||
const payload: LanyardPayload = JSON.parse(data.data);
|
||||
switch (payload.op) {
|
||||
case 1: {
|
||||
// Initialize
|
||||
sendToSocket(socket, {
|
||||
op: 2,
|
||||
d: {
|
||||
subscribe_to_ids: ["886685857560539176"]
|
||||
}
|
||||
});
|
||||
|
||||
sendToSocket(socket, {
|
||||
op: 3
|
||||
});
|
||||
setInterval(
|
||||
() => {
|
||||
sendToSocket(socket, {
|
||||
op: 3
|
||||
});
|
||||
},
|
||||
"heartbeat_interval" in payload.d
|
||||
? payload.d.heartbeat_interval
|
||||
: 30000
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 0: {
|
||||
const typedPayload = payload as
|
||||
| LanyardInitStatePayload
|
||||
| LanyardPresenceUpdatePayload;
|
||||
updateCallback(
|
||||
typedPayload.t === "INIT_STATE"
|
||||
? typedPayload.d["886685857560539176"]
|
||||
: typedPayload.d
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
import "../css/style.css";
|
||||
import "@css/global.css";
|
||||
import "@css/clip-paths.css";
|
||||
import "@fontsource/inter";
|
||||
import "@fontsource-variable/noto-emoji";
|
||||
|
||||
|
@ -17,12 +18,9 @@ const { tabTitle } = Astro.props;
|
|||
<body>
|
||||
<slot />
|
||||
<script>
|
||||
import { dragElement } from "@js/drag";
|
||||
|
||||
import { WindowManager } from "@models/WindowManager.ts";
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
document.querySelectorAll(".window").forEach(window => {
|
||||
dragElement(window);
|
||||
});
|
||||
document.querySelectorAll(".window").forEach(window => WindowManager.observe(window));
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
|
77
src/models/Lanyard.ts
Normal file
77
src/models/Lanyard.ts
Normal file
|
@ -0,0 +1,77 @@
|
|||
import type {
|
||||
AnyLanyardPayload,
|
||||
LanyardInitStatePayload,
|
||||
LanyardPayload,
|
||||
LanyardPresence,
|
||||
LanyardPresenceUpdatePayload
|
||||
} from "types/lanyard";
|
||||
|
||||
export class Lanyard {
|
||||
private socket: WebSocket;
|
||||
private readonly userId: string;
|
||||
private readonly updateCallback: (presence: LanyardPresence) => void;
|
||||
private heartbeatInterval?: number;
|
||||
|
||||
constructor(
|
||||
userId: string,
|
||||
updateCallback: (presence: LanyardPresence) => void
|
||||
) {
|
||||
this.userId = userId;
|
||||
this.updateCallback = updateCallback;
|
||||
this.socket = new WebSocket("wss://api.lanyard.rest/socket");
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
private send(data: AnyLanyardPayload) {
|
||||
this.socket.send(JSON.stringify(data));
|
||||
}
|
||||
|
||||
private initialize() {
|
||||
this.socket.onmessage = event => {
|
||||
const payload: LanyardPayload = JSON.parse(event.data);
|
||||
switch (payload.op) {
|
||||
case 1: {
|
||||
// Initialize
|
||||
this.send({
|
||||
op: 2,
|
||||
d: {
|
||||
subscribe_to_ids: [this.userId]
|
||||
}
|
||||
});
|
||||
|
||||
this.send({ op: 3 });
|
||||
|
||||
const interval =
|
||||
"heartbeat_interval" in payload.d
|
||||
? payload.d.heartbeat_interval
|
||||
: 30000;
|
||||
|
||||
this.heartbeatInterval = window.setInterval(() => {
|
||||
this.send({ op: 3 });
|
||||
}, interval);
|
||||
break;
|
||||
}
|
||||
case 0: {
|
||||
const typedPayload = payload as
|
||||
| LanyardInitStatePayload
|
||||
| LanyardPresenceUpdatePayload;
|
||||
|
||||
const presence =
|
||||
typedPayload.t === "INIT_STATE"
|
||||
? typedPayload.d[this.userId]
|
||||
: typedPayload.d;
|
||||
|
||||
this.updateCallback(presence);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public close() {
|
||||
if (this.heartbeatInterval) {
|
||||
clearInterval(this.heartbeatInterval);
|
||||
}
|
||||
this.socket.close();
|
||||
}
|
||||
}
|
70
src/models/WindowManager.ts
Normal file
70
src/models/WindowManager.ts
Normal file
|
@ -0,0 +1,70 @@
|
|||
export class WindowManager {
|
||||
private static _topmost: HTMLElement | null;
|
||||
|
||||
static get topmost() {
|
||||
return this._topmost;
|
||||
}
|
||||
|
||||
static set topmost(e) {
|
||||
if (this._topmost) {
|
||||
this._topmost.style.zIndex = "0";
|
||||
}
|
||||
this._topmost = e;
|
||||
this._topmost.style.display = "block";
|
||||
this._topmost.style.zIndex = "1";
|
||||
}
|
||||
|
||||
static observe(element) {
|
||||
element.addEventListener("mousedown", () => {
|
||||
this.topmost = element;
|
||||
});
|
||||
|
||||
const titleBar = element.querySelector(".title-bar");
|
||||
|
||||
titleBar.addEventListener("mousedown", startDrag);
|
||||
titleBar.addEventListener("touchstart", startDrag, { passive: false });
|
||||
|
||||
function startDrag(e) {
|
||||
if (e.target !== titleBar) return;
|
||||
|
||||
let isTouch = e.type.startsWith("touch");
|
||||
let startX = isTouch ? e.touches[0].clientX : e.clientX;
|
||||
let startY = isTouch ? e.touches[0].clientY : e.clientY;
|
||||
|
||||
let offsetX = startX - element.offsetLeft;
|
||||
let offsetY = startY - element.offsetTop;
|
||||
|
||||
function onMove(e) {
|
||||
let clientX = isTouch ? e.touches[0].clientX : e.clientX;
|
||||
let clientY = isTouch ? e.touches[0].clientY : e.clientY;
|
||||
|
||||
let newX = clientX - offsetX;
|
||||
let newY = clientY - offsetY;
|
||||
|
||||
element.style.top = `${newY}px`;
|
||||
element.style.left = `${newX}px`;
|
||||
}
|
||||
|
||||
function stopMove() {
|
||||
let maxX = window.innerWidth - element.offsetWidth;
|
||||
let maxY = window.innerHeight - element.offsetHeight;
|
||||
|
||||
let finalX = Math.min(Math.max(element.offsetLeft, 0), maxX);
|
||||
let finalY = Math.min(Math.max(element.offsetTop, 0), maxY);
|
||||
|
||||
element.style.top = `${finalY}px`;
|
||||
element.style.left = `${finalX}px`;
|
||||
|
||||
document.removeEventListener("mousemove", onMove);
|
||||
document.removeEventListener("mouseup", stopMove);
|
||||
document.removeEventListener("touchmove", onMove);
|
||||
document.removeEventListener("touchend", stopMove);
|
||||
}
|
||||
|
||||
document.addEventListener("mousemove", onMove);
|
||||
document.addEventListener("mouseup", stopMove);
|
||||
document.addEventListener("touchmove", onMove, { passive: false });
|
||||
document.addEventListener("touchend", stopMove);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,7 +2,6 @@
|
|||
import FearOfTechnology from "@windows/mainWindow/FearOfTechnology.astro";
|
||||
import MainWindow from "@windows/mainWindow/MainWindow.astro";
|
||||
import BaseLayout from "../layouts/BaseLayout.astro";
|
||||
import Window from "@components/Window.astro";
|
||||
import CodeWindow from "@windows/code/CodeWindow.astro";
|
||||
---
|
||||
|
||||
|
|
|
@ -7,8 +7,8 @@
|
|||
"@windows/*": ["components/windows/*"],
|
||||
"@layouts/*": ["layouts/*"],
|
||||
"@css/*": ["css/*"],
|
||||
"@js/*": ["js/*"],
|
||||
"@info/*": ["info/*"]
|
||||
"@models/*": ["models/*"],
|
||||
"@assets/*": ["assets/*"]
|
||||
},
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "react"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue