A decentralized music tracking and discovery platform built on AT Protocol 🎵

Refactor header, add Embed, prune CSS

+107 -122
+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 } 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; 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 } 1458 .mt-\[-3px\] { 1459 margin-top: -3px; 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 } 1473 .mr-\[8px\] { 1474 margin-right: 8px; 1475 } ··· 1485 .mb-\[5px\] { 1486 margin-bottom: 5px; 1487 } 1488 .mb-\[15px\] { 1489 margin-bottom: 15px; 1490 } ··· 1493 } 1494 .mb-\[25px\] { 1495 margin-bottom: 25px; 1496 } 1497 .status { 1498 display: inline-block; ··· 1724 .max-h-\[18px\] { 1725 max-height: 18px; 1726 } 1727 .max-h-\[25px\] { 1728 max-height: 25px; 1729 } 1730 .max-h-\[60px\] { 1731 max-height: 60px; ··· 1736 .max-h-\[100px\] { 1737 max-height: 100px; 1738 } 1739 .max-h-\[280px\] { 1740 max-height: 280px; 1741 } ··· 1770 outline-style: none; 1771 } 1772 } 1773 .w-1\/3 { 1774 width: calc(1/3 * 100%); 1775 } ··· 1788 .max-w-\[18px\] { 1789 max-width: 18px; 1790 } 1791 .max-w-\[25px\] { 1792 max-width: 25px; 1793 } 1794 .max-w-\[60px\] { 1795 max-width: 60px; ··· 1800 .max-w-\[100px\] { 1801 max-width: 100px; 1802 } 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; 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 } 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> 27 <a 28 - href={`https://rocksky.app/profile/${props.profile.handle}`} 29 target="_blank" 30 - className="no-underline text-inherit" 31 > 32 - <div className="text-[14px] mt-[-6px]">@{props.profile.handle}</div> 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 );
··· 1 import type { Profile } from "../../types/profile"; 2 3 export type HeaderProps = { 4 profile: Profile; 5 }; 6 7 function Header(props: HeaderProps) { 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" 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> 50 </div> 51 </div> 52 </> 53 );
+42
apps/embed/src/embeds/Embed.tsx
···
··· 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"; 5 6 export type TopAlbumEmbedPageProps = { 7 profile: Profile; ··· 9 }; 10 11 export function TopAlbumsEmbedPage(props: TopAlbumEmbedPageProps) { 12 return ( 13 <div className="p-[15px]"> 14 <Header profile={props.profile} /> 15 <h2 className="m-[0px]">Top Albums</h2> 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) { 14 return ( 15 <div className="p-[15px]"> 16 <Header profile={props.profile} /> 17 - <h2 className="m-[0px] mb-[15px]">Top Artists</h2> 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"; 5 6 export type TopTracksEmbedPageProps = { 7 profile: Profile; ··· 9 }; 10 11 export function TopTracksEmbedPage(props: TopTracksEmbedPageProps) { 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"; 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 />); 159 }); 160 161 console.log(