Function bodies 190 total
components.PageHeaderH2 function · go · L266-L275 (10 LOC)internal/components/primitives.go
func PageHeaderH2(title string, actions ...g.Node) g.Node {
return Div(Class("page-header"),
H2(g.Text(title)),
g.If(len(actions) > 0,
Div(Class("page-header-actions"),
g.Group(actions),
),
),
)
}components.EmptyState function · go · L278-L283 (6 LOC)internal/components/primitives.go
func EmptyState(message string, action g.Node) g.Node {
return Div(Class("empty-state"),
P(g.Text(message)),
g.If(action != nil, action),
)
}components.Breadcrumbs function · go · L299-L322 (24 LOC)internal/components/primitives.go
func Breadcrumbs(items []BreadcrumbItem) g.Node {
listItems := make([]g.Node, 0, len(items))
for _, item := range items {
var content g.Node
if item.IsHome {
content = g.Group([]g.Node{g.Raw(homeIconSVG), g.Text(item.Label)})
} else {
content = g.Text(item.Label)
}
if item.Href == "" {
// Current page: no link, aria-current
listItems = append(listItems,
Li(g.Attr("aria-current", "page"), content))
} else {
listItems = append(listItems,
Li(A(Href(item.Href), content)))
}
}
return Nav(
g.Attr("aria-label", "Breadcrumb"),
Ol(g.Group(listItems)),
)
}components.PageTopmatter function · go · L326-L337 (12 LOC)internal/components/primitives.go
func PageTopmatter(title string, actions ...g.Node) g.Node {
return Div(Class("page-topmatter"),
Div(Class("page-topmatter-header"),
H1(g.Text(title)),
g.If(len(actions) > 0,
Div(Class("page-header-actions"),
g.Group(actions),
),
),
),
)
}components.PageTopmatterWithSubtitle function · go · L341-L355 (15 LOC)internal/components/primitives.go
func PageTopmatterWithSubtitle(title, subtitle string, actions ...g.Node) g.Node {
return Div(Class("page-topmatter"),
Div(Class("page-topmatter-header"),
Div(
H1(g.Text(title)),
g.If(subtitle != "", P(Class("meta"), g.Text(subtitle))),
),
g.If(len(actions) > 0,
Div(Class("page-header-actions"),
g.Group(actions),
),
),
),
)
}components.PageTopmatterH2 function · go · L358-L369 (12 LOC)internal/components/primitives.go
func PageTopmatterH2(title string, actions ...g.Node) g.Node {
return Div(Class("page-topmatter"),
Div(Class("page-topmatter-header"),
H2(g.Text(title)),
g.If(len(actions) > 0,
Div(Class("page-header-actions"),
g.Group(actions),
),
),
),
)
}components.ClassCrumb function · go · L384-L389 (6 LOC)internal/components/primitives.go
func ClassCrumb(class *core.Record) BreadcrumbItem {
return BreadcrumbItem{
Label: class.GetString("course_id"),
Href: "/classes/" + class.Id,
}
}Powered by Repobility — scan your code at https://repobility.com
components.ProjectCrumb function · go · L392-L397 (6 LOC)internal/components/primitives.go
func ProjectCrumb(project *core.Record) BreadcrumbItem {
return BreadcrumbItem{
Label: project.GetString("name"),
Href: "/projects/" + project.Id,
}
}components.TableCell function · go · L417-L422 (6 LOC)internal/components/primitives.go
func TableCell(label string, content g.Node) g.Node {
return Td(
g.Attr("data-label", label),
content,
)
}components.ProjectForm function · go · L15-L96 (82 LOC)internal/components/projects.go
func ProjectForm(class *core.Record, project *core.Record) g.Node {
isEdit := project != nil && project.Id != ""
title := "Create Project"
action := "/classes/" + class.Id + "/projects"
name, instructions, deadline := "", "", ""
isOpen := false
if isEdit {
title = "Edit Project"
action = "/projects/" + project.Id
name = project.GetString("name")
instructions = project.GetString("instructions")
deadline = timezone.FormatForDisplay(project.GetDateTime("deadline").Time(), "2006-01-02T15:04")
isOpen = project.GetBool("is_open")
}
return Div(
PageTopmatter(title),
FormEl(
Action(action),
Method("post"),
Div(
Label(
For("name"),
g.Text("Project Name"),
Input(
Type("text"),
Name("name"),
ID("name"),
Value(name),
Required(),
),
),
),
Div(
Label(
For("instructions"),
g.Text("Instructions for students"),
Textarea(
Name("instructions"),
ID("instructions"),
Rows(components.ProjectProgressStats function · go · L100-L112 (13 LOC)internal/components/projects.go
func ProjectProgressStats(projectId string, progress *db.ProjectProgress) g.Node {
return Article(
ID("progress-stats"),
Header(H3(g.Text("Results"))),
Dl(Class("metadata-list"),
Dt(g.Text("Students")),
Dd(g.Textf("%d / %d submitted", progress.StudentsSubmitted, progress.TotalStudents)),
Dt(g.Text("Teams")),
Dd(g.Textf("%d / %d complete", progress.TeamsSubmitted, progress.TotalTeams)),
),
Footer(A(Href("/projects/"+projectId+"/results"), Role("button"), g.Text("View Results"))),
)
}components.ProjectDetail function · go · L115-L173 (59 LOC)internal/components/projects.go
func ProjectDetail(project *core.Record, teams []db.TeamWithMembers, progress *db.ProjectProgress) g.Node {
isOpen := project.GetBool("is_open")
var statusBadge g.Node
if isOpen {
statusBadge = StatusBadge("Open", StatusSuccess)
} else {
statusBadge = StatusBadge("Closed", StatusNeutral)
}
deadline := timezone.FormatForDisplay(project.GetDateTime("deadline").Time(), "Jan 2, 3:04 PM")
return Div(
PageTopmatter(project.GetString("name")),
// Results section (live-updating via Datastar SSE)
Div(
DataOn("load", "@get('/projects/"+project.Id+"/progress-stream')"),
ProjectProgressStats(project.Id, progress),
),
// Settings section
Article(
Header(H3(g.Text("Settings"))),
Dl(Class("metadata-list"),
Dt(g.Text("Status")),
Dd(statusBadge),
Dt(g.Text("Deadline")),
Dd(g.Text(deadline)),
Dt(g.Text("Instructions")),
Dd(g.Text(project.GetString("instructions"))),
),
Footer(A(Href("/projects/"+project.Id+"/edit"), Role("button"), Clacomponents.TeamRow function · go · L176-L185 (10 LOC)internal/components/projects.go
func TeamRow(team db.TeamWithMembers) g.Node {
membersText := "—"
if len(team.Members) > 0 {
membersText = strings.Join(team.Members, ", ")
}
return Tr(
TableCell("Team", g.Text(team.Team.GetString("slug"))),
TableCell("Members", g.Text(membersText)),
)
}components.TeamUploadForm function · go · L188-L213 (26 LOC)internal/components/projects.go
func TeamUploadForm(project *core.Record) g.Node {
return Details(
Summary(g.Text("Upload Team Assignments")),
P(g.Text("Upload a CSV file with columns: netid, team_id")),
P(g.Text("Example: kj123,team-alpha")),
FormEl(
Action("/projects/"+project.Id+"/teams/upload"),
Method("post"),
g.Attr("enctype", "multipart/form-data"),
Div(
Label(
For("teams_csv"),
g.Text("Teams CSV File"),
Input(
Type("file"),
Name("teams_csv"),
ID("teams_csv"),
Accept(".csv"),
Required(),
),
),
),
Button(Type("submit"), g.Text("Upload Teams")),
),
)
}components.TeamUploadResults function · go · L216-L298 (83 LOC)internal/components/projects.go
func TeamUploadResults(projectID string, teamsCreated, membersAdded int, skippedNetIDs []string, duplicateInCSV []string, alreadyOnTeam []string, createdStubs []string) g.Node {
hasWarnings := len(skippedNetIDs) > 0
hasCSVDuplicates := len(duplicateInCSV) > 0
hasConflicts := len(alreadyOnTeam) > 0
hasCreatedStubs := len(createdStubs) > 0
return Div(
PageTopmatter("Team Upload Results"),
// Success summary
Article(
g.Attr("role", "alert"),
Class("alert alert--success"),
H3(g.Text("Upload Summary")),
Ul(
Li(g.Textf("Teams created: %d", teamsCreated)),
Li(g.Textf("Members added: %d", membersAdded)),
),
),
// Info for created stubs (new users who haven't logged in yet)
g.If(hasCreatedStubs,
Article(
g.Attr("role", "alert"),
Class("alert alert--info"),
H3(g.Text("New Student Accounts Created")),
P(g.Textf("The following %d student(s) had accounts created for them:", len(createdStubs))),
P(g.Text("These students will see theirSame scanner, your repo: https://repobility.com — Repobility
components.ProjectResults function · go · L12-L45 (34 LOC)internal/components/results.go
func ProjectResults(project *core.Record, results []db.ProjectResult) g.Node {
return Div(
PageTopmatter("Results",
A(
Href("/projects/"+project.Id+"/results/export"),
Role("button"),
g.Text("Export CSV"),
),
),
g.If(len(results) == 0,
EmptyState("No reviews submitted yet.", nil),
),
g.If(len(results) > 0,
Table(Class("responsive-table"),
THead(
Tr(
Th(g.Text("Student")),
Th(g.Text("Team")),
Th(g.Text("Avg %")),
Th(g.Text("Fair Share")),
Th(Class("hide-mobile"), g.Text("# Ratings")),
Th(g.Text("Status")),
Th(g.Text("Actions")),
),
),
TBody(
g.Group(g.Map(results, func(result db.ProjectResult) g.Node {
return ResultRow(project.Id, result)
})),
),
),
),
)
}components.ResultRow function · go · L48-L104 (57 LOC)internal/components/results.go
func ResultRow(projectId string, result db.ProjectResult) g.Node {
// Determine status text and variant based on outlier detection
var statusLabel string
var statusVariant StatusVariant
rowClass := ""
// Handle removed members
if result.IsRemoved {
statusLabel = "Removed"
statusVariant = StatusMuted
rowClass = "result-removed"
} else if result.IsOutlier {
if result.ContributionLevel == "high" {
statusLabel = "Above Expected"
statusVariant = StatusSuccess
rowClass = "outlier-high"
} else if result.ContributionLevel == "low" {
statusLabel = "Below Expected"
statusVariant = StatusWarning
rowClass = "outlier-low"
}
} else {
statusLabel = "Normal"
statusVariant = StatusNeutral
}
// Build student name with removed indicator
studentDisplay := result.Name + " (" + result.NetId + ")"
// Build value nodes based on removed status
var avgPctNode, fairShareNode, numRatingsNode g.Node
if result.IsRemoved {
dashNode := Span(Class("text-muted"), g.Tcomponents.ResultDetail function · go · L107-L141 (35 LOC)internal/components/results.go
func ResultDetail(student *core.Record, result db.ProjectResult, details []db.ReviewDetail) g.Node {
studentName := student.GetString("name")
if studentName == "" {
studentName = student.GetString("netid")
}
return Div(
PageTopmatter(studentName+" ("+student.GetString("netid")+")"),
MetadataList([]MetaItem{
{Label: "Team", Value: MetaText(result.TeamSlug)},
{Label: "Average Contribution", Value: g.Textf("%.1f%%", result.AvgPercentage)},
{Label: "Number of Ratings", Value: g.Textf("%d", result.NumRatings)},
{Label: "Contribution Level", Value: MetaText(result.ContributionLevel)},
}),
H3(g.Text("Individual Ratings")),
g.If(len(details) == 0,
EmptyState("No ratings received yet.", nil),
),
g.If(len(details) > 0,
Table(Class("responsive-table"),
THead(
Tr(
Th(g.Text("Reviewer")),
Th(g.Text("Percentage")),
Th(g.Text("Submitted")),
),
),
TBody(
g.Group(g.Map(details, func(detail db.ReviewDetail) g.Node {
components.DetailRow function · go · L144-L150 (7 LOC)internal/components/results.go
func DetailRow(detail db.ReviewDetail) g.Node {
return Tr(
TableCell("Reviewer", g.Text(detail.ReviewerName+" ("+detail.ReviewerNetId+")")),
TableCell("Percentage", g.Textf("%.1f%%", detail.Percentage)),
TableCell("Submitted", g.Text(detail.SubmittedAt)),
)
}components.StudentOwnResults function · go · L153-L173 (21 LOC)internal/components/results.go
func StudentOwnResults(avgPct, fairShare float64, numRaters int) g.Node {
return Div(
PageTopmatter("Your Results"),
Div(Class("results-summary"),
P(
Strong(g.Text("Your teammates rated your contribution at: ")),
Span(Class("result-value"), g.Textf("%.1f%%", avgPct)),
),
P(
g.Text("Fair share would be: "),
Span(g.Textf("%.1f%%", fairShare)),
),
P(Class("meta"),
g.Textf("Based on ratings from %d teammates", numRaters),
),
),
Div(Class("privacy-note"),
P(g.Text("Individual rater identities are kept private to encourage honest feedback.")),
),
)
}components.PrivacySuppressedMessage function · go · L176-L182 (7 LOC)internal/components/results.go
func PrivacySuppressedMessage() g.Node {
return Div(Class("privacy-suppressed"),
PageTopmatter("Results Not Available"),
P(g.Text("Not enough responses to show results (privacy protection).")),
P(g.Text("At least 3 teammates must submit reviews before results are visible.")),
)
}components.StudentReviewsPage function · go · L21-L94 (74 LOC)internal/components/students.go
func StudentReviewsPage(pending []db.PendingReview, editable, completed []db.StudentSubmission, submittedProjectId, submittedProjectName string) g.Node {
return Div(
PageTopmatter("Your Peer Reviews"),
// Success message (e.g. after submitting a review)
g.If(submittedProjectId != "",
AlertNode("success",
g.Text("Your review for "),
A(Href("/reviews/"+submittedProjectId+"/submit"), g.Text(submittedProjectName)),
g.Text(" has been saved. You can edit it until the deadline."),
),
),
// Section 1: Action Needed
Article(Class("review-section"),
Header(
H3(g.Text("Action Needed")),
g.If(len(pending) > 0, Small(g.Textf("(%d)", len(pending)))),
),
g.If(len(pending) == 0,
P(Class("empty-state"), g.Text("You're all caught up! No pending reviews.")),
),
g.If(len(pending) > 0,
ResourceListContainer(
ResourceList(
g.Group(g.Map(pending, func(review db.PendingReview) g.Node {
return ResourceListItem(PendingReviewCard(rcomponents.PendingReviewsList function · go · L98-L116 (19 LOC)internal/components/students.go
func PendingReviewsList(reviews []db.PendingReview) g.Node {
if len(reviews) == 0 {
return Div(
PageTopmatter("Pending Reviews"),
EmptyState("You're all caught up! No pending peer reviews at this time.", nil),
)
}
return Div(
PageTopmatter("Pending Reviews"),
ResourceListContainer(
ResourceList(
g.Group(g.Map(reviews, func(review db.PendingReview) g.Node {
return ResourceListItem(PendingReviewCard(review))
})),
),
),
)
}Repobility · open methodology · https://repobility.com/research/
components.PendingReviewCard function · go · L119-L133 (15 LOC)internal/components/students.go
func PendingReviewCard(review db.PendingReview) g.Node {
deadline := parseDeadline(review.Deadline)
urgencyClass := getUrgencyClass(deadline)
relativeTime := getRelativeTime(deadline)
return ActionCard(ActionCardConfig{
Title: review.ProjectName,
Subtitle: review.ClassName,
Tertiary: review.CourseId + " · " + review.Instance,
Meta: []MetaItem{{Label: "Due", Value: MetaText(relativeTime)}},
ActionLabel: "Start Review",
ActionHref: "/reviews/" + review.ProjectId + "/submit",
Urgency: urgencyClass,
})
}components.EditableReviewCard function · go · L136-L153 (18 LOC)internal/components/students.go
func EditableReviewCard(sub db.StudentSubmission) g.Node {
deadline := parseDeadline(sub.Deadline)
submittedAt := parseDeadline(sub.SubmittedAt)
return ActionCard(ActionCardConfig{
Title: sub.ProjectName,
Subtitle: sub.ClassName,
Tertiary: sub.CourseId + " · " + sub.Instance,
Meta: []MetaItem{
{Label: "Submitted", Value: MetaText(timezone.FormatForDisplay(submittedAt, "Jan 2, 3:04 PM"))},
{Label: "Editable until", Value: MetaText(timezone.FormatForDisplay(deadline, "Jan 2, 3:04 PM"))},
},
ActionLabel: "Edit Review",
ActionHref: "/reviews/" + sub.ProjectId + "/submit",
ActionStyle: "secondary",
ExtraClasses: []string{"review-editable"},
})
}components.CompletedReviewCard function · go · L156-L179 (24 LOC)internal/components/students.go
func CompletedReviewCard(sub db.StudentSubmission) g.Node {
submittedAt := parseDeadline(sub.SubmittedAt)
// Determine status text
statusText := "Closed"
if !sub.IsOpen {
statusText = "Project closed"
} else {
statusText = "Deadline passed"
}
return ActionCard(ActionCardConfig{
Title: sub.ProjectName,
Subtitle: sub.ClassName,
Tertiary: sub.CourseId + " · " + sub.Instance,
Meta: []MetaItem{
{Label: "Submitted", Value: MetaText(timezone.FormatForDisplay(submittedAt, "Jan 2, 3:04 PM"))},
{Label: "Status", Value: MetaText(statusText)},
},
// No ActionLabel/ActionHref - completed reviews have no action
// TODO: Add "View Results" link when results endpoint is ready
ExtraClasses: []string{"review-completed"},
})
}components.getUrgencyClass function · go · L182-L192 (11 LOC)internal/components/students.go
func getUrgencyClass(deadline time.Time) string {
hoursUntil := time.Until(deadline).Hours()
if hoursUntil < 0 {
return "extended" // Past deadline but still accepting (faculty reopened/extended)
} else if hoursUntil < 24 {
return "urgent"
} else if hoursUntil < 72 {
return "soon"
}
return ""
}components.getRelativeTime function · go · L196-L206 (11 LOC)internal/components/students.go
func getRelativeTime(deadline time.Time) string {
hoursUntil := time.Until(deadline).Hours()
if hoursUntil < 0 {
return "Late submissions accepted"
} else if hoursUntil < 24 {
return fmt.Sprintf("Due in %d hours", int(hoursUntil))
} else if hoursUntil < 72 {
return fmt.Sprintf("Due in %d days", int(hoursUntil/24))
}
return timezone.FormatForDisplay(deadline, "Due Mon Jan 2, 3:04 PM")
}components.ReviewSubmissionForm function · go · L222-L284 (63 LOC)internal/components/students.go
func ReviewSubmissionForm(projectName, instructions string, teammates []Teammate, existingRatings map[string]int, projectId string) g.Node {
// Calculate initial percentages, distributing remainder across first N teammates
initialPercents := calculateInitialPercents(len(teammates))
// Build teammates JSON for data attribute
var tmList []teammateJSData
for i, tm := range teammates {
percent := initialPercents[i]
if existing, ok := existingRatings[tm.Id]; ok {
percent = existing
}
tmList = append(tmList, teammateJSData{
Id: tm.Id,
NetId: tm.NetId,
Name: tm.Name,
Initial: percent,
})
}
teammatesJSONBytes, err := json.Marshal(tmList)
if err != nil {
teammatesJSONBytes = []byte("[]")
}
return Div(
PageTopmatter(projectName),
Article(
H3(g.Text("Instructions")),
P(g.Text(instructions)),
),
FormEl(
ID("review-form"),
Action("/reviews/"+projectId+"/submit"),
Method("post"),
g.Attr("data-teammates", string(teammatesJSONcomponents.calculateInitialPercents function · go · L289-L305 (17 LOC)internal/components/students.go
func calculateInitialPercents(teamSize int) []int {
if teamSize <= 0 {
return nil
}
basePercent := 100 / teamSize
remainder := 100 % teamSize
percents := make([]int, teamSize)
for i := 0; i < teamSize; i++ {
if i < remainder {
percents[i] = basePercent + 1
} else {
percents[i] = basePercent
}
}
return percents
}components.TeammateRatingControl function · go · L308-L360 (53 LOC)internal/components/students.go
func TeammateRatingControl(tm Teammate, initialPercent int) g.Node {
return Div(
ID("control-"+tm.Id),
Class("rating-control"),
Div(
Class("rating-control__info"),
Strong(g.Text(tm.Name)),
Br(),
Span(Class("rating-control__netid"), g.Text("("+tm.NetId+")")),
),
Div(
Class("rating-control__buttons"),
Button(
Type("button"),
ID("minus-"+tm.Id),
Class("adjust-button rating-control__adjust-btn"),
g.Attr("data-user", tm.Id),
g.Attr("data-delta", "-5"),
g.Text("−"),
),
Span(
ID("percent-"+tm.Id),
Class("rating-control__value"),
g.Textf("%d%%", initialPercent),
),
Button(
Type("button"),
ID("plus-"+tm.Id),
Class("adjust-button rating-control__adjust-btn"),
g.Attr("data-user", tm.Id),
g.Attr("data-delta", "5"),
g.Text("+"),
),
Span(
ID("lock-"+tm.Id),
Class("rating-control__lock"),
g.Attr("data-user", tm.Id),
g.Attr("role", "switch"),
g.Attr("aria-checked", "false"),
Repobility analyzer · published findings · https://repobility.com
components.SubmissionHistory function · go · L372-L395 (24 LOC)internal/components/students.go
func SubmissionHistory(submissions []db.StudentSubmission) g.Node {
return Div(
PageTopmatter("Submission History"),
g.If(len(submissions) == 0,
EmptyState("You haven't submitted any reviews yet.", nil),
),
g.If(len(submissions) > 0,
Table(Class("responsive-table"),
THead(
Tr(
Th(g.Text("Class")),
Th(g.Text("Project")),
Th(g.Text("Submitted")),
Th(g.Text("Status")),
Th(g.Text("Actions")),
),
),
TBody(
g.Group(g.Map(submissions, SubmissionRow)),
),
),
),
)
}components.SubmissionRow function · go · L398-L424 (27 LOC)internal/components/students.go
func SubmissionRow(sub db.StudentSubmission) g.Node {
submittedAt := parseDeadline(sub.SubmittedAt)
deadline := parseDeadline(sub.Deadline)
canEdit := sub.IsOpen && time.Now().Before(deadline)
statusText := "Closed"
if sub.IsOpen {
statusText = "Open"
}
return Tr(
Td(g.Text(sub.ClassName)),
Td(g.Text(sub.ProjectName)),
Td(g.Text(submittedAt.Format("Jan 2, 3:04 PM"))),
Td(g.Text(statusText)),
Td(
g.If(canEdit,
A(
Href("/reviews/"+sub.ProjectId+"/submit"),
Role("button"),
Class("secondary"),
g.Text("Edit"),
),
),
),
)
}components.TeamChangeConfirmation function · go · L27-L100 (74 LOC)internal/components/teams.go
func TeamChangeConfirmation(projectId string, changes []TeamChange, hasReviews bool, changesJSON string) g.Node {
// Count changes by type
removes := 0
moves := 0
adds := 0
for _, c := range changes {
switch c.ChangeType {
case "remove":
removes++
case "move":
moves++
case "add":
adds++
}
}
return Div(
PageTopmatter("Confirm Team Changes"),
// Warning banner if reviews exist
g.If(hasReviews,
Article(g.Attr("role", "alert"), Class("alert alert--warning"),
P(Strong(g.Text("⚠️ This project has submitted reviews."))),
P(g.Text("Some changes will affect review data. Please review carefully.")),
),
),
// Summary
Article(Class("review-section"),
Header(H3(g.Text("Changes Summary"))),
Ul(
g.If(removes > 0, Li(g.Textf("%d student(s) will be removed from teams", removes))),
g.If(moves > 0, Li(g.Textf("%d student(s) will be moved between teams", moves))),
g.If(adds > 0, Li(g.Textf("%d student(s) will be added to teams", addscomponents.changeDetailRow function · go · L103-L158 (56 LOC)internal/components/teams.go
func changeDetailRow(c TeamChange) g.Node {
// Build change description
var changeDesc g.Node
switch c.ChangeType {
case "remove":
changeDesc = g.Group([]g.Node{
Span(Class("text-danger"), g.Text("Remove")),
g.Text(" from "),
Strong(g.Text(c.FromTeam)),
})
case "move":
changeDesc = g.Group([]g.Node{
Span(Class("text-warning"), g.Text("Move")),
g.Text(" from "),
Strong(g.Text(c.FromTeam)),
g.Text(" to "),
Strong(g.Text(c.ToTeam)),
})
case "add":
changeDesc = g.Group([]g.Node{
Span(Class("text-success"), g.Text("Add")),
g.Text(" to "),
Strong(g.Text(c.ToTeam)),
})
}
// Build impact description
var impactDesc g.Node
if c.Impact != nil && (c.Impact.HasSubmittedReview || c.Impact.RatingsReceivedCount > 0) {
impactItems := []g.Node{}
if c.Impact.HasSubmittedReview {
impactItems = append(impactItems,
Li(g.Textf("Review of %d teammates will be excluded", c.Impact.RatingsGivenCount)),
)
}
if c.Impact.RatingsReceivedCount components.TeamRosterPage function · go · L167-L208 (42 LOC)internal/components/teams.go
func TeamRosterPage(roster []db.RosterEntry, teams []*core.Record, teamCounts map[string]int, projectId, successMsg, errorMsg string) g.Node {
return Div(
PageTopmatter("Team Roster"),
// Messages
g.If(successMsg != "",
Article(g.Attr("role", "alert"), Class("alert alert--success"), P(g.Text(successMsg))),
),
g.If(errorMsg != "",
Article(g.Attr("role", "alert"), Class("alert alert--danger"), P(g.Text(errorMsg))),
),
// Teams Summary
Article(Class("review-section"),
Header(H3(g.Text("Teams"))),
teamsSummary(teams, teamCounts, projectId),
),
// Add Student Form
Article(Class("review-section"),
Header(H3(g.Text("Add Student"))),
addStudentForm(projectId, teams),
),
// Create Team Form
Article(Class("review-section"),
Header(H3(g.Text("Create Team"))),
createTeamForm(projectId),
),
// Roster Table
Article(Class("review-section"),
Header(H3(g.Text("Student Assignments"))),
g.If(len(roster) == 0,
EmptyState("No studentcomponents.teamsSummary function · go · L211-L257 (47 LOC)internal/components/teams.go
func teamsSummary(teams []*core.Record, teamCounts map[string]int, projectId string) g.Node {
if len(teams) == 0 {
return Article(Class("resource-card"),
P(g.Text("No teams created yet.")),
)
}
return Article(Class("resource-card"),
Table(
THead(
Tr(
Th(g.Text("Team")),
Th(g.Text("Members")),
Th(g.Text("Actions")),
),
),
TBody(
g.Group(g.Map(teams, func(team *core.Record) g.Node {
teamId := team.Id
slug := team.GetString("slug")
count := teamCounts[teamId]
return Tr(
Td(g.Attr("data-label", "Team"), g.Text(slug)),
Td(g.Attr("data-label", "Members"), g.Text(strconv.Itoa(count))),
Td(g.Attr("data-label", "Actions"),
g.If(count == 0,
FormEl(
Action("/projects/"+projectId+"/teams/"+teamId+"/delete"),
Method("post"),
Class("form-inline"),
Button(
Type("submit"),
Class("secondary"),
g.Text("Delete"),
),
),
components.addStudentForm function · go · L260-L304 (45 LOC)internal/components/teams.go
func addStudentForm(projectId string, teams []*core.Record) g.Node {
if len(teams) == 0 {
return Article(Class("resource-card"),
P(g.Text("Create a team first before adding students.")),
)
}
return Article(Class("resource-card"),
FormEl(
Action("/projects/"+projectId+"/teams/add-student"),
Method("post"),
Div(Class("grid"),
Label(
For("netid"),
g.Text("NetID"),
Input(
Type("text"),
Name("netid"),
ID("netid"),
Placeholder("e.g., abc123"),
Required(),
),
),
Label(
For("team_id"),
g.Text("Team"),
Select(
Name("team_id"),
ID("team_id"),
Required(),
g.Group(g.Map(teams, func(team *core.Record) g.Node {
return Option(
Value(team.Id),
g.Text(team.GetString("slug")),
)
})),
),
),
),
Div(Class("button-row"),
Button(Type("submit"), g.Text("Add Student")),
),
),
)
}components.createTeamForm function · go · L307-L330 (24 LOC)internal/components/teams.go
func createTeamForm(projectId string) g.Node {
return Article(Class("resource-card"),
FormEl(
Action("/projects/"+projectId+"/teams/create"),
Method("post"),
Div(
Label(
For("slug"),
g.Text("Team Name"),
Input(
Type("text"),
Name("slug"),
ID("slug"),
Placeholder("e.g., alpha, team-1"),
Required(),
),
),
),
Div(Class("button-row"),
Button(Type("submit"), g.Text("Create Team")),
),
),
)
}Powered by Repobility — scan your code at https://repobility.com
components.rosterForm function · go · L333-L365 (33 LOC)internal/components/teams.go
func rosterForm(projectId string, roster []db.RosterEntry, teams []*core.Record) g.Node {
return Article(Class("resource-card"),
FormEl(
ID("roster-form"),
Action("/projects/"+projectId+"/teams"),
Method("post"),
Table(
THead(
Tr(
Th(g.Text("Name")),
Th(g.Text("NetID")),
Th(g.Text("Team")),
),
),
TBody(
g.Group(g.Map(roster, func(entry db.RosterEntry) g.Node {
return Tr(
Td(g.Attr("data-label", "Name"), g.Text(entry.Name)),
Td(g.Attr("data-label", "NetID"), Code(g.Text(entry.NetId))),
Td(g.Attr("data-label", "Team"),
teamSelect(entry.UserId, entry.TeamId, teams),
),
)
})),
),
),
Div(Class("button-row"),
Button(Type("submit"), g.Text("Save Changes")),
),
),
Script(Src(assets.Path("js/roster-form.js")), Defer()),
)
}components.teamSelect function · go · L368-L388 (21 LOC)internal/components/teams.go
func teamSelect(userId, currentTeamId string, teams []*core.Record) g.Node {
return Select(
Name("team_"+userId),
ID("team_"+userId),
Class("team-select"),
// Unassigned option
Option(
Value(""),
g.If(currentTeamId == "", Selected()),
g.Text("- Unassigned -"),
),
// Team options
g.Group(g.Map(teams, func(team *core.Record) g.Node {
return Option(
Value(team.Id),
g.If(team.Id == currentTeamId, Selected()),
g.Text(team.GetString("slug")),
)
})),
)
}db.GetClassesByTeacher function · go · L20-L34 (15 LOC)internal/db/helpers.go
func GetClassesByTeacher(app *pocketbase.PocketBase, teacherId string, includeArchived bool) ([]*core.Record, error) {
filter := "(teacher = {:user} || staff.id ?= {:user})"
params := map[string]any{"user": teacherId}
if !includeArchived {
filter += " && archived = false"
}
records, err := app.FindRecordsByFilter("classes", filter, "", 0, 0, params)
if err != nil {
return nil, err
}
return records, nil
}db.GetProjectsByClass function · go · L59-L66 (8 LOC)internal/db/helpers.go
func GetProjectsByClass(app *pocketbase.PocketBase, classId string) ([]*core.Record, error) {
return app.FindRecordsByFilter("projects",
"class = {:class}",
"",
0,
0,
map[string]any{"class": classId})
}db.GetOpenProjectsByClass function · go · L69-L76 (8 LOC)internal/db/helpers.go
func GetOpenProjectsByClass(app *pocketbase.PocketBase, classId string) ([]*core.Record, error) {
return app.FindRecordsByFilter("projects",
"class = {:class} && is_open = true",
"",
0,
0,
map[string]any{"class": classId})
}db.GetTeamsByProject function · go · L86-L93 (8 LOC)internal/db/helpers.go
func GetTeamsByProject(app *pocketbase.PocketBase, projectId string) ([]*core.Record, error) {
return app.FindRecordsByFilter("teams",
"project = {:project}",
"slug",
0,
0,
map[string]any{"project": projectId})
}db.GetTeamsWithMembersByProject function · go · L102-L146 (45 LOC)internal/db/helpers.go
func GetTeamsWithMembersByProject(app *pocketbase.PocketBase, projectId string) ([]TeamWithMembers, error) {
// Get all teams
teams, err := GetTeamsByProject(app, projectId)
if err != nil {
return nil, err
}
// Get all ACTIVE team members for this project with expanded user data
members, err := app.FindRecordsByFilter("team_members",
"team.project = {:project} && removed_at = ''",
"",
0,
0,
map[string]any{"project": projectId})
if err != nil {
return nil, err
}
// Build a map of team ID to member netids
teamMembers := make(map[string][]string)
for _, member := range members {
teamId := member.GetString("team")
// Get the user record to get the netid
userId := member.GetString("user")
user, err := app.FindRecordById("users", userId)
if err != nil {
continue // Skip if user not found
}
netid := user.GetString("netid")
teamMembers[teamId] = append(teamMembers[teamId], netid)
}
// Build result
result := make([]TeamWithMembers, len(teams))db.GetTeamMembersByTeam function · go · L163-L170 (8 LOC)internal/db/helpers.go
func GetTeamMembersByTeam(app *pocketbase.PocketBase, teamId string) ([]*core.Record, error) {
return app.FindRecordsByFilter("team_members",
"team = {:team}",
"",
0,
0,
map[string]any{"team": teamId})
}Same scanner, your repo: https://repobility.com — Repobility
db.GetTeamMembersByUser function · go · L173-L180 (8 LOC)internal/db/helpers.go
func GetTeamMembersByUser(app *pocketbase.PocketBase, userId string) ([]*core.Record, error) {
return app.FindRecordsByFilter("team_members",
"user = {:user}",
"",
0,
0,
map[string]any{"user": userId})
}db.GetTeamMembersByProject function · go · L183-L190 (8 LOC)internal/db/helpers.go
func GetTeamMembersByProject(app *pocketbase.PocketBase, projectId string) ([]*core.Record, error) {
return app.FindRecordsByFilter("team_members",
"team.project = {:project}",
"",
0,
0,
map[string]any{"project": projectId})
}db.IsUserOnTeam function · go · L194-L205 (12 LOC)internal/db/helpers.go
func IsUserOnTeam(app *pocketbase.PocketBase, teamId, userId string) (bool, error) {
_, err := app.FindFirstRecordByFilter("team_members",
"team = {:team} && user = {:user}",
map[string]any{"team": teamId, "user": userId})
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return false, nil
}
return false, err
}
return true, nil
}