tangled
alpha
login
or
join now
t1c.dev
/
rocksky
forked from
rocksky.app/rocksky
2
fork
atom
A decentralized music tracking and discovery platform built on AT Protocol 🎵
2
fork
atom
overview
issues
pulls
pipelines
Refactor header, add Embed, prune CSS
tsiry-sandratraina.com
1 month ago
a1c222ed
39cc7bbf
+107
-122
8 changed files
expand all
collapse all
unified
split
apps
embed
public
styles.css
src
components
Header
Header.tsx
embeds
Embed.tsx
RecentScrobblesEmbedPage.tsx
TopAlbumsEmbedPage.tsx
TopArtistsEmbedPage.tsx
TopTracksEmbedPage.tsx
index.tsx
+9
-63
apps/embed/public/styles.css
···
1455
outline: none;
1456
}
1457
}
1458
-
.mt-\[-2px\] {
1459
-
margin-top: -2px;
1460
-
}
1461
.mt-\[-3px\] {
1462
margin-top: -3px;
1463
-
}
1464
-
.mt-\[-5px\] {
1465
-
margin-top: -5px;
1466
}
1467
.mt-\[-6px\] {
1468
margin-top: -6px;
1469
}
1470
-
.mt-\[5px\] {
1471
-
margin-top: 5px;
1472
}
1473
.mt-\[10px\] {
1474
margin-top: 10px;
···
1476
.mt-\[20px\] {
1477
margin-top: 20px;
1478
}
1479
-
.mr-\[5px\] {
1480
-
margin-right: 5px;
1481
-
}
1482
.mr-\[8px\] {
1483
margin-right: 8px;
1484
}
···
1494
.mb-\[5px\] {
1495
margin-bottom: 5px;
1496
}
1497
-
.mb-\[10px\] {
1498
-
margin-bottom: 10px;
1499
-
}
1500
.mb-\[15px\] {
1501
margin-bottom: 15px;
1502
}
···
1505
}
1506
.mb-\[25px\] {
1507
margin-bottom: 25px;
1508
-
}
1509
-
.ml-\[5px\] {
1510
-
margin-left: 5px;
1511
-
}
1512
-
.ml-\[10px\] {
1513
-
margin-left: 10px;
1514
}
1515
.status {
1516
display: inline-block;
···
1742
.max-h-\[18px\] {
1743
max-height: 18px;
1744
}
1745
-
.max-h-\[20-px\] {
1746
-
max-height: 20-px;
1747
-
}
1748
-
.max-h-\[20px\] {
1749
-
max-height: 20px;
1750
-
}
1751
-
.max-h-\[22px\] {
1752
-
max-height: 22px;
1753
-
}
1754
.max-h-\[25px\] {
1755
max-height: 25px;
1756
-
}
1757
-
.max-h-\[30px\] {
1758
-
max-height: 30px;
1759
}
1760
.max-h-\[60px\] {
1761
max-height: 60px;
···
1766
.max-h-\[100px\] {
1767
max-height: 100px;
1768
}
1769
-
.max-h-\[200px\] {
1770
-
max-height: 200px;
1771
-
}
1772
-
.max-h-\[250px\] {
1773
-
max-height: 250px;
1774
-
}
1775
.max-h-\[280px\] {
1776
max-height: 280px;
1777
}
···
1806
outline-style: none;
1807
}
1808
}
1809
-
.w-1\/2 {
1810
-
width: calc(1/2 * 100%);
1811
-
}
1812
.w-1\/3 {
1813
width: calc(1/3 * 100%);
1814
}
···
1827
.max-w-\[18px\] {
1828
max-width: 18px;
1829
}
1830
-
.max-w-\[20px\] {
1831
-
max-width: 20px;
1832
-
}
1833
-
.max-w-\[22px\] {
1834
-
max-width: 22px;
1835
-
}
1836
.max-w-\[25px\] {
1837
max-width: 25px;
1838
-
}
1839
-
.max-w-\[30px\] {
1840
-
max-width: 30px;
1841
}
1842
.max-w-\[60px\] {
1843
max-width: 60px;
···
1848
.max-w-\[100px\] {
1849
max-width: 100px;
1850
}
1851
-
.max-w-\[200px\] {
1852
-
max-width: 200px;
1853
-
}
1854
-
.max-w-\[250px\] {
1855
-
max-width: 250px;
1856
-
}
1857
.max-w-\[280px\] {
1858
max-width: 280px;
1859
}
···
1893
.items-end {
1894
align-items: flex-end;
1895
}
0
0
0
1896
.\!justify-between {
1897
justify-content: space-between !important;
1898
}
···
1956
}
1957
.rounded-\[5px\] {
1958
border-radius: 5px;
1959
-
}
1960
-
.rounded-\[8px\] {
1961
-
border-radius: 8px;
1962
}
1963
.rounded-\[10px\] {
1964
border-radius: 10px;
···
2054
.\!border-none {
2055
--tw-border-style: none !important;
2056
border-style: none !important;
0
0
0
0
2057
}
2058
.input {
2059
&.is-valid, &:has(.is-valid), .validate &:valid, .validate &:has(:valid) {
···
2406
@supports (color: color-mix(in lab, red, red)) {
2407
background-color: color-mix(in oklab, var(--color-base-300) 60%, transparent);
2408
}
2409
-
}
2410
-
.bg-gradient-to-br {
2411
-
--tw-gradient-position: to bottom right in oklab;
2412
-
background-image: linear-gradient(var(--tw-gradient-stops));
2413
}
2414
.loading-spinner {
2415
mask-image: url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='%23000' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cstyle%3E.spinner_V8m1%7Btransform-origin:center;animation:spinner_zKoa 2s linear infinite%7D.spinner_V8m1 circle%7Bstroke-linecap:round;animation:spinner_YpZS 1.5s ease-out infinite%7D%40keyframes spinner_zKoa%7B100%25%7Btransform:rotate(360deg)%7D%7D%40keyframes spinner_YpZS%7B0%25%7Bstroke-dasharray:0 150;stroke-dashoffset:0%7D47.5%25%7Bstroke-dasharray:42 150;stroke-dashoffset:-16%7D95%25%2C100%25%7Bstroke-dasharray:42 150;stroke-dashoffset:-59%7D%7D%3C%2Fstyle%3E%3Cg class='spinner_V8m1'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3'%3E%3C%2Fcircle%3E%3C%2Fg%3E%3C%2Fsvg%3E");
···
1455
outline: none;
1456
}
1457
}
0
0
0
1458
.mt-\[-3px\] {
1459
margin-top: -3px;
0
0
0
1460
}
1461
.mt-\[-6px\] {
1462
margin-top: -6px;
1463
}
1464
+
.mt-\[3px\] {
1465
+
margin-top: 3px;
1466
}
1467
.mt-\[10px\] {
1468
margin-top: 10px;
···
1470
.mt-\[20px\] {
1471
margin-top: 20px;
1472
}
0
0
0
1473
.mr-\[8px\] {
1474
margin-right: 8px;
1475
}
···
1485
.mb-\[5px\] {
1486
margin-bottom: 5px;
1487
}
0
0
0
1488
.mb-\[15px\] {
1489
margin-bottom: 15px;
1490
}
···
1493
}
1494
.mb-\[25px\] {
1495
margin-bottom: 25px;
0
0
0
0
0
0
1496
}
1497
.status {
1498
display: inline-block;
···
1724
.max-h-\[18px\] {
1725
max-height: 18px;
1726
}
0
0
0
0
0
0
0
0
0
1727
.max-h-\[25px\] {
1728
max-height: 25px;
0
0
0
1729
}
1730
.max-h-\[60px\] {
1731
max-height: 60px;
···
1736
.max-h-\[100px\] {
1737
max-height: 100px;
1738
}
0
0
0
0
0
0
1739
.max-h-\[280px\] {
1740
max-height: 280px;
1741
}
···
1770
outline-style: none;
1771
}
1772
}
0
0
0
1773
.w-1\/3 {
1774
width: calc(1/3 * 100%);
1775
}
···
1788
.max-w-\[18px\] {
1789
max-width: 18px;
1790
}
0
0
0
0
0
0
1791
.max-w-\[25px\] {
1792
max-width: 25px;
0
0
0
1793
}
1794
.max-w-\[60px\] {
1795
max-width: 60px;
···
1800
.max-w-\[100px\] {
1801
max-width: 100px;
1802
}
0
0
0
0
0
0
1803
.max-w-\[280px\] {
1804
max-width: 280px;
1805
}
···
1839
.items-end {
1840
align-items: flex-end;
1841
}
1842
+
.items-start {
1843
+
align-items: flex-start;
1844
+
}
1845
.\!justify-between {
1846
justify-content: space-between !important;
1847
}
···
1905
}
1906
.rounded-\[5px\] {
1907
border-radius: 5px;
0
0
0
1908
}
1909
.rounded-\[10px\] {
1910
border-radius: 10px;
···
2000
.\!border-none {
2001
--tw-border-style: none !important;
2002
border-style: none !important;
2003
+
}
2004
+
.border-none {
2005
+
--tw-border-style: none;
2006
+
border-style: none;
2007
}
2008
.input {
2009
&.is-valid, &:has(.is-valid), .validate &:valid, .validate &:has(:valid) {
···
2356
@supports (color: color-mix(in lab, red, red)) {
2357
background-color: color-mix(in oklab, var(--color-base-300) 60%, transparent);
2358
}
0
0
0
0
2359
}
2360
.loading-spinner {
2361
mask-image: url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='%23000' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cstyle%3E.spinner_V8m1%7Btransform-origin:center;animation:spinner_zKoa 2s linear infinite%7D.spinner_V8m1 circle%7Bstroke-linecap:round;animation:spinner_YpZS 1.5s ease-out infinite%7D%40keyframes spinner_zKoa%7B100%25%7Btransform:rotate(360deg)%7D%7D%40keyframes spinner_YpZS%7B0%25%7Bstroke-dasharray:0 150;stroke-dashoffset:0%7D47.5%25%7Bstroke-dasharray:42 150;stroke-dashoffset:-16%7D95%25%2C100%25%7Bstroke-dasharray:42 150;stroke-dashoffset:-59%7D%7D%3C%2Fstyle%3E%3Cg class='spinner_V8m1'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3'%3E%3C%2Fcircle%3E%3C%2Fg%3E%3C%2Fsvg%3E");
+35
-39
apps/embed/src/components/Header/Header.tsx
···
1
-
import dayjs from "dayjs";
2
import type { Profile } from "../../types/profile";
3
4
export type HeaderProps = {
5
profile: Profile;
6
-
withoutRange?: boolean;
7
};
8
9
function Header(props: HeaderProps) {
10
-
const end = dayjs();
11
-
const start = end.subtract(7, "day");
12
-
const range = `${start.format("DD MMM YYYY")} — ${end.format("DD MMM YYYY")}`;
13
-
14
return (
15
<>
16
-
<div className="flex flex-row items-center mb-[20px]">
17
<div className="flex flex-1 items-center">
18
-
<a
19
-
href={`https://rocksky.app/profile/${props.profile.handle}`}
20
-
target="_blank"
21
-
>
22
-
<img
23
-
className="max-h-[25px] max-w-[25px] rounded-full mr-[10px]"
24
-
src={props.profile.avatar}
25
-
/>
26
-
</a>
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
27
<a
28
-
href={`https://rocksky.app/profile/${props.profile.handle}`}
0
29
target="_blank"
30
-
className="no-underline text-inherit"
31
>
32
-
<div className="text-[14px] mt-[-6px]">@{props.profile.handle}</div>
0
0
0
0
0
0
33
</a>
34
-
{!props.withoutRange && (
35
-
<>
36
-
<span className="text-[14px] mt-[-3px] ml-[5px] mr-[5px]">|</span>
37
-
<span className="text-[13px] mt-[-3px]">{range}</span>
38
-
</>
39
-
)}
40
</div>
41
-
42
-
<a
43
-
href="https://rocksky.app"
44
-
className="text-inherit no-underline"
45
-
target="_blank"
46
-
>
47
-
<div className="flex flex-row items-center ">
48
-
<img
49
-
className="max-h-[18px] max-w-[18px] mr-[8px] "
50
-
src="/public/logo.png"
51
-
/>
52
-
<span className="text-[15px]">Rocksky</span>
53
-
</div>
54
-
</a>
55
</div>
56
</>
57
);
···
0
1
import type { Profile } from "../../types/profile";
2
3
export type HeaderProps = {
4
profile: Profile;
0
5
};
6
7
function Header(props: HeaderProps) {
0
0
0
0
8
return (
9
<>
10
+
<div className="flex flex-row mb-[20px]">
11
<div className="flex flex-1 items-center">
12
+
<div>
13
+
<div className="flex items-center">
14
+
<a
15
+
href={`https://rocksky.app/profile/${props.profile.handle}`}
16
+
target="_blank"
17
+
>
18
+
<img
19
+
className="max-h-[25px] max-w-[25px] rounded-full mr-[10px]"
20
+
src={props.profile.avatar}
21
+
/>
22
+
</a>
23
+
<a
24
+
href={`https://rocksky.app/profile/${props.profile.handle}`}
25
+
target="_blank"
26
+
className="no-underline text-inherit"
27
+
>
28
+
<div className="text-[14px] mt-[-6px]">
29
+
@{props.profile.handle}
30
+
</div>
31
+
</a>
32
+
</div>
33
+
</div>
34
+
</div>
35
+
36
+
<div>
37
<a
38
+
href="https://rocksky.app"
39
+
className="text-inherit no-underline"
40
target="_blank"
0
41
>
42
+
<div className="flex flex-row items-start ">
43
+
<img
44
+
className="max-h-[18px] max-w-[18px] mr-[8px] "
45
+
src="/public/logo.png"
46
+
/>
47
+
<span className="text-[15px]">Rocksky</span>
48
+
</div>
49
</a>
0
0
0
0
0
0
50
</div>
0
0
0
0
0
0
0
0
0
0
0
0
0
0
51
</div>
52
</>
53
);
+42
apps/embed/src/embeds/Embed.tsx
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
import { useState } from "react";
2
+
3
+
export function Embed() {
4
+
const [url, setUrl] = useState<string>(""); // Initialize with an empty string
5
+
6
+
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
7
+
const value = (e.target as any).value;
8
+
console.log("Input changed:", value); // Debugging log
9
+
setUrl(value);
10
+
11
+
if (
12
+
value.match(
13
+
/^https?:\/\/rocksky\.app\/did:plc:[a-z2-7]{24}\/scrobble\/[a-z0-9]{13,}$/,
14
+
)
15
+
) {
16
+
alert("ok");
17
+
}
18
+
alert("z");
19
+
};
20
+
21
+
return (
22
+
<div className="min-h-screen w-1/3 flex items-center justify-center m-auto">
23
+
<div className="w-full">
24
+
<h1 className="text-4xl font-bold text-gray-800 mb-4 text-center">
25
+
Embed a Rocksky Scrobble
26
+
</h1>
27
+
{/*<iframe
28
+
src="https://api.rocksky.app/embed/did:plc:7vdlgi2bflelz7mmuxoqjfcr/scrobble/3mdtacalsqs23"
29
+
height={500}
30
+
width={500}
31
+
className="mt-[20px] border-none rounded-[10px]"
32
+
/>*/}
33
+
<iframe
34
+
src="https://api.rocksky.app/embed/u/tsiry-sandratraina.com/top/artists"
35
+
height={500}
36
+
width={600}
37
+
className="mt-[20px] border-none rounded-[10px]"
38
+
/>
39
+
</div>
40
+
</div>
41
+
);
42
+
}
+1
-1
apps/embed/src/embeds/RecentScrobblesEmbedPage.tsx
···
15
export function RecentScrobblesEmbedPage(props: RecentScrobblesEmbedPageProps) {
16
return (
17
<div className="p-[15px]">
18
-
<Header profile={props.profile} withoutRange />
19
<h2 className="m-[0px]">Recent Listens</h2>
20
21
<div className="w-full overflow-x-auto">
···
15
export function RecentScrobblesEmbedPage(props: RecentScrobblesEmbedPageProps) {
16
return (
17
<div className="p-[15px]">
18
+
<Header profile={props.profile} />
19
<h2 className="m-[0px]">Recent Listens</h2>
20
21
<div className="w-full overflow-x-auto">
+6
apps/embed/src/embeds/TopAlbumsEmbedPage.tsx
···
2
import Header from "../components/Header";
3
import type { Album } from "../types/album";
4
import type { Profile } from "../types/profile";
0
5
6
export type TopAlbumEmbedPageProps = {
7
profile: Profile;
···
9
};
10
11
export function TopAlbumsEmbedPage(props: TopAlbumEmbedPageProps) {
0
0
0
0
12
return (
13
<div className="p-[15px]">
14
<Header profile={props.profile} />
15
<h2 className="m-[0px]">Top Albums</h2>
0
16
17
<div className="w-full overflow-x-auto">
18
<table className="table-borderless table">
···
2
import Header from "../components/Header";
3
import type { Album } from "../types/album";
4
import type { Profile } from "../types/profile";
5
+
import dayjs from "dayjs";
6
7
export type TopAlbumEmbedPageProps = {
8
profile: Profile;
···
10
};
11
12
export function TopAlbumsEmbedPage(props: TopAlbumEmbedPageProps) {
13
+
const end = dayjs();
14
+
const start = end.subtract(7, "day");
15
+
const range = `${start.format("DD MMM YYYY")} — ${end.format("DD MMM YYYY")}`;
16
+
17
return (
18
<div className="p-[15px]">
19
<Header profile={props.profile} />
20
<h2 className="m-[0px]">Top Albums</h2>
21
+
<div className="text-[14px] mt-[3px] mb-[20px]">{range}</div>
22
23
<div className="w-full overflow-x-auto">
24
<table className="table-borderless table">
+6
-1
apps/embed/src/embeds/TopArtistsEmbedPage.tsx
···
11
};
12
13
export function TopArtistsEmbedPage(props: TopArtistsEmbedPageProps) {
0
0
0
0
14
return (
15
<div className="p-[15px]">
16
<Header profile={props.profile} />
17
-
<h2 className="m-[0px] mb-[15px]">Top Artists</h2>
0
18
19
<div className="w-full overflow-x-auto">
20
<table className="table-borderless table">
···
11
};
12
13
export function TopArtistsEmbedPage(props: TopArtistsEmbedPageProps) {
14
+
const end = dayjs();
15
+
const start = end.subtract(7, "day");
16
+
const range = `${start.format("DD MMM YYYY")} — ${end.format("DD MMM YYYY")}`;
17
+
18
return (
19
<div className="p-[15px]">
20
<Header profile={props.profile} />
21
+
<h2 className="m-[0px]">Top Artists</h2>
22
+
<div className="text-[14px] mt-[3px] mb-[20px]">{range}</div>
23
24
<div className="w-full overflow-x-auto">
25
<table className="table-borderless table">
+6
-1
apps/embed/src/embeds/TopTracksEmbedPage.tsx
···
2
import Header from "../components/Header";
3
import type { Profile } from "../types/profile";
4
import type { Track } from "../types/track";
0
5
6
export type TopTracksEmbedPageProps = {
7
profile: Profile;
···
9
};
10
11
export function TopTracksEmbedPage(props: TopTracksEmbedPageProps) {
0
0
0
0
12
return (
13
<div className="p-[15px]">
14
<Header profile={props.profile} />
15
<h2 className="m-[0px]">Top Tracks</h2>
16
-
17
<div className="w-full overflow-x-auto">
18
<table className="table-borderless table">
19
<tbody>
···
2
import Header from "../components/Header";
3
import type { Profile } from "../types/profile";
4
import type { Track } from "../types/track";
5
+
import dayjs from "dayjs";
6
7
export type TopTracksEmbedPageProps = {
8
profile: Profile;
···
10
};
11
12
export function TopTracksEmbedPage(props: TopTracksEmbedPageProps) {
13
+
const end = dayjs();
14
+
const start = end.subtract(7, "day");
15
+
const range = `${start.format("DD MMM YYYY")} — ${end.format("DD MMM YYYY")}`;
16
+
17
return (
18
<div className="p-[15px]">
19
<Header profile={props.profile} />
20
<h2 className="m-[0px]">Top Tracks</h2>
21
+
<div className="text-[14px] mt-[3px] mb-[20px]">{range}</div>
22
<div className="w-full overflow-x-auto">
23
<table className="table-borderless table">
24
<tbody>
+2
-17
apps/embed/src/index.tsx
···
23
import { logger } from "hono/logger";
24
import { ScrobbleEmbedPage } from "./embeds/ScrobbleEmbedPage";
25
import getScrobble from "./xrpc/getScrobble";
0
26
27
const app = new Hono();
28
···
154
});
155
156
app.get("/", (c) => {
157
-
return c.render(
158
-
<div className="min-h-screen w-1/3 flex items-center justify-center m-auto">
159
-
<div className="w-full">
160
-
<h1 className="text-4xl font-bold text-gray-800 mb-4 text-center">
161
-
Embed a Rocksky Scrobble
162
-
</h1>
163
-
<div>
164
-
<input
165
-
type="text"
166
-
className="input w-full"
167
-
aria-label="input"
168
-
placeholder="https://rocksky.app/did:plc:7vdlgi2bflelz7mmuxoqjfcr/scrobble/3mdt3zncfoc23"
169
-
/>
170
-
</div>
171
-
</div>
172
-
</div>,
173
-
);
174
});
175
176
console.log(
···
23
import { logger } from "hono/logger";
24
import { ScrobbleEmbedPage } from "./embeds/ScrobbleEmbedPage";
25
import getScrobble from "./xrpc/getScrobble";
26
+
import { Embed } from "./embeds/Embed";
27
28
const app = new Hono();
29
···
155
});
156
157
app.get("/", (c) => {
158
+
return c.render(<Embed />);
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
159
});
160
161
console.log(