diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml
new file mode 100644
index 0000000..610047c
--- /dev/null
+++ b/.idea/dataSources.xml
@@ -0,0 +1,19 @@
+
+
+
+
+ postgresql
+ true
+ org.postgresql.Driver
+ jdbc:postgresql://db03.millslan.net:5432/www_1
+ $ProjectFileDir$
+
+
+ mongo
+ true
+ com.dbschema.MongoJdbcDriver
+ mongodb://db03.millslan.net:27017
+ $ProjectFileDir$
+
+
+
\ No newline at end of file
diff --git a/.idea/extollo.iml b/.idea/extollo.iml
index cb5ff22..9e388b7 100644
--- a/.idea/extollo.iml
+++ b/.idea/extollo.iml
@@ -1,7 +1,9 @@
-
+
+
+
diff --git a/package.json b/package.json
index f7ee3dc..e1b7c90 100644
--- a/package.json
+++ b/package.json
@@ -8,7 +8,7 @@
"lib": "lib"
},
"dependencies": {
- "@extollo/lib": "^0.9.0",
+ "@extollo/lib": "^0.9.1",
"copyfiles": "^2.4.1",
"rimraf": "^3.0.2",
"ts-expose-internals": "^4.5.4",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index d0be490..e4e37cb 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -2,7 +2,7 @@ lockfileVersion: 5.3
specifiers:
'@extollo/cc': ^0.6.0
- '@extollo/lib': ^0.9.0
+ '@extollo/lib': ^0.9.1
copyfiles: ^2.4.1
rimraf: ^3.0.2
ts-expose-internals: ^4.5.4
@@ -12,7 +12,7 @@ specifiers:
zod: ^3.11.6
dependencies:
- '@extollo/lib': 0.9.0
+ '@extollo/lib': 0.9.1
copyfiles: 2.4.1
rimraf: 3.0.2
ts-expose-internals: 4.5.4
@@ -110,8 +110,8 @@ packages:
- supports-color
dev: true
- /@extollo/lib/0.9.0:
- resolution: {integrity: sha512-Gu9qwjwjDWPfXrp1ThyjFIROWXD8jXqwz4c7JxjWoWbGnY0dLGAZHv6MOF1tqNGU9zTW399jpGS/7lyazApMpQ==}
+ /@extollo/lib/0.9.1:
+ resolution: {integrity: sha512-HPYwlKO0SHZsTQBQh5zTA8oErIEncdFWKr3LGs3LmL7Dq8rWN7dQ1qK2qk809Fc7FBqWeiZKuOsRA8yLkhr6Kg==}
dependencies:
'@atao60/fse-cli': 0.1.7
'@extollo/ui': 0.1.0_@types+node@14.18.12
diff --git a/src/app/configs/app.config.ts b/src/app/configs/app.config.ts
index 57042be..d78a24d 100644
--- a/src/app/configs/app.config.ts
+++ b/src/app/configs/app.config.ts
@@ -1,5 +1,5 @@
import { env } from '@extollo/lib'
export default {
- name: env('APP_NAME', 'Extollo'),
+ name: env('APP_NAME', 'Garrett Mills'),
}
diff --git a/src/app/configs/database.config.ts b/src/app/configs/database.config.ts
index 0a4f070..e8873e2 100644
--- a/src/app/configs/database.config.ts
+++ b/src/app/configs/database.config.ts
@@ -3,7 +3,7 @@ import { env } from "@extollo/lib";
export default {
connections: {
default: {
- user: env('DATABASE_USERNAME', 'extollo'),
+ user: env('DATABASE_USERNAME', 'www'),
password: env('DATABASE_PASSWORD'),
host: env('DATABASE_HOST', 'localhost'),
port: env('DATABASE_PORT', 5432),
diff --git a/src/app/http/controllers/Home.controller.ts b/src/app/http/controllers/Home.controller.ts
new file mode 100644
index 0000000..3946bc4
--- /dev/null
+++ b/src/app/http/controllers/Home.controller.ts
@@ -0,0 +1,59 @@
+import {Controller, view, Injectable, SecurityContext, Inject, Collection} from '@extollo/lib'
+import {WorkItem} from '../../models/WorkItem.model'
+import {FeedPost} from '../../models/FeedPost.model'
+
+@Injectable()
+export class Home extends Controller {
+ @Inject()
+ protected readonly security!: SecurityContext
+
+ public async welcome() {
+ const workItems = await this.getWorkItems()
+ const feedPosts = await this.getFeedPosts()
+
+ return view('welcome', {
+ feedPosts: feedPosts.toArray(),
+ workItemGroups: workItems.groupBy(item => item.startDate.getFullYear()),
+ workItemYears: workItems.map(item => item.startDate.getFullYear())
+ .unique()
+ .toArray(),
+ workItemHiddenYears: workItems.filter(item => item.endDate)
+ .map(item => item.startDate.getFullYear())
+ .unique()
+ .toArray()
+ })
+ }
+
+ public async feed() {
+ const feedPosts = await this.getFeedPosts(true)
+ return view('feed', {
+ feedPosts: feedPosts.toArray(),
+ })
+ }
+
+ protected async getFeedPosts(all = false): Promise> {
+ const query = FeedPost.query()
+ .orderByDescending('posted_at')
+
+ if ( !all ) {
+ query.limit(6)
+ }
+
+ if ( !this.security.hasUser() ) {
+ query.where('visible', '=', true)
+ }
+
+ return query.get().collect()
+ }
+
+ protected async getWorkItems(): Promise> {
+ const query = WorkItem.query()
+ .orderByDescending('start_date')
+
+ if ( !this.security.hasUser() ) {
+ query.where('visible', '=', true)
+ }
+
+ return query.get().collect()
+ }
+}
diff --git a/src/app/http/controllers/main/Home.controller.ts b/src/app/http/controllers/main/Home.controller.ts
deleted file mode 100644
index 14275ba..0000000
--- a/src/app/http/controllers/main/Home.controller.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import {Controller, view, Session, Inject, Injectable, Locale, Validator} from '@extollo/lib'
-import {UserLogin} from "../../../types/UserLogin";
-
-@Injectable()
-export class Home extends Controller {
- @Inject()
- protected readonly session!: Session;
-
- @Inject()
- protected readonly locale!: Locale;
-
- public welcome() {
- this.session.set('app_visits', this.session.get('app_visits', 0) + 1)
-
- const valid = new Promise(() => {})
-
- return view('@extollo:welcome', {
- app_visits: this.session.get('app_visits'),
- locale: this.locale.helper(),
- })
- }
-
- public test() {
- return new Validator()
- }
-}
diff --git a/src/app/http/routes/app.routes.ts b/src/app/http/routes/app.routes.ts
index c7f578d..0aed250 100644
--- a/src/app/http/routes/app.routes.ts
+++ b/src/app/http/routes/app.routes.ts
@@ -1,13 +1,17 @@
-import {Route, SessionAuthMiddleware, AuthRequiredMiddleware} from '@extollo/lib'
-import {Home} from '../controllers/main/Home.controller'
+import {Route, SessionAuthMiddleware} from '@extollo/lib'
+import {Home} from '../controllers/Home.controller'
Route.group('/', () => {
Route.get('/')
.calls(Home, home => home.welcome)
-})
-Route.group('', () => {
- Route.get('/dash')
- .pre(AuthRequiredMiddleware)
- .calls(Home, home => home.welcome)
+ Route.get('/feed')
+ .calls(Home, home => home.feed)
+ .alias('feed')
}).pre(SessionAuthMiddleware)
+
+// Route.group('', () => {
+// Route.get('/dash')
+// .pre(AuthRequiredMiddleware)
+// .calls(Home, home => home.welcome)
+// }).pre(SessionAuthMiddleware)
diff --git a/src/app/migrations/2022-03-29T14:53:08.624Z_CreateWorkItemsTableMigration.migration.ts b/src/app/migrations/2022-03-29T14:53:08.624Z_CreateWorkItemsTableMigration.migration.ts
new file mode 100644
index 0000000..a58ce4b
--- /dev/null
+++ b/src/app/migrations/2022-03-29T14:53:08.624Z_CreateWorkItemsTableMigration.migration.ts
@@ -0,0 +1,58 @@
+import {DatabaseService, FieldType, Inject, Injectable, Migration} from '@extollo/lib'
+
+/**
+ * CreateWorkItemsTableMigration
+ * ----------------------------------
+ * Create the work_items table to track project history.
+ */
+@Injectable()
+export default class CreateWorkItemsTableMigration extends Migration {
+ @Inject()
+ protected readonly db!: DatabaseService
+
+ /**
+ * Apply the migration.
+ */
+ async up(): Promise {
+ const schema = this.db.get().schema()
+ const table = await schema.table('work_items')
+
+ table.primaryKey('work_item_id').required()
+
+ table.column('visible')
+ .type(FieldType.bool)
+ .required()
+ .default(false)
+
+ table.column('name')
+ .type(FieldType.varchar)
+ .required()
+
+ table.column('description')
+ .type(FieldType.text)
+ .required()
+ .default('')
+
+ table.column('start_date')
+ .type(FieldType.timestamp)
+ .required()
+
+ table.column('end_date')
+ .type(FieldType.timestamp)
+ .nullable()
+
+ await schema.commit(table)
+ }
+
+ /**
+ * Undo the migration.
+ */
+ async down(): Promise {
+ const schema = this.db.get().schema()
+ const table = await schema.table('work_items')
+
+ table.dropIfExists()
+
+ await schema.commit(table)
+ }
+}
diff --git a/src/app/migrations/2022-03-29T17:04:30.346Z_CreateFeedPostsTableMigration.migration.ts b/src/app/migrations/2022-03-29T17:04:30.346Z_CreateFeedPostsTableMigration.migration.ts
new file mode 100644
index 0000000..fc82630
--- /dev/null
+++ b/src/app/migrations/2022-03-29T17:04:30.346Z_CreateFeedPostsTableMigration.migration.ts
@@ -0,0 +1,58 @@
+import {DatabaseService, FieldType, Inject, Injectable, Migration, raw} from '@extollo/lib'
+
+/**
+ * CreateFeedPostsTableMigration
+ * ----------------------------------
+ * Create the table to hold posts to the "updates" feed.
+ */
+@Injectable()
+export default class CreateFeedPostsTableMigration extends Migration {
+ @Inject()
+ protected readonly db!: DatabaseService
+
+ /**
+ * Apply the migration.
+ */
+ async up(): Promise {
+ const schema = this.db.get().schema()
+ const table = await schema.table('feed_posts')
+
+ table.primaryKey('feed_post_id', FieldType.varchar)
+ .required()
+
+ // date, tag, text, visible
+
+ table.column('posted_at')
+ .type(FieldType.timestamp)
+ .default(raw('NOW()'))
+ .required()
+
+ table.column('tag')
+ .type(FieldType.varchar)
+ .required()
+
+ table.column('visible')
+ .type(FieldType.bool)
+ .default(false)
+ .required()
+
+ table.column('body')
+ .type(FieldType.text)
+ .default('')
+ .required()
+
+ await schema.commit(table)
+ }
+
+ /**
+ * Undo the migration.
+ */
+ async down(): Promise {
+ const schema = this.db.get().schema()
+ const table = await schema.table('feed_posts')
+
+ table.dropIfExists()
+
+ await schema.commit(table)
+ }
+}
diff --git a/src/app/models/FeedPost.model.ts b/src/app/models/FeedPost.model.ts
new file mode 100644
index 0000000..32a6037
--- /dev/null
+++ b/src/app/models/FeedPost.model.ts
@@ -0,0 +1,21 @@
+import {Field, FieldType, Model} from '@extollo/lib'
+
+export class FeedPost extends Model {
+ protected static table = 'feed_posts'
+ protected static key = 'feed_post_id'
+
+ @Field(FieldType.varchar, 'feed_post_id')
+ public readonly feedPostId?: string
+
+ @Field(FieldType.timestamp, 'posted_at')
+ public postedAt: Date = new Date()
+
+ @Field(FieldType.varchar)
+ public tag!: string
+
+ @Field(FieldType.bool)
+ public visible = false
+
+ @Field(FieldType.text)
+ public body = ''
+}
diff --git a/src/app/models/WorkItem.model.ts b/src/app/models/WorkItem.model.ts
new file mode 100644
index 0000000..c521729
--- /dev/null
+++ b/src/app/models/WorkItem.model.ts
@@ -0,0 +1,44 @@
+import {Field, FieldType, Model} from '@extollo/lib'
+
+export class WorkItem extends Model {
+ protected static table = 'work_items'
+ protected static key = 'work_item_id'
+
+ @Field(FieldType.serial, 'work_item_id')
+ public readonly workItemId!: number
+
+ @Field(FieldType.bool)
+ public visible: boolean = false
+
+ @Field(FieldType.varchar)
+ public name!: string
+
+ @Field(FieldType.text)
+ public description: string = ''
+
+ @Field(FieldType.timestamp, 'start_date')
+ public startDate!: Date
+
+ @Field(FieldType.timestamp, 'end_date')
+ public endDate?: Date
+
+ public startDisplay(): string {
+ return `${this.getMonth(this.startDate.getMonth())} ${this.startDate.getFullYear()}`
+ }
+
+ public endDisplay(): string {
+ if ( !this.endDate ) {
+ return 'Present'
+ }
+
+ return `${this.getMonth(this.endDate.getMonth())} ${this.endDate.getFullYear()}`
+ }
+
+ public rangeDisplay(): string {
+ return `${this.startDisplay()} - ${this.endDisplay()}`
+ }
+
+ protected getMonth(index: number): string {
+ return ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'][index]
+ }
+}
diff --git a/src/app/resources/assets/ffox.png b/src/app/resources/assets/ffox.png
new file mode 100644
index 0000000..4d54735
Binary files /dev/null and b/src/app/resources/assets/ffox.png differ
diff --git a/src/app/resources/assets/font/fira/FiraCode-Bold.ttf b/src/app/resources/assets/font/fira/FiraCode-Bold.ttf
new file mode 100644
index 0000000..fe2dd41
Binary files /dev/null and b/src/app/resources/assets/font/fira/FiraCode-Bold.ttf differ
diff --git a/src/app/resources/assets/font/fira/FiraCode-Light.ttf b/src/app/resources/assets/font/fira/FiraCode-Light.ttf
new file mode 100644
index 0000000..d4a1efe
Binary files /dev/null and b/src/app/resources/assets/font/fira/FiraCode-Light.ttf differ
diff --git a/src/app/resources/assets/font/fira/FiraCode-Medium.ttf b/src/app/resources/assets/font/fira/FiraCode-Medium.ttf
new file mode 100644
index 0000000..459d586
Binary files /dev/null and b/src/app/resources/assets/font/fira/FiraCode-Medium.ttf differ
diff --git a/src/app/resources/assets/font/fira/FiraCode-Regular.ttf b/src/app/resources/assets/font/fira/FiraCode-Regular.ttf
new file mode 100644
index 0000000..f60e430
Binary files /dev/null and b/src/app/resources/assets/font/fira/FiraCode-Regular.ttf differ
diff --git a/src/app/resources/assets/font/fira/FiraCode-SemiBold.ttf b/src/app/resources/assets/font/fira/FiraCode-SemiBold.ttf
new file mode 100644
index 0000000..62557fe
Binary files /dev/null and b/src/app/resources/assets/font/fira/FiraCode-SemiBold.ttf differ
diff --git a/src/app/resources/assets/font/playfair/PlayfairDisplay-Black.ttf b/src/app/resources/assets/font/playfair/PlayfairDisplay-Black.ttf
new file mode 100644
index 0000000..4bc5afd
Binary files /dev/null and b/src/app/resources/assets/font/playfair/PlayfairDisplay-Black.ttf differ
diff --git a/src/app/resources/assets/font/playfair/PlayfairDisplay-BlackItalic.ttf b/src/app/resources/assets/font/playfair/PlayfairDisplay-BlackItalic.ttf
new file mode 100644
index 0000000..7f2e6ee
Binary files /dev/null and b/src/app/resources/assets/font/playfair/PlayfairDisplay-BlackItalic.ttf differ
diff --git a/src/app/resources/assets/font/playfair/PlayfairDisplay-Bold.ttf b/src/app/resources/assets/font/playfair/PlayfairDisplay-Bold.ttf
new file mode 100644
index 0000000..70668c1
Binary files /dev/null and b/src/app/resources/assets/font/playfair/PlayfairDisplay-Bold.ttf differ
diff --git a/src/app/resources/assets/font/playfair/PlayfairDisplay-BoldItalic.ttf b/src/app/resources/assets/font/playfair/PlayfairDisplay-BoldItalic.ttf
new file mode 100644
index 0000000..d25f49e
Binary files /dev/null and b/src/app/resources/assets/font/playfair/PlayfairDisplay-BoldItalic.ttf differ
diff --git a/src/app/resources/assets/font/playfair/PlayfairDisplay-Italic.ttf b/src/app/resources/assets/font/playfair/PlayfairDisplay-Italic.ttf
new file mode 100644
index 0000000..58b8104
Binary files /dev/null and b/src/app/resources/assets/font/playfair/PlayfairDisplay-Italic.ttf differ
diff --git a/src/app/resources/assets/font/playfair/PlayfairDisplay-Regular.ttf b/src/app/resources/assets/font/playfair/PlayfairDisplay-Regular.ttf
new file mode 100644
index 0000000..782c1a4
Binary files /dev/null and b/src/app/resources/assets/font/playfair/PlayfairDisplay-Regular.ttf differ
diff --git a/src/app/resources/assets/font/rajdhani/Rajdhani-Bold.ttf b/src/app/resources/assets/font/rajdhani/Rajdhani-Bold.ttf
new file mode 100644
index 0000000..6c0454c
Binary files /dev/null and b/src/app/resources/assets/font/rajdhani/Rajdhani-Bold.ttf differ
diff --git a/src/app/resources/assets/font/rajdhani/Rajdhani-Light.ttf b/src/app/resources/assets/font/rajdhani/Rajdhani-Light.ttf
new file mode 100644
index 0000000..1c7a361
Binary files /dev/null and b/src/app/resources/assets/font/rajdhani/Rajdhani-Light.ttf differ
diff --git a/src/app/resources/assets/font/rajdhani/Rajdhani-Medium.ttf b/src/app/resources/assets/font/rajdhani/Rajdhani-Medium.ttf
new file mode 100644
index 0000000..e52b403
Binary files /dev/null and b/src/app/resources/assets/font/rajdhani/Rajdhani-Medium.ttf differ
diff --git a/src/app/resources/assets/font/rajdhani/Rajdhani-Regular.ttf b/src/app/resources/assets/font/rajdhani/Rajdhani-Regular.ttf
new file mode 100644
index 0000000..37663a5
Binary files /dev/null and b/src/app/resources/assets/font/rajdhani/Rajdhani-Regular.ttf differ
diff --git a/src/app/resources/assets/font/rajdhani/Rajdhani-SemiBold.ttf b/src/app/resources/assets/font/rajdhani/Rajdhani-SemiBold.ttf
new file mode 100644
index 0000000..f7ec9bb
Binary files /dev/null and b/src/app/resources/assets/font/rajdhani/Rajdhani-SemiBold.ttf differ
diff --git a/src/app/resources/assets/img/profile.jpg b/src/app/resources/assets/img/profile.jpg
new file mode 100644
index 0000000..b48af6d
Binary files /dev/null and b/src/app/resources/assets/img/profile.jpg differ
diff --git a/src/app/resources/assets/img/profile_wide.jpg b/src/app/resources/assets/img/profile_wide.jpg
new file mode 100644
index 0000000..af2ccdb
Binary files /dev/null and b/src/app/resources/assets/img/profile_wide.jpg differ
diff --git a/src/app/resources/assets/img/profile_wide.jpg~ b/src/app/resources/assets/img/profile_wide.jpg~
new file mode 100644
index 0000000..b48af6d
Binary files /dev/null and b/src/app/resources/assets/img/profile_wide.jpg~ differ
diff --git a/src/app/resources/assets/main.css b/src/app/resources/assets/main.css
new file mode 100644
index 0000000..63bf051
--- /dev/null
+++ b/src/app/resources/assets/main.css
@@ -0,0 +1,374 @@
+@font-face {
+ font-family: "Raj";
+ src: url("/assets/font/rajdhani/Rajdhani-Regular.ttf");
+ font-weight: normal;
+ font-display: swap;
+}
+@font-face {
+ font-family: "Raj";
+ src: url("/assets/font/rajdhani/Rajdhani-Bold.ttf");
+ font-weight: bold;
+ font-display: swap;
+}
+@font-face {
+ font-family: "Raj";
+ src: url("/assets/font/rajdhani/Rajdhani-Light.ttf");
+ font-weight: 200;
+ font-display: swap;
+}
+@font-face {
+ font-family: "Raj";
+ src: url("/assets/font/rajdhani/Rajdhani-Medium.ttf");
+ font-weight: 300;
+ font-display: swap;
+}
+@font-face {
+ font-family: "Raj";
+ src: url("/assets/font/rajdhani/Rajdhani-SemiBold.ttf");
+ font-weight: 400;
+ font-display: swap;
+}
+body {
+ background: #5e6572;
+ color: #c9d4e2;
+ font-family: "Raj", sans-serif;
+ margin: 0;
+ padding: 0;
+}
+.container {
+ display: flex;
+ justify-content: center;
+ width: 100%;
+ height: 100%;
+ text-align: left;
+}
+h1 {
+ font-size: 64pt;
+ margin: 0;
+ padding: 0;
+}
+h2 {
+ font-size: 36pt;
+ padding-top: 70px;
+ padding-bottom: 0;
+ margin: 0;
+ font-weight: 300;
+}
+h3 {
+ font-size: 24pt;
+ text-transform: lowercase;
+ margin: 0;
+ padding: 30px 0 0;
+ font-weight: 300;
+}
+h4 {
+ font-size: 18pt;
+ margin: 0;
+ padding: 40px 0 0;
+ font-weight: 300;
+}
+p {
+ font-size: 18pt;
+ font-weight: 300;
+}
+li {
+ font-size: 16pt;
+ font-weight: 300;
+}
+code {
+ font-size: 12pt;
+ background: #3e4552;
+ padding: 5px;
+ border-radius: 5px;
+}
+pre code {
+ display: block;
+}
+.inner {
+ padding-bottom: 10%;
+ max-width: min(70%, 1000px);
+ min-height: 100vh;
+}
+.theme-hide {
+ display: none !important;
+}
+button,
+a.button,
+a.button-small {
+ border: 1px solid #3e4552;
+ font-family: "Raj", sans-serif;
+ font-weight: 300;
+ font-size: 16pt;
+ padding: 5px 20px;
+ border-radius: 5px;
+ transition: background-color 0.1s linear;
+ text-decoration: none;
+ color: #c9d4e2;
+ background: rgba(0, 0, 0, 0);
+}
+button:hover,
+a.button:hover,
+a.button-small:hover {
+ cursor: pointer;
+ background: #3e4552;
+}
+button:active,
+a.button:active,
+a.button-small:active {
+ background: #323742;
+}
+a {
+ color: #c9d4e2;
+}
+a.button-small,
+button.small {
+ padding: 1px 10px;
+ background: #c9d4e2;
+ border: none;
+ color: #3e4552;
+ margin: 5px;
+}
+a.button-small:hover,
+button.small:hover {
+ color: #c9d4e2;
+ background: #3e4552;
+}
+.form-group {
+ margin: 10px;
+}
+.form-group label {
+ font-size: 14pt;
+ font-weight: 300;
+ text-transform: lowercase;
+}
+.form-group input,
+.form-group textarea,
+.form-group select {
+ border: 1px solid #3e4552;
+ border-radius: 5px;
+ padding: 7px;
+ font-size: 14pt;
+ min-width: 400px;
+ background: rgba(0, 0, 0, 0);
+ color: #c9d4e2;
+}
+.form-group input::placeholder,
+.form-group textarea::placeholder,
+.form-group select::placeholder {
+ color: #c9d4e2;
+}
+footer {
+ background: #3e4552;
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ padding-bottom: 50px;
+}
+footer ul {
+ list-style-type: none;
+ margin: 0;
+ padding: 50px 0 0;
+}
+footer ul li {
+ display: inline;
+ padding-left: 20px;
+ padding-right: 20px;
+}
+footer ul li a {
+ color: #a9b4c2;
+ text-decoration: none;
+ font-size: 16pt;
+ font-weight: 300;
+ transition: all 0.5s;
+}
+footer ul li a:hover {
+ color: #ffffff;
+}
+footer .col {
+ margin: 0 30px;
+}
+footer .by-line {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ font-size: 36pt;
+ font-weight: 300;
+ text-align: right;
+ padding-top: 30px;
+}
+footer .copy {
+ font-size: 14pt;
+ font-weight: 200;
+}
+footer ul li {
+ display: block;
+ margin: 10px 0;
+}
+footer #tagline:hover {
+ cursor: pointer;
+}
+footer .auth-container {
+ display: flex;
+ flex-direction: row;
+}
+footer .auth-container .profile {
+ margin-right: 15px;
+ margin-top: 15px;
+}
+.button-links {
+ margin: 20px;
+}
+.button-links a {
+ margin-right: 10px;
+}
+@media (max-width: 480px) {
+ .container {
+ justify-content: left;
+ padding: 20px;
+ width: calc(100% - 40px);
+ }
+ .inner {
+ width: 100%;
+ max-width: unset;
+ }
+ .form-group input,
+ .form-group textarea,
+ .form-group select {
+ min-width: 100%;
+ }
+ footer {
+ flex-direction: column;
+ }
+ footer .by-line {
+ text-align: left;
+ padding: 30px;
+ }
+ footer .links {
+ padding: 20px 0 0;
+ }
+}
+.hero {
+ margin-top: 100px;
+}
+.hero h1 {
+ font-weight: 300;
+ text-transform: lowercase;
+}
+.hero h2 {
+ font-size: 36pt;
+ padding: 0;
+ font-weight: 300;
+ margin: 10px 0 0;
+ text-transform: lowercase;
+}
+.hero p {
+ font-size: 20pt;
+}
+section#about .about-container {
+ display: flex;
+ flex-direction: row;
+}
+section#about .img {
+ flex-grow: 0;
+ flex-shrink: 0;
+}
+section#about .img img {
+ max-width: 300px;
+ margin-top: 30px;
+ margin-right: 20px;
+ border-radius: 5px;
+}
+section#work .timeline-group .timeline-year {
+ font-size: 18pt;
+ margin-top: 40px;
+}
+section#work .timeline-group .timeline-content {
+ background: #3e4552;
+ padding: 20px;
+ border-radius: 5px;
+ margin: 20px;
+}
+section#work .timeline-group .timeline-content h3 {
+ padding: 0;
+}
+section#work .timeline-group .timeline-content p {
+ margin: 0;
+ font-size: 16pt;
+}
+section#contact .contact-container {
+ display: flex;
+ flex-direction: row;
+}
+section#contact .contact-container .message {
+ text-align: right;
+ margin-right: 20px;
+}
+section#recent .feed-item {
+ background: #3e4552;
+ padding: 20px;
+ margin: 20px;
+ border-radius: 5px;
+}
+section#recent .feed-item .feed-category {
+ display: flex;
+}
+section#recent .feed-item .feed-category .tag {
+ background: #5e6572;
+ padding: 2px 5px;
+ border-radius: 2px;
+ font-size: 14pt;
+}
+section#recent .feed-item .bottom {
+ display: flex;
+ flex-direction: row;
+ justify-content: end;
+}
+section#recent .feed-item .bottom .stamp {
+ margin-left: 20px;
+ font-size: 14pt;
+ font-weight: 300;
+ text-align: right;
+ flex: 1;
+}
+section#recent a.button {
+ margin-left: 20px;
+}
+section#auth {
+ max-width: 500px;
+}
+section#auth h3 {
+ margin-bottom: 30px;
+}
+@media (max-width: 480px) {
+ .hero h1 {
+ font-size: 48pt;
+ }
+ section#about .about-container {
+ display: flex;
+ flex-direction: column;
+ }
+ section#work .timeline-content {
+ margin: 20px 0 !important;
+ }
+ section#contact .contact-container {
+ display: flex;
+ flex-direction: column;
+ }
+ section#contact .contact-container .message {
+ text-align: left;
+ }
+ section#recent .feed-item {
+ margin: 20px 0;
+ }
+ section#recent a.button {
+ margin-left: 0;
+ }
+ section#recent .bottom {
+ flex-wrap: wrap;
+ }
+ section#recent .bottom .stamp {
+ max-width: 100%;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+ }
+}
diff --git a/src/app/resources/assets/welcome.js b/src/app/resources/assets/welcome.js
new file mode 100644
index 0000000..69590a6
--- /dev/null
+++ b/src/app/resources/assets/welcome.js
@@ -0,0 +1,37 @@
+window.glmdev = window.glmdev || {}
+
+window.glmdev.taglines = [
+ '...is proud that this site is Google-free',
+ '...is a supporter of FLOSS',
+ '...prefers JavaScript, but Python will do',
+ '...occasionally rants about dependency injection',
+ '...loves GNU/Linux',
+ '(or as I\'ve recently taken to calling it, GNU+Linux)',
+ '...uses spaces, not tabs',
+ '...self-hosts all the things',
+ '...occasionally does contract work',
+ '...reads the terms and conditions',
+ '...enjoys r/sysadmin',
+ '...just lost the game',
+ '...is running out of tag-line ideas',
+ `copyright © ${(new Date()).getFullYear()} garrett mills`,
+]
+
+document.querySelector('#tagline')
+ .addEventListener('click', event => {
+ if ( typeof glmdev.tagline_index === 'undefined' ) glmdev.tagline_index = 0
+ else if ( glmdev.tagline_index === glmdev.taglines.length - 1 ) glmdev.tagline_index = 0
+ else glmdev.tagline_index += 1
+
+ document.querySelector('#tagline').innerHTML = glmdev.taglines[glmdev.tagline_index]
+ })
+
+document.querySelector('#timeline-view-all')
+ .addEventListener('click', event => {
+ const hidden = document.querySelectorAll('.work-container.theme-hide')
+ for ( const item of hidden ) {
+ item.classList.remove('theme-hide')
+ }
+
+ document.querySelector('#timeline-view-all').classList.add('theme-hide')
+ }, false)
diff --git a/src/app/resources/views/feed.pug b/src/app/resources/views/feed.pug
new file mode 100644
index 0000000..3d28631
--- /dev/null
+++ b/src/app/resources/views/feed.pug
@@ -0,0 +1,23 @@
+extends template_raj
+
+block content
+ .container#top
+ .inner
+ .hero
+ h1 garrettmills
+ section#recent
+ h2 posts & updates
+ .button-links
+ a.button-small(href='/feed/rss.xml') rss
+ a.button-small(href='/feed/atom.xml') atom
+ a.button-small(href='/feed/json.json') json
+ each item in feedPosts
+ .feed-item
+ .feed-category(id=item.feedPostId)
+ .tag #{item.tag}
+ span
+ if !item.visible
+ p.text (draft)
+ p.text !{item.body}
+ .bottom
+ .stamp permalink | #{item.postedAt.toLocaleString()}
diff --git a/src/app/resources/views/template_raj.pug b/src/app/resources/views/template_raj.pug
new file mode 100644
index 0000000..13e1463
--- /dev/null
+++ b/src/app/resources/views/template_raj.pug
@@ -0,0 +1,65 @@
+doctype html
+head
+ block meta
+ meta(charset='utf-8')
+ meta(name='viewport' content='width=device-width, initial-scale=1, shrink-to-fit=no')
+ meta(http-equiv='x-ua-compatible' content='ie=edge')
+ meta(name='description' content='Hi, there! My name is Garrett. I am a self-taught software developer and speaker.')
+ meta(name='keywords' content='garrett mills glmdev developer speaker flitter extollo student')
+ meta(name='author' content=config('app.name', 'Garrett Mills'))
+ meta(name='robots' content='index, follow')
+
+ block title
+ title #{config('app.name', 'Garrett Mills')} | Developer, Speaker, Designer
+
+ block style
+ link(rel='stylesheet' href=asset('main.css'))
+
+ link(rel='author' href='humans.txt')
+ link(rel="alternate" href="/feed/atom.xml" title="Garrett Mills - Posts & Updates" type="application/atom+xml")
+ link(rel="alternate" href="/feed/rss.xml" title="Garrett Mills - Posts & Updates" type="application/rss+xml")
+
+body
+ block content
+
+ footer
+ .ff-tag(style="margin-right: 80px")
+ a(href="https://www.mozilla.org/en-US/firefox/browsers/" target="_blank")
+ img(src="/assets/ffox.png" width="75" style="margin-top: -1px")
+ .by-line garrettmills
+ .copy#tagline(title="my, aren't you curious...") copyright © #{(new Date()).getFullYear()} garrett mills
+ if false
+ div.auth-container
+ div.profile
+ img(src=user.profile_url width=50 style="border-radius: 50%")
+ p Welcome, #{user.first_name} #{user.last_name} | Log out
+ .col
+ ul.links
+ li
+ a(href='/#home') home
+ li
+ a(href='/#about') about me
+ li
+ a(href='/#work') what I'm working on
+ .col
+ ul.links
+ li
+ a(href='/#contact') get in touch
+ li
+ a(href='/#recent') latest updates
+ li
+ a(href='https://static.garrettmills.dev/Resume.pdf' target='_blank') résumé
+ .col
+ ul.links
+ li
+ a(href='/blog') blog
+ li
+ a(href='https://code.garrettmills.dev/garrettmills' target='_blank') my code
+ li
+ a(href='/technical') technical info
+ if false
+ li
+ a(href='/dash/main') dashboard
+
+ block script
+ script(src=asset('welcome.js'))
diff --git a/src/app/resources/views/welcome.pug b/src/app/resources/views/welcome.pug
index 7649fb1..ebd88d0 100644
--- a/src/app/resources/views/welcome.pug
+++ b/src/app/resources/views/welcome.pug
@@ -1,6 +1,85 @@
-html
- head
- title !{locale('app_name')}
- body
- h1 !{locale('welcome_to_extollo')}
- h2 !{locale('viewed_page_num_times', { interp: { num: app_visits } })}
+extends template_raj
+block content
+ .container#home
+ .inner
+ .hero
+ h1 garrettmills
+ p Hi, there. My name is Garrett, and I'm a self-taught software-developer and speaker.
+ section#about
+ h2 about me
+ .about-container
+ .img
+ img(src="/assets/img/profile.jpg" width=300 height=300)
+ .about
+ p Hi! My name is Garrett. Welcome to my corner of the internet. I'm a self-taught developer and maker. I like to build software to improve the developer/user experience. I created the Extollo framework, Noded, CoreID authentication server, and a couple other projects. I love to communicate my work, and help others pursue their projects. I write blog posts, create video tutorials, hold talks, and publish code from my projects in the hope that others will find it useful.
+ p A bit more background: I grew up in the rural mid-west, and I got started by teaching myself everything I know. I'm a big fan of learning to code this way. I'm currently studying computer science at the University of Kansas.
+
+ if workItemYears && workItemYears.length
+ section#work
+ h2 what I'm working on
+ .timeline-group
+ each year in workItemYears
+ .work-container(class=(workItemHiddenYears.includes(year) ? 'timeline-item theme-hide' : 'timeline-item'))
+ .timeline-container
+ .timeline-year #{year}
+
+ each item in workItemGroups[year]
+ .work-container(class=(item.endDate ? 'timeline-item theme-hide' : 'timeline-item'))
+ .timeline-container
+ .timeline-content
+ .range-small #{item.rangeDisplay()}
+ h3 !{item.name}
+ p !{item.description}
+ .work-container
+ button#timeline-view-all show past work
+
+ section#contact
+ h2 get in touch
+ .contact-container
+ .message
+ p I'd love to hear from you if you have questions or inquiries related to me or my projects. You can get in touch by text, e-mail, or using this form. I also occasionally share thoughts on my blog.
+ p E-mail: shout@garrettmills.dev
+ .form
+ form#contact-form
+ .form-group
+ input#contactEmail.form-control(type='email' name='email' placeholder='E-Mail Address' required)
+ .form-group
+ input#contactFirst.form-control(name='first' placeholder='First Name' required)
+ .form-group
+ input#contactLast.form-control(name='last' placeholder='Last Name' required)
+ .form-group
+ textarea.form-control#contactMessage(name='message' placeholder='Message' required rows=6)
+ .form-group
+ button Send
+
+ section#recent
+ h2 latest updates
+ each item in feedPosts
+ .feed-item
+ .feed-category(id='feedPostTag_' + item.feedPostId)
+ .tag #{item.tag}
+ span
+ if !item.visible
+ p.text (draft)
+ p.text !{item.body}
+ .bottom
+ .stamp permalink | #{item.postedAt.toLocaleString()}
+ // each item in feed_items
+ // .feed-item
+ // div.feed-category(id='feedPostTag_' + item.id)
+ // .tag #{item.tag}
+ // span
+ // if item.draft
+ // p.text (draft)
+ // p.text !{item.text}
+ // .bottom
+ // if user && can_access.feed_delete && can_access.feed_edit
+ // .stamp edit | delete
+ // .stamp permalink | #{item.date.toLocaleString()}
+ if feed_overflow
+ .row.mt-4
+ .col-12.text-center
+ a.button(href="/feed") view more
+
+block append script
+ script(src="/assets/js/raj/welcome.js")