Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions surfaces/webview/src/market/MarketScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export function MarketScreen() {
detail={state.marketDetail}
owned={state.marketOwned.includes(state.marketDetail.card.name)}
onBack={() => { send({ type: "ownedSkills" }); clearMarketDetail(); }}
onOpenSkill={(card) => send({ type: "getSkillDetail", mint: card.id })}
/>
{state.buyCelebrate && <BuyCelebration />}
</div>
Expand Down Expand Up @@ -195,6 +196,7 @@ export function MarketScreen() {
key={card.id}
card={card}
owned={state.marketOwned.includes(card.name)}
disposed={Object.values(state.marketDisposed).includes(card.id)}
firing={state.firingSkill === card.name}
onOpen={handleOpenCard}
/>
Expand Down
9 changes: 8 additions & 1 deletion surfaces/webview/src/market/SkillCardTile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import { SkillIcon } from "../icons";
interface Props {
card: SkillCard;
owned?: boolean;
disposed?: boolean;
firing?: boolean;
onOpen: (card: SkillCard) => void;
}

export function SkillCardTile({ card, owned, firing, onOpen }: Props) {
export function SkillCardTile({ card, owned, disposed, firing, onOpen }: Props) {
const priceSol = card.price ? (Number(card.price) / 1_000_000_000).toFixed(3) : null;
return (
<button
Expand All @@ -18,6 +19,7 @@ export function SkillCardTile({ card, owned, firing, onOpen }: Props) {
"bg-zinc-900 border-zinc-800 hover:border-zinc-600 active:scale-[0.98]",
firing ? "skill-firing border-green-500/60" : "",
owned ? "border-l-2 border-l-green-500" : "",
disposed ? "opacity-55 grayscale border-dashed border-zinc-700" : "",
].join(" ")}
>
<div className="flex items-start gap-2">
Expand All @@ -36,6 +38,11 @@ export function SkillCardTile({ card, owned, firing, onOpen }: Props) {
owned
</span>
)}
{disposed && (
<span className="shrink-0 rounded px-1 py-0.5 text-[10px] font-semibold bg-zinc-800 text-zinc-500">
un-equipped
</span>
)}
{firing && (
<span className="shrink-0 rounded px-1 py-0.5 text-[10px] font-semibold bg-green-500/20 text-green-300 animate-pulse">
casting
Expand Down
56 changes: 52 additions & 4 deletions surfaces/webview/src/market/SkillDetailView.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
import { useState } from "react";
import { useStore } from "../state/store";
import type { SkillDetail } from "../transport/protocol";
import type { SkillCard, SkillDetail } from "../transport/protocol";
import { SkillIcon } from "../icons";

interface Props {
detail: SkillDetail;
owned: boolean;
onBack: () => void;
onOpenSkill?: (card: SkillCard) => void;
}

export function SkillDetailView({ detail, owned, onBack }: Props) {
const { send } = useStore();
export function SkillDetailView({ detail, owned, onBack, onOpenSkill }: Props) {
const { state, send } = useStore();
const [buying, setBuying] = useState(false);
const [noteText, setNoteText] = useState("");
const [noteGitLink, setNoteGitLink] = useState("");
const { card, skillText, notes } = detail;
const priceSol = card.price ? (Number(card.price) / 1_000_000_000).toFixed(3) : null;
const disposed = Object.values(state.marketDisposed).includes(card.id);

function handleBuy() {
setBuying(true);
Expand All @@ -40,6 +42,11 @@ export function SkillDetailView({ detail, owned, onBack }: Props) {
owned
</span>
)}
{disposed && (
<span className="ml-auto shrink-0 rounded px-1.5 py-0.5 text-[10px] font-semibold bg-zinc-800 text-zinc-500">
un-equipped
</span>
)}
</header>

<div className="flex-1 overflow-y-auto p-3 space-y-4">
Expand Down Expand Up @@ -68,6 +75,25 @@ export function SkillDetailView({ detail, owned, onBack }: Props) {
</div>
)}

{Array.isArray(detail.requiredCards) && detail.requiredCards.length > 0 && (
<div className="space-y-2">
<p className="text-[11px] text-zinc-500 uppercase tracking-wide">Required skills</p>
<div className="space-y-2">
{detail.requiredCards.map((req) => (
<button
key={req.id}
type="button"
onClick={() => onOpenSkill?.(req)}
className="w-full rounded-lg bg-zinc-900 border border-zinc-800 p-2.5 text-left active:bg-zinc-800"
>
<p className="text-xs font-medium text-zinc-200">{req.name}</p>
<p className="mt-0.5 line-clamp-2 text-[11px] text-zinc-500">{req.description}</p>
</button>
))}
</div>
</div>
)}

{Array.isArray(notes) && notes.length > 0 && (
<div className="space-y-2">
<p className="text-[11px] text-zinc-500 uppercase tracking-wide">Comments</p>
Expand Down Expand Up @@ -107,7 +133,29 @@ export function SkillDetailView({ detail, owned, onBack }: Props) {
)}
</div>

{!owned && (
{owned && (
<div className="shrink-0 border-t border-red-900/40 bg-gradient-to-t from-red-950/30 to-transparent p-3 pb-[max(0.75rem,env(safe-area-inset-bottom))]">
<button
onClick={() => send({ type: "disposeSkill", skillId: card.id })}
className="w-full rounded-xl border border-red-500/30 bg-red-950/20 py-3 text-sm font-semibold text-red-400 active:bg-red-900/30"
>
Remove Skill
</button>
</div>
)}

{!owned && disposed && (
<div className="shrink-0 border-t border-green-800/40 bg-gradient-to-t from-green-900/30 to-transparent p-3 pb-[max(0.75rem,env(safe-area-inset-bottom))]">
<button
onClick={() => send({ type: "reEquipSkill", skillId: card.id })}
className="w-full rounded-xl bg-green-600 py-3 text-sm font-semibold text-white active:bg-green-500"
>
Re-equip Skill
</button>
</div>
)}

{!owned && !disposed && (
<div className="shrink-0 border-t border-amber-700/40 bg-gradient-to-t from-amber-900/30 to-transparent p-3 pb-[max(0.75rem,env(safe-area-inset-bottom))]">
<button
onClick={handleBuy}
Expand Down
20 changes: 19 additions & 1 deletion surfaces/webview/src/state/store.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export interface State {
marketSearchError: string | null;
marketDetail: SkillDetail | null;
marketOwned: string[];
marketDisposed: Record<string, string>;
marketBalance: number | null;
rpcStatus: RpcStatus | null;
publishResult: { ok: boolean; mint?: string; error?: string } | null;
Expand Down Expand Up @@ -125,6 +126,7 @@ const initialState: State = {
marketSearchError: null,
marketDetail: null,
marketOwned: [],
marketDisposed: {},
marketBalance: null,
rpcStatus: null,
publishResult: null,
Expand Down Expand Up @@ -358,8 +360,24 @@ function reducer(state: State, ev: Action): State {
marketOwned: ev.ok ? [...state.marketOwned, ev.slug ?? ev.skillId] : state.marketOwned,
buyCelebrate: ev.ok ? true : state.buyCelebrate,
};
case "disposeResult":
return {
...state,
toast: ev.ok ? "Skill removed." : `Remove failed: ${ev.error ?? "unknown"}`,
marketOwned: ev.ok ? state.marketOwned.filter((name) => name !== (ev.slug ?? ev.skillId)) : state.marketOwned,
marketDisposed: ev.ok ? { ...state.marketDisposed, [ev.slug ?? ev.skillId]: ev.skillId } : state.marketDisposed,
};
case "reEquipResult":
return {
...state,
toast: ev.ok ? "Skill re-equipped." : `Re-equip failed: ${ev.error ?? "unknown"}`,
marketOwned: ev.ok ? [...state.marketOwned, ev.slug ?? ev.skillId] : state.marketOwned,
marketDisposed: ev.ok
? Object.fromEntries(Object.entries(state.marketDisposed).filter(([, mint]) => mint !== ev.skillId && mint !== ev.slug))
: state.marketDisposed,
};
case "ownedSkills":
return { ...state, marketOwned: ev.names };
return { ...state, marketOwned: ev.names, marketDisposed: ev.disposedMints ?? {} };
case "balance":
return { ...state, marketBalance: ev.lamports };
case "rpcStatus":
Expand Down
6 changes: 5 additions & 1 deletion surfaces/webview/src/transport/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ export type ClientMessage =
| { type: "searchSkills"; query: string; kind?: "skill" | "workflow" }
| { type: "getSkillDetail"; mint: string }
| { type: "buySkill"; skillId: string; creatorWallet?: string }
| { type: "disposeSkill"; skillId: string }
| { type: "reEquipSkill"; skillId: string }
| { type: "ownedSkills" }
| { type: "getBalance" }
| { type: "getRpcStatus" }
Expand Down Expand Up @@ -186,7 +188,9 @@ export type ServerMessage =
| { type: "searchError"; message: string }
| { type: "skillDetail"; detail: import("@iqlabs-official/agent-sdk").SkillDetail }
| { type: "buyResult"; skillId: string; ok: boolean; slug?: string; error?: string }
| { type: "ownedSkills"; names: string[]; mints?: Record<string, string> }
| { type: "disposeResult"; skillId: string; ok: boolean; slug?: string; error?: string }
| { type: "reEquipResult"; skillId: string; ok: boolean; slug?: string; error?: string }
| { type: "ownedSkills"; names: string[]; mints?: Record<string, string>; disposedMints?: Record<string, string> }
| { type: "balance"; lamports: number | null }
| { type: "skillActive"; name: string }
| { type: "rpcStatus"; status: import("@iqlabs-official/agent-sdk").RpcStatus }
Expand Down