swadhinbiswas feat: optimize Docker CI/CD builds, refine site-wide UI aesthetic, and remediate secrets · 8/19/2026
c3e5c18
180 lines · 8.0 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
---
import BaseLayout from "@/layouts/BaseLayout.astro";
import { getDatabase, schema } from "@/db";
import { eq } from "drizzle-orm";
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
import { Building2, Plus, Users, Shield } from "lucide-react";

const db = getDatabase() as NodePgDatabase<typeof schema>;
const user = Astro.locals.user;

if (!user) return Astro.redirect("/login");

const memberships = await db.query.organizationMembers.findMany({
  where: eq(schema.organizationMembers.userId, user.id),
  with: { organization: true },
  orderBy: (m, { desc }) => [desc(m.createdAt)],
});
---

<BaseLayout title="Organizations · OpenCodeHub">
  <div class="container max-w-[1280px] mx-auto py-8 px-4 sm:px-6">
    <div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 pb-6 border-b border-border mb-6">
      <div>
        <h1 class="text-2xl font-bold text-foreground tracking-tight flex items-center gap-2.5">
          <Building2 className="h-6 w-6 text-foreground" />
          <span>Organizations</span>
          <span class="rounded-full bg-secondary px-2.5 py-0.5 text-xs font-semibold text-muted-foreground border border-border/60">
            {memberships.length}
          </span>
        </h1>
        <p class="text-sm text-muted-foreground mt-1">
          Manage your enterprise teams, members, and shared repositories
        </p>
      </div>
      <button
        id="new-org-btn"
        class="inline-flex items-center gap-2 px-3.5 py-1.5 rounded-md bg-[#238636] hover:bg-[#2ea043] text-white text-xs sm:text-sm font-medium transition-colors shadow-sm self-start sm:self-auto"
      >
        <Plus className="h-4 w-4" />
        <span>New organization</span>
      </button>
    </div>

    <!-- Create form (hidden by default) -->
    <div id="new-org-form" class="hidden rounded-xl border border-border/80 bg-card/60 backdrop-blur-sm p-6 mb-6 space-y-4 shadow-sm">
      <h2 class="text-base font-semibold text-foreground">Create a new organization</h2>
      <div class="space-y-3 max-w-lg">
        <div>
          <label class="block text-xs font-medium text-muted-foreground mb-1">Organization handle</label>
          <input
            id="org-name"
            placeholder="e.g. acme-corp"
            class="h-9 w-full rounded-md border border-border/70 bg-background/80 px-3 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/20"
          />
        </div>
        <div>
          <label class="block text-xs font-medium text-muted-foreground mb-1">Display name</label>
          <input
            id="org-display"
            placeholder="e.g. Acme Corporation"
            class="h-9 w-full rounded-md border border-border/70 bg-background/80 px-3 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/20"
          />
        </div>
        <div>
          <label class="block text-xs font-medium text-muted-foreground mb-1">Description</label>
          <textarea
            id="org-desc"
            placeholder="Brief description of your organization"
            rows="2"
            class="w-full rounded-md border border-border/70 bg-background/80 p-3 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/20"
          ></textarea>
        </div>
      </div>
      <div class="flex gap-2 pt-2">
        <button
          id="org-create-submit"
          class="inline-flex items-center gap-1.5 px-4 py-2 rounded-md bg-[#238636] hover:bg-[#2ea043] text-white text-xs sm:text-sm font-medium transition-colors shadow-sm"
        >
          Create organization
        </button>
        <button
          id="org-create-cancel"
          class="inline-flex items-center px-4 py-2 rounded-md border border-border bg-card text-xs sm:text-sm font-medium text-foreground hover:bg-accent transition-colors shadow-sm"
        >
          Cancel
        </button>
      </div>
      <div id="org-create-error" class="text-xs font-medium text-red-500 hidden mt-2"></div>
    </div>

    <!-- Organizations List Card -->
    <div class="rounded-xl border border-border/80 bg-card/60 backdrop-blur-sm overflow-hidden shadow-sm">
      {memberships.length === 0 ? (
        <div class="text-center py-16 px-4">
          <div class="w-12 h-12 rounded-full bg-secondary/80 border border-border flex items-center justify-center mx-auto mb-3 text-muted-foreground">
            <Building2 className="h-6 w-6" />
          </div>
          <h3 class="text-base font-semibold text-foreground mb-1">No organizations</h3>
          <p class="text-sm text-muted-foreground max-w-sm mx-auto mb-4">
            You are not a member of any organization yet.
          </p>
        </div>
      ) : (
        <div class="divide-y divide-border/60">
          {memberships.map((m) => (
            <a
              href={`/orgs/${m.organization.name}`}
              class="group flex items-center justify-between gap-4 p-5 hover:bg-accent/40 transition-colors"
            >
              <div class="flex items-center gap-4 min-w-0">
                <div class="h-12 w-12 rounded-xl bg-secondary flex items-center justify-center text-lg font-bold border border-border/80 text-foreground flex-shrink-0">
                  {m.organization.avatarUrl ? (
                    <img src={m.organization.avatarUrl} alt="" class="w-full h-full rounded-xl object-cover" />
                  ) : (
                    m.organization.name[0]?.toUpperCase()
                  )}
                </div>
                <div class="min-w-0">
                  <h3 class="text-sm font-bold text-foreground group-hover:text-primary transition-colors truncate">
                    {m.organization.displayName || m.organization.name}
                  </h3>
                  <p class="text-xs text-muted-foreground">@{m.organization.name}</p>
                  {m.organization.description && (
                    <p class="text-xs text-muted-foreground mt-1 truncate max-w-lg">
                      {m.organization.description}
                    </p>
                  )}
                </div>
              </div>

              <div class="flex items-center gap-2 flex-shrink-0">
                <span class="inline-flex items-center gap-1 rounded-full border border-border/60 bg-muted/40 px-2.5 py-0.5 text-xs font-semibold text-muted-foreground capitalize">
                  <Shield className="h-3 w-3" />
                  {m.role}
                </span>
              </div>
            </a>
          ))}
        </div>
      )}
    </div>
  </div>
</BaseLayout>

<script>
  const form = document.getElementById("new-org-form");
  const toggleBtn = document.getElementById("new-org-btn");
  const cancelBtn = document.getElementById("org-create-cancel");
  const submitBtn = document.getElementById("org-create-submit");
  const errorDiv = document.getElementById("org-create-error");

  toggleBtn?.addEventListener("click", () => {
    form?.classList.toggle("hidden");
    errorDiv?.classList.add("hidden");
  });
  cancelBtn?.addEventListener("click", () => form?.classList.add("hidden"));

  submitBtn?.addEventListener("click", async () => {
    errorDiv?.classList.add("hidden");
    const name = (document.getElementById("org-name") as HTMLInputElement).value;
    const displayName = (document.getElementById("org-display") as HTMLInputElement).value;
    const description = (document.getElementById("org-desc") as HTMLTextAreaElement).value;
    try {
      const res = await fetch("/api/orgs", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ name, displayName, description }),
      });
      const data = await res.json();
      if (!res.ok) throw new Error(data.error?.message || "Failed to create organization");
      window.location.href = `/orgs/${data.data.organization.name}`;
    } catch (e) {
      if (errorDiv) {
        errorDiv.textContent = e instanceof Error ? e.message : String(e);
        errorDiv.classList.remove("hidden");
      }
    }
  });
</script>